当前位置: 首页 > 科技观察

在SwiftUI的HStack和VStack之间切换

时间:2023-03-17 11:01:25 科技观察

前言SwiftUI的各种栈是很多框架中最基本的布局工具,可以让我们定义组视图,可以水平对齐,垂直对齐,也可以叠加视图。当涉及水平和垂直变体(HStack和VStack)时,我们需要在两者之间动态切换。例如,假设我们正在构建一个包含LoginActionsView的应用程序,该类允许用户在登录时从列表中选择操作:structLoginActionsView:View{...varbody:someView{VStack{Button("Login"){...}Button("重置密码"){...}Button("创建帐户"){...}.buttonStyle(ActionButtonStyle())}}structActionButtonStyle:ButtonStyle{funcmakeBody(configuration:Configuration)->someView{configuration.label.fixedSize().frame(maxWidth:.infinity).padding().foregroundColor(.white).background(Color.blue).cornerRadius(10)}}在上面的代码中,我们使用fixedSize防止按钮文本被截断,前提是我们确定给定的内容视图不会大于视图本身。更多信息,可以查看我的文章——SwiftUI布局系统第3章目前,我们的按钮是垂直排列的,并填充水平线上的可用空间(你可以使用上面的示例代码来预览按钮的样子),而这在纵向模式下的iPhone上看起来不错,假设我们现在希望UI在横向模式下水平对齐。GeometryReaderGeometryReader能实现吗?一种方式是使用GeometryReader来衡量当前的可用空间,根据宽度是否大于它的高度,你可以选择使用HStack或VStack来渲染内容。虽然逻辑可以放在LoginActionsView中,但是我们希望以后复用代码,所以需要重新创建一个专门的视图作为一个独立的组件来实现动态栈的切换逻辑。为了使代码更有用,我们不对两个堆栈变体进行硬编码以使用对齐或间距。相反,让我们像SwiftUI那样参数化这些属性,并设置框架使用的默认值——像这样:?@ViewBuildervarcontent:()->Contentvarbody:someView{GeometryReader{proxyinGroup{ifproxy.size.width>proxy.size.height{HStack(alignment:verticalAlignment,spacing:spacing,content:content)}}else{VStack(alignment:horizo??ntalAlignment,spacing:spacing,content:content)}}}}}由于我们让新的DynamicStack使用与HStack和VStack相同的API,我们现在可以直接在LoginActionsView中将之前的VStack替换为新的自定义实例:structLoginActionsView:View{...varbody:someView{DynamicStack{Button("Login"){...}Button("重置密码"){...}Button("Createaccount"){...}}.buttonStyle(ActionButtonStyle())}}太棒了!然而,正如上面的代码所展示的那样,使用GeometryReader来显示动态切换有一个相当明显的缺点,因为在我们的示例中,GeometryReader将始终在水平和垂直方向(为了测量实际空间)填充所有可用空间,LoginActionsView不再只是水平排列,它现在也可以移动到屏幕顶部。虽然我们有很多方法可以解决这些问题(例如使用像本问答中使用的技术使多个视图具有相同的宽度和高度),但真正的问题是在我们想要动态确定方向时测量可用空间.是不是好方法。使用大小类的示例相反,让我们使用Apple的大小类系统来决定DynamicStack应该在底层使用HStack还是VStack。这样做的好处不仅是在引入GeometryReader之前保留相同的紧凑布局,而且还使DynamicStack从一开始就以与系统组件类似的方式构建在所有设备和方向上。为了观察当前水平方向的大小,我们需要使用SwiftUI环境系统——通过在DynamicStack中声明@Environment-标签属性(带horizo??ntalSizeClass关键路径),它会把我们切换到当前的sizeClass值视图内容:structDynamicStack:View{...@Environment(\.horizo??ntalSizeClass)privatevarsizeClassvarbody:someView{switchsizeClass{case.regular:hStackcase.compact,.none:vStack@未知默认值:vStack}}}私有扩展DynamicStack{varhStack:一些视图{HStack(对齐方式:verticalAlignment,间距:间距,内容:内容)}varvStack:一些视图{VStack(对齐方式:horizo??ntalAlignment,间距:间距,内容:content)}}通过以上操作,LoginActionsView在常规尺寸渲染时,将能够动态切换到水平布局(例如,在大尺寸iPhone上使用横屏,或者在全屏上使用任意方向iPad),瓦特所有其他尺寸配置均使用垂直布局。所有这些仍然使用紧凑的垂直布局,它使用的空间不超过呈现其内容所需的空间。使用布局协议虽然我们最终得到了一个适用于所有支持SwiftUI的iOS版本的很棒的解决方案,但让我们也探索iOS16中引入的一些新布局工具(在撰写本文时,它仍处于测试阶段作为Xcode14的一部分)这些工具之一就是新的Layout协议,它不仅可以让我们创建完整的自定义布局,直接集成到SwiftUI的布局系统中,还为我们提供了一种更流畅、更流畅地在各种布局之间动态切换的功能动画方式。这都是因为事实证明,Layout不仅仅是我们第三方开发者的API,Apple也让SwiftUI自己的布局容器使用了这个新协议。因此,与其将HStack和VStack直接用作容器视图,不如将它们包装为使用AnyLayout类型包装的符合布局的实例——像这样:case.compact:returnverticalLayout@unknowndefault:returnverticalLayout}}varhorizo??ntalLayout:AnyLayout{AnyLayout(HStack(alignment:verticalAlignment,spacing:spacing))}varverticalLayout:AnyLayout{AnyLayout(VStack(alignment:horizo??ntalAlignment,spacing:spacing)复制代码)}}上面之所以可以,是因为当HStack和VStack的内容类型为EmptyView(即内容为空时)时,HStack和VStack都符合新的Layout协议,我们来看看SwiftUI的公共接口。structDynamicStack:View{...varbody:someView{currentLayout(content)}}注意:根据MattRicketson的说法,由于回归,上述条件的一致性在Xcode14beta3中被省略SwiftUI团队的换句话说,您可以直接使用底层的_HStackLayout和_VStackLayout类型作为临时解决方案。希望它将在未来的测试版中得到修复。现在我们可以通过使用新的currentLayout来确定要使用的布局,让我们更新主体实现以简单地调用从该属性返回的AnyLayout,就像函数一样—像这样:structDynamicStack:View{。..varbody:someView{currentLayout(content)}}我们可以像函数一样调用布局方法的原因(即使它实际上是一个结构体)是因为Layout协议使用了Swift的“像函数一样调用”特性。那么我们之前的方案和上面的layout-based方案有什么区别呢?关键区别在于(除了后者需要iOS16之外)切换布局会保留正在渲染的底层视图的身份,而在HStack和VStack之间切换则不会。这样做会导致更流畅的动画,例如切换设备方向时,我们也可能在执行此类更改时获得小幅性能提升(因为SwiftUI在其视图层次结构为静态时始终表现最佳)。选择合适的视图但是我们还没有完成,因为iOS16还为我们提供了其他有趣的新布局工具,这些工具也可能用于实现DynamicStack——一种名为ViewThatFits的全新视图类型。顾名思义,这个新容器将根据我们在初始化时传递的候选列表中的当前上下文选择最佳视图。在我们的例子中,这意味着我们可以将HStack和VStack都传递给它,它会代表我们自动在它们之间切换。structDynamicStack:View{...varbody:someView{ViewThatFits{HStack(alignment:verticalAlignment,spacing:spacing,content:content)VStack(对齐:horizo??ntalAlignment,spacing:spacing,content:content)}}注意:在这种情况下,我们首先放置HStack很重要,因为VStack可能总是合适的,即使在我们希望布局为横向的情况下(例如iPad的全屏模式)。同样重要的是要指出,上面描述的基于ViewThatFits的技术将始终尝试HStack,即使在渲染具有紧凑尺寸的布局时也是如此,并且只有在HStack不适合时才选择基于VStack的布局。结语以上就是通过四种不同的方式实现DynamicStack视图,可以根据当前内容在HStack和VStack之间动态切换。