✨views with same width
Last updated
Last updated
利用 PreferenceKey 來對齊本來寬度不同的 TextField。
// ⭐️ PreferenceKey:
// 1. 準備接收所有 Text 的最大寬度。
typealias MaxWidth = MaxValue<CGFloat> // 📦 MaxValue<T: FloatingPoint>
// live view
struct ContentView: View {
var body: some View {
VStack {
MyForm() // 🌅 MyForm
Color.pink.cornerRadius(12)
}
.padding()
.background(Color.gray)
.cornerRadius(16)
.shadow(color: .black, radius: 6, x: 6, y: 6)
}
}
當一開始在佈局 (layout) 的時候,就要修改 child view 的尺寸的話,通常不能在 child view 的 🌅 GeometryReader 中直接修改 parent view 的 @State 變數。 👉 SwiftUI GeometryReader failed to update its parent's state
這時最好是:
使用 🅿️ PreferenceKey ,讓 child view 間接告訴 parent view 它要的尺寸是什麼
child view 事先用 parent view 的 @State 變數設定自己的尺寸
再由 parent view 透過 .onPreferenceChange() 來主動更新它的 @State 變數,然後帶動 child view 的更新。
// 🌅 MyForm
struct MyForm: View {
@State var value1 = ""
@State var value2 = ""
@State var value3 = ""
// ⭐️ label 的統一寬度
@State private var labelWidth: CGFloat?
var body: some View {
Form {
// 🌅 MyTextField
MyTextField(
labelWidth : $labelWidth, // 承接來自母物件的「統一寬度」
label : "姓名",
placeholder: "請輸入您的名字",
text : $value1
)
MyTextField(
labelWidth : $labelWidth,
label : "電話",
placeholder: "0900-000-123",
text : $value2
)
MyTextField(
labelWidth : $labelWidth,
label : "電子信箱",
placeholder: "email@company.com",
text : $value3
)
}// Form
.cornerRadius(12)
// ⭐️ 2. 母物件根據回報的寬度,準備更新自己的 labelWidth
.onPreferenceChange(MaxWidth.self) { self.labelWidth = $0 }
}
}
// 🌅 MyTextField
// -----------------------------------------------------------
// 主功能:
// 1. 會向 `MaxWidth` (PreferenceKey) 報告自己 label 的寬度。
// 2. 設定自己 label 的寬度為 `labelWidth` (⭐️ 來自 parent view)。
// (剛開始為 nil,表示不強制設定寬度,由 label 自己決定寬度)
// 3. 只要來自 parent view 的 `labelWidth` 更新了,自己就會跟著更新。
// -----------------------------------------------------------
struct MyTextField: View {
@Binding var labelWidth: CGFloat? // label's width (⭐️ 由 parent @State 決定)
@Binding var text : String // text field's content (⭐️ 同上)
let label : String // text field's label
let placeholder: String // text field's placeholder
init(labelWidth: Binding<CGFloat?>, label: String, placeholder: String, text: Binding<String>) {
self._labelWidth = labelWidth
self._text = text
self.label = label
self.placeholder = placeholder
}
var body: some View {
HStack {
// label
Text(label)
// ⭐️ 3. 向 MaxWidth 回報自己的寬度
.reportWidth(to: MaxWidth.self) // 🌀View + pref, 📦 MaxWidth
// 設定尺寸與靠右對齊
.frame(width: labelWidth, alignment: .trailing)
// text field
TextField(placeholder, text: $text)
.style(.rounded) // 🌀TextField + style
}
}
}
2020.10.13: ➖ 🌅 SizeReporter ➕ 🌀View + report(width: key)
2020.10.14: ✏️ 微修改,讓語義更通順。 ✏️ view.report(width: key) -> view.reportWidth(to: key) ➕ view.reportFrame(to: key, in: space)
2020.10.15: ✏️ MaxWidth 改為 generic MaxValue<T: FloatingPoint>
import SwiftUI
import PlaygroundSupport
// 📦 MaxWidth
struct MaxWidth: PreferenceKey {
// default value (nil == not set)
static let defaultValue: CGFloat? = nil
// ⭐️ choose max width
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
if let next = nextValue() {
value = max(next , value ?? 0)
}
}
}
// 🌅 SizeReporter
struct SizeReporter: View {
var body: some View {
GeometryReader { geo in // ⭐️ 1. read Text's size
Color.clear
// ⭐️ 2. set preference:
// tell parent view its preferred width
// - 🌀GeometryProxy + 🅿️ Rectangular
.preference(key: MaxWidth.self, value: geo.width)
}
}
}
// 🌅 TextFieldWithPreference
struct TextFieldWithPreference: View {
@Binding var width: CGFloat? // label's preferred width ⭐️
@Binding var text : String // text field's content
let label: String // text field's label
let placeholder: String // text field's placeholder
init(_ width: Binding<CGFloat?>, label: String, placeholder: String, text: Binding<String>) {
self._width = width
self._text = text
self.label = label
self.placeholder = placeholder
}
var body: some View {
HStack {
// label
Text(label)
// ⭐️ 1. report size to parent
.background(SizeReporter())
// ⭐️ 2. set optional width ⭐️ 3. set alignment
.frame(width: width, alignment: .trailing)
// text field
TextField(placeholder, text: $text)
.style(.rounded) // 🌀TextField + style
}
}
}
// 🌅 MyForm
struct MyForm: View {
@State var value1 = ""
@State var value2 = ""
@State var value3 = ""
// textfield label's preferred width
@State private var width: CGFloat?
var body: some View {
Form {
TextFieldWithPreference($width, label: "First Item", placeholder: "Enter first item", text: $value1)
TextFieldWithPreference($width, label: "Second Item", placeholder: "Enter second item", text: $value2)
TextFieldWithPreference($width, label: "Third Item", placeholder: "Enter third item", text: $value3)
}// Form
.cornerRadius(12)
.onPreferenceChange(MaxWidth.self) { self.width = $0 }
}
}
struct ContentView: View {
var body: some View {
MyForm()
.padding()
.background(Color.gray)
.cornerRadius(16)
.shadow(color: .black, radius: 6, x: 6, y: 6)
}
}
// live view
PlaygroundPage.current.setLiveView(ContentView())
import SwiftUI
import PlaygroundSupport
// 📦 MaxWidth
public struct MaxWidth: PreferenceKey {
// default value (nil == not set)
public static let defaultValue: CGFloat? = nil
// ⭐️ choose max width
public static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
if let next = nextValue() {
value = max(next , value ?? 0)
}
}
}
// 🌅 MyTextField
// -----------------------------------------------------------
// 主功能:
// 1. 會向 `MaxWidth` (PreferenceKey) 報告自己 label 的寬度。
// 2. 設定自己 label 的寬度為 `labelWidth` (⭐️ 來自 parent view)。
// (剛開始為 nil,表示不強制設定寬度,由 label 自己決定寬度)
// 3. 只要來自 parent view 的 `labelWidth` 更新了,自己就會跟著更新。
// -----------------------------------------------------------
struct MyTextField: View {
@Binding var labelWidth: CGFloat? // label's width (⭐️ 由 parent @State 決定)
@Binding var text : String // text field's content (⭐️ 同上)
let label : String // text field's label
let placeholder: String // text field's placeholder
init(labelWidth: Binding<CGFloat?>, label: String, placeholder: String, text: Binding<String>) {
self._labelWidth = labelWidth
self._text = text
self.label = label
self.placeholder = placeholder
}
var body: some View {
HStack {
// label
Text(label)
// ⭐️ 1. report text's width to `MaxWidth` PreferenceKey
// (wait for parent to update its `maxWidth`)
.reportWidth(to: MaxWidth.self) // 🌀View + report, 📦 MaxWidth
// ⭐️ 2. set width with `labelWidth`(optional), and alignment
.frame(width: labelWidth, alignment: .trailing)
// text field
TextField(placeholder, text: $text)
.style(.rounded) // 🌀TextField + style
}
}
}
// 🌅 MyForm
struct MyForm: View {
@State var value1 = ""
@State var value2 = ""
@State var value3 = ""
// ⭐️ textfield label's width
@State private var labelWidth: CGFloat?
var body: some View {
Form {
// 🌅 MyTextField
// ⭐️ 1. report width to `MaxWidth`
// 2. set label's width to `labelWidth`
MyTextField(
labelWidth : $labelWidth,
label : "姓名",
placeholder: "請輸入您的名字",
text : $value1
)
MyTextField(
labelWidth : $labelWidth,
label : "電話",
placeholder: "0900-000-123",
text : $value2
)
MyTextField(
labelWidth : $labelWidth,
label : "電子信箱",
placeholder: "email@company.com",
text : $value3
)
}// Form
.cornerRadius(12)
// ⭐️ 3. parent view responses to preference change
// (update its `labelWidth`)
.onPreferenceChange(MaxWidth.self) { self.labelWidth = $0 }
}
}
struct ContentView: View {
var body: some View {
VStack {
MyForm() // 🌅 MyForm
Color.pink.cornerRadius(12)
}
.padding()
.background(Color.gray)
.cornerRadius(16)
.shadow(color: .black, radius: 6, x: 6, y: 6)
}
}
// live view
PlaygroundPage.current.setLiveView(ContentView())
import SwiftUI
import PlaygroundSupport
/*
* ⭐️ 使用 K: PreferenceKey 的三大要素
* 1. PreferenceKey 本身
* 2. parent view 的 @State 變數:通常型別與 K.Value 相同
* 3. child view 的 @Binding 變數:可以承接來自 parent view 的更新通知。
*
* ⭐️ 處理過程三部曲:
* 1. 由子物件回報值給 K:用 child.preference(key:value:) 回報
* 2. 由 K 負責處理回報的值:用 K.reduce() 處理
* 3. 由母物件根據回報的值,更新自己的 @State:用 parent.onPreferenceChange() 處理
*
* 子物件回報 --> PreferenceKey 處理 --> 母物件更新
*/
// ⭐️ 所有 Text 的最大寬度
typealias MaxWidth = MaxValue<CGFloat> // 📦 MaxValue<T: FloatingPoint>
// 🌅 MyTextField
// -----------------------------------------------------------
// 主功能:
// 1. 會向 `MaxWidth` (PreferenceKey) 報告自己 label 的寬度。
// 2. 設定自己 label 的寬度為 `labelWidth` (⭐️ 來自 parent view)。
// (剛開始為 nil,表示不強制設定寬度,由 label 自己決定寬度)
// 3. 只要來自 parent view 的 `labelWidth` 更新了,自己就會跟著更新。
// -----------------------------------------------------------
struct MyTextField: View {
@Binding var labelWidth: CGFloat? // label's width (⭐️ 由 parent @State 決定)
@Binding var text : String // text field's content (⭐️ 同上)
let label : String // text field's label
let placeholder: String // text field's placeholder
init(labelWidth: Binding<CGFloat?>, label: String, placeholder: String, text: Binding<String>) {
self._labelWidth = labelWidth
self._text = text
self.label = label
self.placeholder = placeholder
}
var body: some View {
HStack {
// label
Text(label)
// ⭐️ 1. 向 MaxWidth 回報自己的寬度
.reportWidth(to: MaxWidth.self) // 🌀View + pref, 📦 MaxWidth
// ⭐️ 2. 承接來自母物件的 labelWidth (optional),並設定尺寸
// ⭐️ 3. 設定靠右
.frame(width: labelWidth, alignment: .trailing)
// text field
TextField(placeholder, text: $text)
.style(.rounded) // 🌀TextField + style
}
}
}
// 🌅 MyForm
struct MyForm: View {
@State var value1 = ""
@State var value2 = ""
@State var value3 = ""
// ⭐️ TextField 的統一寬度 (三部曲:由子視件回報,MaxWidth 處理,母視件更新)
@State private var labelWidth: CGFloat?
var body: some View {
Form {
// 🌅 MyTextField
MyTextField(
labelWidth : $labelWidth, // 承接來自母物件的「統一寬度」
label : "姓名",
placeholder: "請輸入您的名字",
text : $value1
)
MyTextField(
labelWidth : $labelWidth,
label : "電話",
placeholder: "0900-000-123",
text : $value2
)
MyTextField(
labelWidth : $labelWidth,
label : "電子信箱",
placeholder: "email@company.com",
text : $value3
)
}// Form
.cornerRadius(12)
// ⭐️ 3. 母物件根據回報的寬度,更新自己的 labelWidth
.onPreferenceChange(MaxWidth.self) { self.labelWidth = $0 }
}
}
struct ContentView: View {
var body: some View {
VStack {
MyForm() // 🌅 MyForm
Color.pink.cornerRadius(12)
}
.padding()
.background(Color.gray)
.cornerRadius(16)
.shadow(color: .black, radius: 6, x: 6, y: 6)
}
}
// live view
PlaygroundPage.current.setLiveView(ContentView())