本文為 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 = https://www.cnblogs.com/bruce2077/p/max(value, nextValue()),然后在 onPreferenceChange 中將最大值傳給 textWidth ,這樣不是更簡單嗎?但是事與愿違,這樣達不到我們想要的效果,觀察控制臺,筆者發現確實可以獲取到最大寬度值,但是不會更新視圖布局,百思不得其解,網上也沒找到合理的解釋,如果有讀者明白其中的奧妙,請不吝賜教,筆者先在此謝過,
iOS 開發,獨立作品: ① Eul:SwiftUI & Combine 簡明教程 ② FontsX:在任意 app 輸入特殊字體 詳情請戳: https://apps.apple.com/cn/developer/ke-zeng/id1322330151本文為 Eul 樣章,如果您喜歡,請移步 AppStore/Eul 查看更多內容,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/308627.html
標籤:iOS
