SwiftUI - 獲取目標視圖 Frame
省流
文章地址:Github
代碼實現:FrameGetter
需要獲取 Frame 的場景
通常我們使用 SwiftUI 佈局 UI 時,利用 VStack、HStack 等即可約束不同 View 之間的位置關係,因此不會再需要獲取指定 View 的 Frame。但是有些時候為了實現複雜的頁面,或者為了一些佈局相關的計算時,就不得不獲取 Frame。
比如下面這個場景,為了實現下拉時放大標題的效果,必須要獲取 ScrollView 的 minY,從而計算出頂部區域的放大倍數:
初嘗使用 GeometryReader
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 協議,直接這樣寫編譯器無法推斷。
You create custom views by declaring types that conform to the
View
protocol. Implement the requiredbody
computed property to provide the content for your custom view.
我們將佈局代碼提取出來,明確返回值即可:
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 關鍵字修飾的屬性。
這個問題的原因在於,@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)
}
}