太长不看版,代码实现:FrameGetter
通常我们使用 SwiftUI 布局 UI 时,利用 VStack、HStack 等即可约束不同 View 之间的位置关系,因此不会再需要获取指定 View 的 Frame。但是有些时候为了实现复杂的页面,或者为了一些布局相关的计算时,就不得不获取 Frame。
比如下面这个场景,为了实现下拉时放大标题的效果,必须要获取 ScrollView 的 minY,从而计算出顶部区域的放大倍数:
A container view that defines its content as a function of its own size and coordinate space.
使用 GeometryReader 可以在布局时获取到对应 View 在指定坐标系的布局信息,我们可以利用这个特点将 frame 信息读取出来。
@State var frame: CGRect = .zero
var body: some View {
GeometryReader { geometry in
self.frame = geometry.frame(in: .global)
MainContent()
}
}
当然我们不能直接设置,因为 block 内的内容必须要遵循 View 协议,直接这样写编译器无法推断。
You create custom views by declaring types that conform to the
View
protocol. Implement the requiredbody
computed property to provide the content for your custom view.
当然,我们将布局代码提取出来,明确返回值即可:
var body: some View {
GeometryReader { geometry in
makeView(geometry)
}
}
func makeView(_ geometry: GeometryProxy) -> some View {
print(geometry.size.width, geometry.size.height)
self.frame = geometry.frame(in: .global)
return MainContent()
}
不过现在有新的问题产生:在布局阶段,不能够更改 @State 关键字修饰的属性。
这个问题的原因在于,@State 关键字的含义简单来说,其实就是 View 的状态;而如果在计算 View 的时候更改 @State,那就有可能造成时序上的混乱,导致布局错误,所以必须要将 @State 变量的修改延后一个周期。
于是现在的完整代码变成了:
struct ContentView: View {
@State var frame: CGRect = .zero
var body: some View {
GeometryReader { (geometry) in
self.makeView(geometry)
}
}
func makeView(_ geometry: GeometryProxy) -> some View {
print(geometry.size.width, geometry.size.height)
DispatchQueue.main.async { self.frame = geometry.frame(in: .global) }
return MainContent()
}
}
首先,我们将已有的部分提取到 ViewModifier 中,当然需要一定的改造 —— 将 GeometryReader 的部分放入 background 中使用。这样做既不会影响布局的获取,又能快捷优雅,并且避免 body 直接返回 GeometryReader 导致的类型推断错误。
extension View {
func frameGetter(_ frame: Binding<CGRect>) -> some View {
modifier(FrameGetter(frame: frame))
}
}
struct FrameGetter: ViewModifier {
@Binding var frame: CGRect
func body(content: Content) -> some View {
content
.background(
GeometryReader { proxy -> AnyView in
let rect = proxy.frame(in: .global)
DispatchQueue.main.async {
self.frame = rect
}
return AnyView(EmptyView())
})
}
}
使用起来也很简单:
struct MyView: View {
@State private var frame: CGRect = CGRect()
var body: some View {
Rectangle()
.frameGetter($frame)
}
}