> For the complete documentation index, see [llms.txt](https://lochiwei.gitbook.io/ios/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lochiwei.gitbook.io/ios/swiftui/data-flow/preferences/examples/same-width.md).

# views with same width

{% tabs %}
{% tab title="💈 範例 " %}
{% hint style="info" %}
利用 [PreferenceKey](/ios/swiftui/data-flow/preferences/preferencekey.md) 來<mark style="color:red;">**對齊**</mark>本來<mark style="color:red;">**寬度不同**</mark>的 [TextField](/ios/swiftui/control/textfield.md)。
{% endhint %}

![](/files/-MJ_0Ssvy-3iiYCgVV7E)

```swift
// ⭐️ 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)
    }
}
```

{% endtab %}

{% tab title="⭐️ 重點" %}
{% hint style="danger" %}
當一開始在佈局 (layout) 的時候，就要修改 child view 的尺寸的話，通常不能在 child view 的 [🌅  GeometryReader](/ios/swiftui/view/measure/geometryreader.md) 中直接修改 parent view 的 @State 變數。\
👉  [SwiftUI GeometryReader failed to update its parent's state](https://stackoverflow.com/q/64310736/5409815)
{% endhint %}

{% hint style="success" %}
這時最好是：

1. 使用 [🅿️ PreferenceKey](/ios/swiftui/data-flow/preferences/preferencekey.md) ，讓 child view 間接告訴 parent view 它要的尺寸是什麼
2. child view 事先用 parent view 的 @State 變數設定自己的尺寸
3. 再由 parent view 透過 .**onPreferenceChange**() 來主動更新它的 @State 變數，然後帶動 child view 的更新。
   {% endhint %}
   {% endtab %}

{% tab title="🌅  MyForm" %}

```swift
// 🌅 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 }
    }
}
```

{% endtab %}

{% tab title="🌅  MyTextField" %}

```swift
// 🌅 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
        }
        
    }
}
```

{% endtab %}

{% tab title="👥  相關 " %}

* [🌀 TextField + style](/ios/swiftui/control/textfield/styles/textfield-+-style.md)
* [🌀 GeometryProxy](/ios/swiftui/view/measure/geometryproxy/geometryproxy.md) + [🅿️  Rectangular](/ios/custom/package/geometrykit/frame.md)
* [🎛️ TextField](/ios/swiftui/control/textfield.md)
* [🌀View + report](/ios/swiftui/data-flow/preferences/examples/same-width/view+pref.md)  ⭐️
* [🅿️ PreferenceKey](/ios/swiftui/data-flow/preferences/preferencekey.md)
* [Preferences](broken://pages/-MIqXYu-EqovuJbj2W-M)
  {% endtab %}

{% tab title="📗 參考" %}

* [x] [透過 PreferenceKey 簡單對齊視圖](https://www.appcoda.com.tw/swiftui-preferencekey/) - AppCoda
* [x] [Inspecting the View Tree – Part 1: PreferenceKey](https://swiftui-lab.com/communicating-with-the-view-tree-part-1/) - The SwiftUI Lab
  {% endtab %}
  {% endtabs %}

## 修改紀錄

{% tabs %}
{% tab title="✏️  紀錄" %}

* 2020.10.13：\
  ➖ 🌅 SizeReporter\
  ➕ [🌀View + report(width: key)](/ios/swiftui/data-flow/preferences/examples/same-width/view+pref.md)
* 2020.10.14：\
  ✏️  微修改，讓語義更通順。\
  ✏️  view\.report(width: key) -> [view.reportWidth(to: key)](/ios/swiftui/data-flow/preferences/examples/same-width/view+pref.md)\
  ➕  view\.reportFrame(to: key, in: space)
* 2020.10.15：\
  ✏️ MaxWidth 改為 generic [MaxValue\<T: FloatingPoint>](/ios/swiftui/data-flow/preferences/preferencekey.md)
  {% endtab %}

{% tab title="2020.10.13" %}

```swift
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())
```

{% endtab %}

{% tab title="2020.10.14" %}

```swift
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())
```

{% endtab %}

{% tab title="2020.10.15" %}

```swift
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())
```

{% endtab %}
{% endtabs %}
