AnyLayout
是SwiftUI
中的一个类型擦除容器,它可以包装任何遵循Layout
协议的布局。这意味着我们可以使用AnyLayout
来抽象具体的布局类型,从而在运行时决定使用哪种布局。这种灵活性极大地增强了UI组件的可重用性和适应性。
AnyLayout
可以在保持视图identifier
的同时在布局(Layout
)之间进行转换。
iOS 16以前的处理
在iOS 16以前,如果如果想要改变布局,或者在横竖屏切换的时候改变布局,我们一般都是通过if
判断来处理加载不同布局的视图,比如下面:
struct AnylayoutDemo: View {@Environment(\.horizontalSizeClass) var horizontalSizeClassvar body: some View {VStack {if horizontalSizeClass == .compact {VStack {Text("One")Text("Two")Text("Three")}} else {HStack {Text("One")Text("Two")Text("Three")}}}.font(.largeTitle)}
}
横竖屏切换的时候horizontalSizeClass
会变化,进而加载不同布局的视图。在iPhone上不是所有设备的horizontalSizeClass
在横竖屏切换的时候都会变化,本文只是示例。
上面代码中horizontalSizeClass
变化的时候,SwiftUI
渲染if
不同分支的代码,这相当于移除一部分组件,又添加了一部分新组件上去。
AnyLayout
引入AnyLayout
是为了平滑布局之间的过渡。它是Layout
协议的类型擦除实例。使用AnyLayout
实例允许我们在不破坏底层子视图状态的情况下改变布局类型。
因为视图层次结构的标识(identifier
)始终保持不变,SwiftUI将其视为一个改变的视图,而不是像if-else那样的新视图。
现在将上面的代码采用AnyLayout
,如下:
struct AnylayoutDemo: View {@Environment(\.horizontalSizeClass) var horizontalSizeClassvar body: some View {let layout = horizontalSizeClass == .compact ? AnyLayout(VStackLayout()) : AnyLayout(HStackLayout())VStack {layout {Text("One")Text("Two")Text("Three")}}.font(.largeTitle)}
}
上面代码中通过horizontalSizeClass
不同的值创建了AnyLayout
的实例layout
,而该layout
代替了HStack
和VStack
。
AnyLayout
中使用的类型必须符合Layout
协议。SwiftUI提供了四个新版本的布局HStackLayout
, VStackLayout
, ZStackLayout
和GridLayout
。
自定义AnyLayout
在SwiftUI
中,实现自定义布局涉及到遵循Layout
协议。这个协议要求实现下面两个方法。
下面,我将通过一个简单的例子来展示如何创建一个自定义布局,这个布局将会简单地将子视图水平排列,并且在它们之间添加固定的间距。
// 用于计算布局所需的总体尺寸。这个方法需要返回一个 CGSize,表示布局的理想大小。
func sizeThatFits(proposal: ProposedViewSize,subviews: Subviews,cache: inout ()
) -> CGSize
// 用于放置子视图。你需要指定每个子视图的位置。
func placeSubviews(in bounds: CGRect,proposal: ProposedViewSize,subviews: Subviews,cache: inout ()
)
首先,定义一个结构体,让它遵循Layout
协议。
struct SimpleHorizontalLayout: Layout {let spacing: CGFloat // 用于定义子视图之间的间距init(spacing: CGFloat) {self.spacing = spacing}
}
这里,SimpleHorizontalLayout
结构体有一个spacing
属性,用于控制子视图之间的间距。
实现sizeThatFits
方法:
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {var totalWidth: CGFloat = 0var maxHeight: CGFloat = 0for subview in subviews {let subviewSize = subview.sizeThatFits(proposal)totalWidth += subviewSize.widthmaxHeight = max(maxHeight, subviewSize.height)}totalWidth += CGFloat(subviews.count - 1) * spacing // 添加间距return CGSize(width: totalWidth, height: maxHeight)
}
实现placeSubviews
方法:
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {var currentX: CGFloat = bounds.minXfor subview in subviews {let subviewSize = subview.sizeThatFits(proposal)subview.place(at: CGPoint(x: currentX, y: bounds.midY - subviewSize.height / 2), anchor: .topLeading, proposal: ProposedViewSize(width: subviewSize.width, height: subviewSize.height))currentX += subviewSize.width + spacing}
}
最终,在调用的地方:
var body: some View {let layout = AnyLayout(SimpleHorizontalLayout(spacing: 10))layout {Text("One")Text("Two")Text("Three")}.font(.largeTitle)
}
自定义Layout
完整代码如下:
struct SimpleHorizontalLayout: Layout {let spacing: CGFloat // 用于定义子视图之间的间距init(spacing: CGFloat) {self.spacing = spacing}func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {var totalWidth: CGFloat = 0var maxHeight: CGFloat = 0for subview in subviews {let subviewSize = subview.sizeThatFits(proposal)totalWidth += subviewSize.widthmaxHeight = max(maxHeight, subviewSize.height)}totalWidth += CGFloat(subviews.count - 1) * spacing // 添加间距return CGSize(width: totalWidth, height: maxHeight)}func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {var currentX: CGFloat = bounds.minXfor subview in subviews {let subviewSize = subview.sizeThatFits(proposal)subview.place(at: CGPoint(x: currentX, y: bounds.midY - subviewSize.height / 2), anchor: .topLeading, proposal: ProposedViewSize(width: subviewSize.width, height: subviewSize.height))currentX += subviewSize.width + spacing}}
}
写在最后
本文主要探索了一下iOS 16以前和以后针对不同布局的一些写法,在iOS 16及以后,我们可以使用AnyLayout做界面的动态布局,整体上都好于iOS 16以前的写法,此外,我们也可以自定义Layout,实现我们想要的布局。
最后,希望能够帮助到有需要的朋友,如果您觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。