⚡️ Advanced SwiftUI Layout System

2023/11/29

布局法则:父 view 提供建议的 size 然后子 view 根据建议以及自身特性进行布局。

Frame

minWidth idealWidth maxWidth
minHeight idealHeight maxHeight
width height alignment

idealWidth 需要搭配 .fixedSize(horizontal: true, vertical: true) 使用。

修改 frame 相当于修改了父 view 建议的 size

GeometryReader

用于获取父 view 建议的 size

struct ContentView: View {
    var body: some View {
        VStack {
            GeometryReader { geo in 
                Text("width: \(geo.size.width), height: \(geo.sieze.height)")
            }
        }
    }
}

geometryProxy 定义(iOS16):

public struct GeometryProxy {
  public var size: CGSize { get } // 父 view 建议的 size。
  public subscript<T>(anchor: Anchor<T>) -> T { get } // 获取 .leading, .top 等类似的数据
  public var safeAreaInsets: EdgeInsets { get } // 获取安全区域的值
  public func frame(in coordinateSpace: CoordinateSpace) -> CGRect // 传入 CoordinateSpace 类型参数[.local, .global, .named("")]
}

强大的 geo.frame(in) ,能够获取到某个 view 相对某个坐标空间的 bounds。

struct ContentView: View {
  var body: some View {
    VStack {
      Spacer()

      GeometryReader { geo in 
                Text("CoordinateSpace: \(geo.frame(in: .named("OutVStack")).minY)")        
                Text("Global: \(geo.frame(in: .global).minY)")
      }

      Spacer()
    }
    .frame(height: 200 )
    .border(Color.green)
    .coordinateSpace(name: "OutVStack")
  }
}

Alignment Guide

alignment-guide

Container Alignment

Preference

用于父 view 获取子 view 信息的场景,核心思想有2个:

  1. *设置 PreferenceKey 和自定义的 PreferenceData ,把子 view 的信息绑定到 PreferenceData上,子 view 通过 .preference(key:, value:) 修饰符进行绑定。*
  2. 父 view 根据 PreferenceKey 获取到所有子 view 的 PreferenceData ,通过 .onPreferenceChange 获取到信息。
  • 🔨 示例代码

    // 固定写法
    struct NumberPreferenceValue: Equatable {
      let viewIdx: Int
      let rect: CGRect
    }
    struct NumberPreferenceKey: PreferenceKey {
      typealias Value = [NumberPreferenceValue]
      
      static var defaultValue: Value = []
      static func reduce(value: inout Value, nextValue: () -> Value) {
        value.append(contentsOf: nextValue())
      }
    }
    
    // 子 view 上进行绑定
    struct NumberView: View {
      let idx: Int
    
      var body: some View {
        GeometryReader { geo in
          Text("\(self.idx)")
            .background(GeometryReader { geo2 in
              // 获取全局坐标
              Color.clear.preference(
                key: NumberPreferenceKey.self, 
                value: [NumberPreferenceValue(viewIdx: self.idx, rect: geo2.frame(in: .global))]
              )
            })
        }
      }
    }
    // 父 view 获取所有子 view 信息
    struct ContentView: View {
      var body: some View {
        VStack {
          NumberView(idx: 0)
          NumberView(idx: 1)
        }
        .onPreferenceChange(NumberPreferenceKey.self) { prefs in
          // 这里获取到所有信息
          for pref in prefs {
            print("viewIdx: \(pref.viewIdx), rect: \(pref.rect)")
          }
        }
      }
    }
    

Stacks

HStack / VStack / ZStack 在无其他约束条件下,其特性表现为:尽量满足子 views 的布局要求,并且自身最终的布局 size 取决于子 views 的 size。ZStack 中后面的子 view 覆盖在前面的 view。

Spacer

搭配 Stacks 使用,特性是在某个方向上占据更多空间。参数 minLength 为最小占据空间。

LayoutPriority

布局优先级