SwiftUI 布局中,background 和 overlay 不会改变主视图的尺寸,布局容器 frame 直接使用主视图的 frame。因此我们可以通过 background + GeometryReader 来实现实时获取视图尺寸。
通过把 Color.clear
放置在 background 的 GeometryReader 中实时读取尺寸:
swift
struct SizeCalculator: ViewModifier {
@Binding var size: CGSize
func body(content: Content) -> some View {
content
.background(
GeometryReader { proxy in
let geoSize = proxy.size
Color.clear
.onAppear {
size = geoSize
}.onChange(of: geoSize) { newValue in
size = geoSize
}
}
)
}
}
特别要强调除了注册 onAppear
还需要注册 onChange
。如果只观察了 onAppear
就会出现在同一个页面手机横屏布局刷新,视图尺寸没有更新的问题。因为我们的 Color.clear 一直都在界面上,onAppear 不会被再次调用,因此不会更新尺寸。View 每一次刷新 GeometryReader 都会被重新调用,因此我们通过在 GeometryReader 的闭包中声明一个变量用来观察尺寸是否变化。
为了让获取尺寸更加方面,我们可以给 View 声明一个扩展:
swift
extension View {
func readSize(in size: Binding<CGSize>) -> some View {
modifier(SizeCalculator(size: size))
}
}
使用的方式如下:
swift
struct SizeReader: View {
@State var size: CGSize = .zero
var body: some View {
VStack {
Text("text width: \(size.width)")
Text("text height: \(size.height)")
Color.gray
.readSize(in: $size)
}
}
}
除了使用 Color.clear 作为占位内容,我们还可以使用 Path 作为内容层。因为每次 View 刷新,Path 都会被重新渲染,因此在 Path 闭包中获取尺寸也是可行的。实现如下:
swift
struct SizeCalculator: ViewModifier {
@Binding var size: CGSize
func body(content: Content) -> some View {
content
.background(
GeometryReader { proxy in
Path { path in
DispatchQueue.main.async {
if size != proxy.size {
size = proxy.size
}
}
}
}
)
}
}
因此 Path 天然每次都会刷新,因此相比 Color.clear 可以省掉注册 onAppear 和 onChange。但是 Path 有一个需要注意的地方是因为我们是在 Path 内容渲染中设置了状态值 size 改变,因此每次设置 size 都会引发 View 的重新渲染。为了避免死循环,在设置 size 值的时候我们包在一个异步主线程中更新,并且设置的时候判断尺寸是否修改。
我们获取尺寸还有一个场景是在开发过程中为了方便调试打印尺寸到控制台。在这个场景下直接使用 Path 代码就会简洁了一些了。
swift
extension View {
func _printSize() -> some View {
self.background(
GeometryReader { proxy in
Path { path in
print("frame size = \(proxy.size)")
}
}
)
}
}