经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » iOS » 查看文章
SwiftUI 简明教程之 GeometryReader、PreferenceKey
来源:cnblogs  作者:Bruce2077  时间:2021/6/28 9:30:20  对本文有异议

本文为 Eul 样章,如果您喜欢,请移步 AppStore/Eul 查看更多内容。

Eul 是一款 SwiftUI & Combine 教程 App(iOS、macOS),以文章(文字、图片、代码)配合真机示例(Xcode 12+、iOS 14+,macOS 11+)的形式呈现给读者。笔者意在尽可能使用简洁明了的语言阐述 SwiftUI & Combine 相关的知识,使读者能快速掌握并在 iOS 开发中实践。

GeometryReader

GeometryReader 是一个通过闭包来构建视图的容器,可以返回一个 GeometryProxy 类型的结构体,它包含如下属性和方法,由此我们可以获取当前视图容器(即父视图)的尺寸和位置,绘制以其为参考坐标系的视图。

  1. var safeAreaInsets: EdgeInsets
  2. // The safe area inset of the container view.
  3. var size: CGSize
  4. // The size of the container view.
  5. func frame(in: CoordinateSpace) -> CGRect
  6. // Returns the container view’s bounds rectangle, converted to a defined coordinate space.

比如,我们需要绘制一个长宽均为父视图一半的矩形:

  1. struct ContentView: View {
  2. var body: some View {
  3. GeometryReader { gr in
  4. RoundedRectangle(cornerRadius: 10)
  5. .fill(Color.blue)
  6. .frame(width: gr.size.width * 0.5, height: gr.size.height * 0.5)
  7. .position(x: gr.frame(in: .local).midX, y: gr.frame(in: .local).midY)
  8. }
  9. }
  10. }

我们再来看看 GeometryProxy 包含的实例方法:func frame(in: CoordinateSpace) -> CGRect,这里的 CoordinateSpace 是个枚举类型,有以下几种情况:

  1. case global // 参考系为屏幕
  2. case local // 参考系为父视图
  3. case named(AnyHashable) // 参考系为自定义

通过这个方法,我们可以获取到当前视图在不同参考系中的位置和尺寸,我们将代码改成如下:

  1. struct ContentView: View {
  2. var body: some View {
  3. VStack(spacing: 10) {
  4. text("Top", width: 100, height: 50)
  5. HStack(spacing: 10) {
  6. text("Left", width: 50, height: 100)
  7. roundRect
  8. .background(Color.black)
  9. text("Right", width: 50, height: 100)
  10. }
  11. text("Bottom", width: 100, height: 50)
  12. }
  13. .coordinateSpace(name: "VStack")
  14. }
  15. var roundRect: some View {
  16. GeometryReader { gr in
  17. RoundedRectangle(cornerRadius: 10)
  18. .fill(Color.blue)
  19. .frame(width: gr.size.width * 0.5, height: gr.size.height * 0.5)
  20. .position(x: gr.frame(in: .local).midX, y: gr.frame(in: .local).midY)
  21. .onTapGesture {
  22. print("screen: \(UIScreen.main.bounds)")
  23. print("global: \(gr.frame(in: .global))")
  24. print("local: \(gr.frame(in: .local))")
  25. print("custom: \(gr.frame(in: .named("VStack")))")
  26. }
  27. }
  28. }
  29. func text(_ text: String, width: CGFloat, height: CGFloat) -> some View {
  30. Text(text)
  31. .frame(width: width, height: height)
  32. .background(Color.orange)
  33. .cornerRadius(10)
  34. }
  35. }

运行模拟器 iPhone 12 Pro(safeAreaInsets: 47.0, 0.0, 34.0, 0.0),点击蓝色区域,控制台打印如下结果:

  1. screen: (0.0, 0.0, 375.0, 812.0)
  2. global: (60.0, 148.0, 255.0, 570.0)
  3. local: (0.0, 0.0, 255.0, 570.0)
  4. custom: (60.0, 60.0, 255.0, 570.0)

这与我们之前所说的枚举类型对应的坐标参考系是一致的。

PreferenceKey

还记得我们在前面的“自定义对齐方式”中讲过的,如何对齐手机和电子邮箱的例子吗?其实,我们还有另外一种思路来实现类似的效果,那就是获取文字列所有的内容的宽度,取最大值,重绘界面即可。那么问题来了,如何获取这个最大值呢?答案就是 PreferenceKey,它可以收集视图树中子视图的数据,回传给父视图(跨层级亦可)。这里我们需要获取尺寸,还用到了 GeometryReader。

  1. struct ContentView : View {
  2. @State private var email = ""
  3. @State private var password = ""
  4. // 保存、更新文字列所需要的合适宽度,这里是最大值
  5. @State private var textWidth: CGFloat?
  6. var body: some View {
  7. Form {
  8. HStack {
  9. Text("电子邮箱")
  10. .frame(width: textWidth, alignment: .leading)
  11. .background(TextBackgroundView())
  12. TextField("请输入", text: $email)
  13. .textFieldStyle(RoundedBorderTextFieldStyle())
  14. }
  15. HStack {
  16. Text("密码")
  17. .frame(width: textWidth, alignment: .leading)
  18. .background(TextBackgroundView())
  19. TextField("请输入", text: $email)
  20. .textFieldStyle(RoundedBorderTextFieldStyle())
  21. }
  22. }
  23. .onPreferenceChange(TextWidthPreferenceKey.self) { (value) in
  24. print(value)
  25. textWidth = value.max()
  26. }
  27. }
  28. }
  29. struct TextBackgroundView: View {
  30. var body: some View {
  31. GeometryReader { gr in
  32. Rectangle()
  33. .fill(Color.clear)
  34. .preference(key: TextWidthPreferenceKey.self,
  35. value: [gr.size.width])
  36. }
  37. }
  38. }
  39. struct TextWidthPreferenceKey: PreferenceKey {
  40. // 偏好值没有被设定时,使用默认值
  41. static var defaultValue: [CGFloat] = []
  42. // 收集视图树中的数据
  43. // nextValue 的闭包是惰性调用的,只有需要用到它时才会去获取相应的值
  44. static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
  45. value.append(contentsOf: nextValue())
  46. }
  47. }

有一点需要注意,为什么我们要使用 TextBackgroundView 来作为背景回传所需要的值呢?因为我们期望 Form 列表的布局是根据子视图的布局来更新的,而子视图又依赖父视图传入的宽度值,这样形成了一个得不到结果的死循环。而 TextBackgroundView 可以打破这个僵局,父视图所依赖的布局不再是文字的布局,而是背景层的视图布局。

补充说明一下,SwiftUI 的视图层级是不同于 UIKit 的,在 UIKit 中,背景是控件的属性,而 SwiftUI 中,.background 会在视图树中生成一个新的视图,是独立与所修饰的控件的。

另外有一点令笔者不解的是,既然我们是要获取最大宽度,只需要在 TextWidthPreferenceKey 将关联类型设置为 CGFloat 即可,在 reduce 方法中写入 value = max(value, nextValue()),然后在 onPreferenceChange 中将最大值传给 textWidth ,这样不是更简单吗?但是事与愿违,这样达不到我们想要的效果,观察控制台,笔者发现确实可以获取到最大宽度值,但是不会更新视图布局,百思不得其解,网上也没找到合理的解释。如果有读者明白其中的奥妙,请不吝赐教,笔者先在此谢过。

本文为 Eul 样章,如果您喜欢,请移步 AppStore/Eul 查看更多内容。

原文链接:http://www.cnblogs.com/bruce2077/p/14911984.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号