省流

文章地址Github

代码完成:FrameGetter

需要获取 Frame 的场景

一般咱们运用 SwiftUI 布局 UI 时,利用 VStack、HStack 等即可约束不同 View 之间的位置联系,因而不会再需要获取指定 View 的 Frame。但是有些时候为了完成杂乱的页面,或者为了一些布局相关的核算时,就不得不获取 Frame。

比方下面这个场景,为了完成下拉时扩大标题的效果,必需要获取 ScrollView 的 minY,从而核算出顶部区域的扩大倍数:

SwiftUI - 获取目标视图 Frame

初尝运用 GeometryReader

GeometryReader – Apple doc

A container view that defines its content as a function of its own size and coordinate space.

运用 GeometryReader 能够在布局时获取到对应 View 在指定坐标系的布局信息,咱们能够利用这个特点将 frame 信息读取出来。

@State var frame: CGRect = .zero
​
var body: some View {
  GeometryReader { geometry in
    self.frame = geometry.frame(in: .global)
    MainContent()
  }
}

当然咱们不能直接设置,因为 block 内的内容必需要遵循 View 协议,直接这样写编译器无法揣度。

View – Apple doc

You create custom views by declaring types that conform to the View protocol. Implement the required body computed property to provide the content for your custom view.

SwiftUI - 获取目标视图 Frame

咱们将布局代码提取出来,明确返回值即可:

var body: some View {
  GeometryReader { geometry in
    makeView(geometry)
  }
}
​
func makeView(_ geometry: GeometryProxy) -> some View {
  print(geometry.size.width, geometry.size.height)
  self.frame = geometry.frame(in: .global)
  return MainContent()
}

不过现在有新的问题产生:在布局阶段,不能够更改 @State 关键字修饰的属性。

SwiftUI - 获取目标视图 Frame

这个问题的原因在于,@State 关键字的意义简略来说,其实便是 View 的状态;而如果在核算 View 的时候更改 @State,那就有可能造成时序上的混乱,导致布局错误,所以必需要将 @State 变量的修正拖延一个周期。

于是现在的完好代码变成了:

struct ContentView: View {
  
  @State var frame: CGRect = .zero
  
  var body: some View {
    GeometryReader { (geometry) in
      self.makeView(geometry)
    }
  }
  
  func makeView(_ geometry: GeometryProxy) -> some View {
    print(geometry.size.width, geometry.size.height)
    DispatchQueue.main.async { self.frame = geometry.frame(in: .global) }
    return MainContent()
  }
}

终究完成方便的 Extension: FrameGetter

首要,咱们将已有的部分提取到 ViewModifier 中,当然需要必定的改造 —— 将 GeometryReader 的部分放入 background 中运用。这样做既不会影响布局的获取,又能方便优雅,并且避免 body 直接返回 GeometryReader 导致的类型揣度错误。

extension View {
  func frameGetter(_ frame: Binding<CGRect>) -> some View {
    modifier(FrameGetter(frame: frame))
  }
}
​
struct FrameGetter: ViewModifier {
  @Binding var frame: CGRect
  
  func body(content: Content) -> some View {
    content
      .background(
        GeometryReader { proxy -> AnyView in
          let rect = proxy.frame(in: .global)
          DispatchQueue.main.async {
            self.frame = rect
          }
          return AnyView(EmptyView())
        })
  }
}

运用起来也很简略:

struct MyView: View {
  @State private var frame: CGRect = CGRect()
​
  var body: some View {
    Rectangle()
      .frameGetter($frame)
  }
}