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 的闭包中声明一个变量用来观察尺寸是否变化。
extension View { func readSize(in size: Binding<CGSize>) -> some View { modifier(SizeCalculator(size: size)) } }使用的方式如下:
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 闭包中获取尺寸也是可行的。实现如下:
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 值的时候我们包在一个异步主线程中更新,并且设置的时候判断尺寸是否修改。
extension View { func _printSize() -> some View { self.background( GeometryReader { proxy in Path { path in print("frame size = \(proxy.size)") } } ) } }