SwiftUI 简明教程之 GeometryReader、PreferenceKey
本文为 eul 样章,如果您喜欢,请移步 appstore/eul 查看更多内容。
eul 是一款 swiftui & combine 教程 app(ios、macos),以文章(文字、图片、代码)配合真机示例(xcode 12+、ios 14+,macos 11+)的形式呈现给读者。笔者意在尽可能使用简洁明了的语言阐述 swiftui & combine 相关的知识,使读者能快速掌握并在 ios 开发中实践。
geometryreader
geometryreader
是一个通过闭包来构建视图的容器,可以返回一个 geometryproxy
类型的结构体,它包含如下属性和方法,由此我们可以获取当前视图容器(即父视图)的尺寸和位置,绘制以其为参考坐标系的视图。
var safeareainsets: edgeinsets // the safe area inset of the container view. var size: cgsize // the size of the container view. func frame(in: coordinatespace) -> cgrect // returns the container view’s bounds rectangle, converted to a defined coordinate space.
比如,我们需要绘制一个长宽均为父视图一半的矩形:
struct contentview: view { var body: some view { geometryreader { gr in roundedrectangle(cornerradius: 10) .fill(color.blue) .frame(width: gr.size.width * 0.5, height: gr.size.height * 0.5) .position(x: gr.frame(in: .local).midx, y: gr.frame(in: .local).midy) } } }
我们再来看看 geometryproxy
包含的实例方法:func frame(in: coordinatespace) -> cgrect
,这里的 coordinatespace
是个枚举类型,有以下几种情况:
case global // 参考系为屏幕 case local // 参考系为父视图 case named(anyhashable) // 参考系为自定义
通过这个方法,我们可以获取到当前视图在不同参考系中的位置和尺寸,我们将代码改成如下:
struct contentview: view { var body: some view { vstack(spacing: 10) { text("top", width: 100, height: 50) hstack(spacing: 10) { text("left", width: 50, height: 100) roundrect .background(color.black) text("right", width: 50, height: 100) } text("bottom", width: 100, height: 50) } .coordinatespace(name: "vstack") } var roundrect: some view { geometryreader { gr in roundedrectangle(cornerradius: 10) .fill(color.blue) .frame(width: gr.size.width * 0.5, height: gr.size.height * 0.5) .position(x: gr.frame(in: .local).midx, y: gr.frame(in: .local).midy) .ontapgesture { print("screen: \(uiscreen.main.bounds)") print("global: \(gr.frame(in: .global))") print("local: \(gr.frame(in: .local))") print("custom: \(gr.frame(in: .named("vstack")))") } } } func text(_ text: string, width: cgfloat, height: cgfloat) -> some view { text(text) .frame(width: width, height: height) .background(color.orange) .cornerradius(10) } }
运行模拟器 iphone 12 pro(safeareainsets: 47.0, 0.0, 34.0, 0.0),点击蓝色区域,控制台打印如下结果:
screen: (0.0, 0.0, 375.0, 812.0) global: (60.0, 148.0, 255.0, 570.0) local: (0.0, 0.0, 255.0, 570.0) custom: (60.0, 60.0, 255.0, 570.0)
这与我们之前所说的枚举类型对应的坐标参考系是一致的。
preferencekey
还记得我们在前面的“自定义对齐方式”中讲过的,如何对齐手机和电子邮箱的例子吗?其实,我们还有另外一种思路来实现类似的效果,那就是获取文字列所有的内容的宽度,取最大值,重绘界面即可。那么问题来了,如何获取这个最大值呢?答案就是 preferencekey
,它可以收集视图树中子视图的数据,回传给父视图(跨层级亦可)。这里我们需要获取尺寸,还用到了 geometryreader。
struct contentview : view { @state private var email = "" @state private var password = "" // 保存、更新文字列所需要的合适宽度,这里是最大值 @state private var textwidth: cgfloat? var body: some view { form { hstack { text("电子邮箱") .frame(width: textwidth, alignment: .leading) .background(textbackgroundview()) textfield("请输入", text: $email) .textfieldstyle(roundedbordertextfieldstyle()) } hstack { text("密码") .frame(width: textwidth, alignment: .leading) .background(textbackgroundview()) textfield("请输入", text: $email) .textfieldstyle(roundedbordertextfieldstyle()) } } .onpreferencechange(textwidthpreferencekey.self) { (value) in print(value) textwidth = value.max() } } } struct textbackgroundview: view { var body: some view { geometryreader { gr in rectangle() .fill(color.clear) .preference(key: textwidthpreferencekey.self, value: [gr.size.width]) } } } struct textwidthpreferencekey: preferencekey { // 偏好值没有被设定时,使用默认值 static var defaultvalue: [cgfloat] = [] // 收集视图树中的数据 // nextvalue 的闭包是惰性调用的,只有需要用到它时才会去获取相应的值 static func reduce(value: inout [cgfloat], nextvalue: () -> [cgfloat]) { value.append(contentsof: nextvalue()) } }
有一点需要注意,为什么我们要使用 textbackgroundview
来作为背景回传所需要的值呢?因为我们期望 form 列表的布局是根据子视图的布局来更新的,而子视图又依赖父视图传入的宽度值,这样形成了一个得不到结果的死循环。而 textbackgroundview
可以打破这个僵局,父视图所依赖的布局不再是文字的布局,而是背景层的视图布局。
补充说明一下,swiftui 的视图层级是不同于 uikit 的,在 uikit 中,背景是控件的属性,而 swiftui 中,.background
会在视图树中生成一个新的视图,是独立与所修饰的控件的。
另外有一点令笔者不解的是,既然我们是要获取最大宽度,只需要在 textwidthpreferencekey
将关联类型设置为 cgfloat
即可,在 reduce
方法中写入 value = max(value, nextvalue())
,然后在 onpreferencechange
中将最大值传给 textwidth
,这样不是更简单吗?但是事与愿违,这样达不到我们想要的效果,观察控制台,笔者发现确实可以获取到最大宽度值,但是不会更新视图布局,百思不得其解,网上也没找到合理的解释。如果有读者明白其中的奥妙,请不吝赐教,笔者先在此谢过。
本文为 eul 样章,如果您喜欢,请移步 appstore/eul 查看更多内容。