> 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/master/todo/exercises/circletext.md).

# CircleText

在文字外面加一個有顏色的圓圈，圓圈會隨著文字變大而變大。

## 設計重點

* 在一開始佈局的時候，請 Text 回報自己的「最大邊」(定義在：[🌀 GeometryProxy](/ios/swiftui/view/measure/geometryproxy/geometryproxy.md) + [🅿️  Rectangular](/ios/custom/package/geometrykit/frame.md)) 給 MaxSide ([🅿️ PreferenceKey](/ios/swiftui/data-flow/preferences/preferencekey.md))，然後 CircleText 再透過 .**onPreferenceChange**()，用回報來的值更新自己的 maxSide @State 變數，進一步推動 Text 更新自己的 frame。

## 程式碼

![](https://1830103165-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M5-JmwCZMKh_d7RfBaN%2F-MJW3j5VqItxMJrb559c%2F-MJW3zkhC8lSwmQA5DMp%2FCircleText.png?alt=media\&token=f01766c9-4a54-4a61-80b3-37f8e6559ee3)

{% tabs %}
{% tab title="💈 範例" %}

```swift
// live view
struct ContentView: View {
    var body: some View {
        HStack {
            
            Group {
                CircleText("Hello, World")           // 🌅 CircleText
                CircleText("Hello", color: .blue)
            }// Group
                // ⭐️ 這裡可以確定 CircleText's frame 已經包含整個圓。
                .border(Color.black)
            
        }// HStack (container)
            .padding()
            .background(Color.gray)
            .shadow(radius: 8).shadow(radius: 8)
    }
}

PlaygroundPage.current.setLiveView(ContentView())
```

{% endtab %}

{% tab title="🌅  CircleText" %}

```swift
// 🌅 CircleText
struct CircleText: View {
    
    // ⭐️ Text 回報的最大邊長
    @State private var maxSide: CGFloat?
    
    // local variables
    let text : String         // text of Text
    let color: Color          // bg color of Text
    
    // init
    init(_ text: String, color: Color = .pink) {
        self.text  = text
        self.color = color
    }
    
    // view body
    var body: some View {
        
        Text(text).padding()
            
            // ⭐️ 1. set (optional) frame
            .frame(width: maxSide, height: maxSide)
            // ⚠️ 2. 要先設定好 frame 再加上 Circle，不然 Circle 只有
            //       Text + padding 的高度 ‼️
            .background(Circle().fill(color))
            
            // ⭐️ 3. 回報自己的「最大邊」給 📦 MaxSide
            //       - report(to:): 🌀View + pref
            //       - $0.maxSide : 🌀GeometryProxy + 🅿️Rectangular
            .report(to: MaxSide.self) { $0.maxSide }
            
            // ⭐️ 4. 母視圖根據回報的值，更新自己的 @State 變數 `maxSide`
            .onPreferenceChange(MaxSide.self) { self.maxSide = $0 }
    }
}
```

{% endtab %}

{% tab title="📦  MaxSide" %}

```swift
// ⭐️ PreferenceKey 所取的名字最好是跟母視件的 @State 變數一致，例如：
//    此例要找的是 Text 的最大邊，所以取 CircleText 的 @State 變數
//    名為： `maxSide`，而這個 PreferenceKey 就取名為: `MaxSide`。
typealias MaxSide = FirstNonNil<CGFloat>      // 📦 FirstNonNil<T>
```

{% endtab %}

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

* [🅿️ PreferenceKey](/ios/swiftui/data-flow/preferences/preferencekey.md) (FirstNonNil) ⭐️
* [🌀View + pref](/ios/swiftui/data-flow/preferences/examples/same-width/view+pref.md)
* [🅿️  Rectangular](/ios/custom/package/geometrykit/frame.md)
* [🌀 GeometryProxy](/ios/swiftui/view/measure/geometryproxy/geometryproxy.md)
  {% endtab %}

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

* [x] Thinking in Swift, Ch. 5 - Custom Layout\
  p.91, Preferences and GeometryReaders
  {% endtab %}
  {% endtabs %}

## 修改記錄

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

* 初版
* 2020.10.15：
  * 將 SizePreferenceKey 改為 generic [FirstNonNil\<T>](/ios/swiftui/data-flow/preferences/preferencekey.md)
  * 新增 generic [view.report(to: key) {...}](/ios/swiftui/data-flow/preferences/examples/same-width/view+pref.md) 統一各種 view\.report 函數。
    {% endtab %}

{% tab title="初版" %}

```swift
/*
 * Required:
 * - 🌀CGRect + .frame
 * - 🌀GeometryProxy + 🅿️ Rectangular
 */
 
import SwiftUI
import PlaygroundSupport
 
// ⭐️ custom preference key
struct SizePreferenceKey: PreferenceKey {
    // default value
    static let defaultValue: CGSize? = nil
    // combine values from different child views
    static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) {
        value = value ?? nextValue()   // nil or first non-nil value
    }
}

// 🌅 CircleText
struct CircleText: View {
    
    // ⭐️ Text's favorite size (a square)
    @State private var size: CGSize?
    
    // local variables
    let text : String         // text of Text
    var color: Color = .pink  // bg color of Text
    
    // view body
    var body: some View {
        Text(text)
            .padding()
            // ⭐️ 1. set optional frame (🌀CGRect + .frame)
            .frame(size)
            // ⭐️ 2. set bg color
            .background(GeometryReader { geo in  // ⭐️ 3. read Text's size
                self.color
                    // ⭐️ 4. set preference:
                    //       tell parent view its favorite size (a square)
                    //     - 🌀GeometryProxy + 🅿️ Rectangular
                    .preference(key: SizePreferenceKey.self, value: geo.boundingSquare.size)
            })
            .clipShape(Circle())
            // ⭐️ 5. parent view update itself in response to the preference change
            .onPreferenceChange(SizePreferenceKey.self) { self.size = $0 }
    }
}

// live view
struct ContentView: View {
    var body: some View {
        HStack {
            Group {
                // 🌅 CircleText
                CircleText(text: "Hello, World")
                CircleText(text: "Hello", color: .blue)
            }// Group
                .border(Color.black)
        }// HStack (container)
            .padding()
            .background(Color.gray)
            .shadow(radius: 8).shadow(radius: 8)
    }
}

PlaygroundPage.current.setLiveView(ContentView())
```

{% endtab %}

{% tab title="2020.10.15" %}

```swift
/*
 * ⭐️ Required:
 * - 🌀GeometryProxy + 🅿️Rectangular
 * - 📦FirstNonNil
 * - 🌀View + pref
 */

import SwiftUI
import PlaygroundSupport

// ⭐️ PreferenceKey 所取的名字最好是跟 parent view 內的 @State 變數一致，
//    如此語意的表現會更好，例如：
//    這個例子要取的是 child view (Text) 長寬中的最大邊，所以我們將
//    parent view (CircleText) 的 @State 變數取名為：
//    -  `maxSide`，
//    因此，我們將這個 PreferenceKey 取名為:
//    -  `MaxSide`。
typealias MaxSide = FirstNonNil<CGFloat>      // 📦 FirstNonNil

// 🌅 CircleText
struct CircleText: View {
    
    // ⭐️ Text 回報的最大邊長
    @State private var maxSide: CGFloat?
    
    // local variables
    let text : String         // text of Text
    let color: Color          // bg color of Text
    
    // init
    init(_ text: String, color: Color = .pink) {
        self.text  = text
        self.color = color
    }
    
    // view body
    var body: some View {
        
        Text(text).padding()
            
            // ⭐️ 1. set (optional) frame
            .frame(width: maxSide, height: maxSide)
            // ⚠️ 2. 要先設定好 frame 再加上 Circle，不然 Circle 只有
            //       Text + padding 的高度 ‼️
            .background(Circle().fill(color))
            
            // ⭐️ 3. 回報自己的「最大邊」給 MaxSide
            //       - report(to:): 🌀View + pref
            //       - $0.maxSide : 🌀GeometryProxy + 🅿️Rectangular
            .report(to: MaxSide.self) { $0.maxSide }
            
            // ⭐️ 4. 母視圖根據回報的值，更新自己的 @State 變數 `maxSide`
            .onPreferenceChange(MaxSide.self) { self.maxSide = $0 }
    }
}

// live view
struct ContentView: View {
    var body: some View {
        HStack {
            
            Group {
                CircleText("Hello, World")           // 🌅 CircleText
                CircleText("Hello", color: .blue)
            }// Group
                // ⭐️ 這裡可以確定 CircleText's frame 已經包含整個圓。
                .border(Color.black)
            
        }// HStack (container)
            .padding()
            .background(Color.gray)
            .shadow(radius: 8).shadow(radius: 8)
    }
}

PlaygroundPage.current.setLiveView(ContentView())
```

{% endtab %}
{% endtabs %}
