# Swatch\_0

[swift](/ios/swift.md)⟩ [custom](/ios/custom.md) ⟩ [extension](/ios/custom/ext.md) ⟩ [Color](/ios/custom/ext/color+.md) ⟩ [system](/ios/custom/ext/color+/system.md) ⟩ <mark style="color:purple;">`Swatch`</mark>

{% tabs %}
{% tab title="✨ 結果" %}
{% hint style="info" %}
每一張<mark style="color:purple;">色票</mark>都有自己<mark style="color:red;">**預設的寬度**</mark>，但如果色票底下的<mark style="color:red;">**文字超過色票寬度**</mark>時，色票會自動延長寬度，<mark style="color:red;">**變成跟文字一樣長**</mark>。
{% endhint %}

![](/files/sbHX29uDO37qkfwAyaPJ)
{% endtab %}

{% tab title="🌇 Swatch" %}
⬆️ 需要： [ViewPreference](/ios/swiftui/data-flow/preferences/viewpreference.md), [system colors](/ios/custom/ext/color+/system.md)

```swift
struct Swatch: View {
    
    let color: Color                      // swatch color
    let preferedWidth: CGFloat            // prefered swatch width
    
    // ⭐ state var without initial value
    @State private var width: CGFloat     // swatch width

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            color
                // ⭐ swatch width is flexible (height is fixed)
                .frame(width: width, height: preferedWidth)
            Text(".\(color.name)")      // 🌀 Color extension
                .fixedSize()            // ⭐ text no wrap
                .actOnSelfWidth {       // 👔 ViewPreference
                    // ⭐ if text width > prefered width, change swatch watch
                    if $0 > preferedWidth { width = $0 }
                }
        }
        .padding(4)
        .border(Color.label3)
    }
}

extension Swatch {
    // ---------------------------
    //     ✨ init State var ✨
    // ---------------------------
    init(_ color: Color, preferedWidth width: CGFloat = 60) {
        self.color = color
        self.preferedWidth = width
        // ⭐ state var initialized in an init
        //    swatch width = prefered width (at first)
        _width = State(initialValue: width)    
    }
}

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 20){
            swatchPad("Label Colors", Color.labelColors)
            swatchPad("Fill Colors", Color.fillColors)
            swatchPad("Content Backgrounds", Color.backgrounds)
            swatchPad("Grouped Content Backgrounds", Color.groupedContentBackgrounds)
        }
    }
}

extension ContentView {
    // a row of Swatches
    func swatchPad(
        _ title : String, 
        _ colors: [Color], 
        spacing : CGFloat? = nil
    ) -> some View 
    {
        VStack(alignment: .leading) {
            Text(title).font(.title)
            HStack(spacing: spacing) { 
                ForEach(colors, id: \.self){ 
                    Swatch($0)                 // 📦 Swatch
                } 
            }
        }
    }
}
```

{% endtab %}

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

* initializes a [＠State](/ios/swiftui/view/state/value/state.md) variable in an initializer.
* [.frame()](/ios/swiftui/view/layout/frame/.frame.md) - related concept
* [ViewPreference](/ios/swiftui/data-flow/preferences/viewpreference.md) - required custom type
* [Text](/ios/swiftui/control/text.md) ⟩ [no wrap](/ios/swiftui/control/text/format/no-wrap.md) &#x20;
* [Swatch](/ios/custom/view/swatch.md)： 新的色卡設計
  {% endtab %}

{% tab title="⬆️ 需要" %}

* [system colors](/ios/custom/ext/color+/system.md)
  {% endtab %}

{% tab title="🗣 討論" %}

* [SwiftUI @State var initialization issue](https://stackoverflow.com/a/58137096/5409815) - how to init a State var?
  {% endtab %}
  {% endtabs %}

## Archive

{% tabs %}
{% tab title="files" %}

1. 👔 ViewPreference
   {% endtab %}

{% tab title="1" %}

```swift
/*
 History:
 2021.??.?? - first version
 2022.01.18 + reportWidth, actOnWidth, actOnSelfWidth, ViewPreference.Width
 */

import SwiftUI

/// 🅿️ HasDefaultValue
public protocol HasDefaultValue {
    static var defaultValue: Self { get }
}

// ------------------------------------------
//     `HasDefaultValue` conforming types
// ------------------------------------------

/// 🌀 CGSize
extension CGSize: HasDefaultValue {
    public static var defaultValue: Self { .zero }
}

/// 🌀 CGRect
extension CGRect: HasDefaultValue {
    public static var defaultValue: Self { .zero }
}

/// 🌀 CGFloat
extension CGFloat: HasDefaultValue {
    public static var defaultValue: Self { .zero }
}

// -----------------------
//     ViewPreference
// -----------------------

/// 🔸 ViewPreference
public enum ViewPreference {
    
    // 將有關 bounds anchor 的資訊放到 view extension methods 的參數中，
    // 盡量讓 method name 不要太長。
    public enum BoundsAnchorType {
        case first
        case last
    }
    
    /// ⭐ ViewPreference type aliases
    public typealias Size = ViewPreference.First<CGSize>
    public typealias Width = ViewPreference.First<CGFloat>
    
    /// anchor preferences
    public typealias FirstBoundsAnchor = ViewPreference.FirstNonNil<Anchor<CGRect>>
    public typealias LastBoundsAnchor  = ViewPreference.LastNonNil<Anchor<CGRect>>
    
    /// 🔸 ViewPreference.First
    public enum First<T: HasDefaultValue>: PreferenceKey {
        public typealias Value = T
        public static var defaultValue: Value { Value.defaultValue }
        public static func reduce(value: inout Value, nextValue: () -> Value) {
            // ⭐ ignore all values other than the first
        }
    }
    
    /// 🔸 ViewPreference.FirstNonNil<T>
    public enum FirstNonNil<T>: PreferenceKey {
        public typealias Value = T?
        public static var defaultValue: Value { nil }
        public static func reduce(value: inout Value, nextValue: () -> Value) {
            value = value ?? nextValue()   // nil or first non-nil value
        }
    }
    
    /// 🔸 ViewPreference.LastNonNil<T>
    public enum LastNonNil<T>: PreferenceKey {
        public typealias Value = T?
        public static var defaultValue: Value { nil }
        public static func reduce(value: inout Value, nextValue: () -> Value) {
            value = nextValue() ?? value   // nil or last non-nil value
        }
    }
}

// -----------------------
//     View extensions
// -----------------------

/// 🌀 CGRect + ViewPreference
extension View {
    
    /// ⭐ usage: `view.actOnSize { size in ... }`
    public func actOnSelfSize(
        action: @escaping (CGSize) -> Void
    ) -> some View
    {
        // 一般的 view 並不知道自己的 size 資訊，但使用：
        // `.background(GeometryReader{ geo in Color.clear })`
        // 就可以在不影響自己的佈局與外觀下，透過 `geo` (GeometryProxy) 物件
        // 得知自己的 size (geo.size)，然後用 .preference() 回報上層。
        self.background( GeometryReader { geo in
            Color.clear
                // ⭐ 下層回報自己的 size
                .preference(key: ViewPreference.Size.self, value: geo.size)
        })
        // ⭐ 上層得知 size，並用 `action` 處理。
        .onPreferenceChange(ViewPreference.Size.self, perform: action)
    }
    
    /// ⭐ usage: `view.actOnSelfWidth { width in ... }`
    public func actOnSelfWidth(
        action: @escaping (CGFloat) -> Void
    ) -> some View
    {
        self.background( GeometryReader { geo in
            Color.clear
                // ⭐ 下層回報自己的 width
                .preference(key: ViewPreference.Width.self, value: geo.size.width)
        })
        // ⭐ 上層得知 width，並用 `action` 處理。
        .onPreferenceChange(ViewPreference.Width.self, perform: action)
    }
    
    // ⭐ local abbreviations
    typealias FBA = ViewPreference.FirstBoundsAnchor
    typealias LBA = ViewPreference.LastBoundsAnchor
    typealias Width = ViewPreference.Width
    
    // ---------------------------------
    // 下層回報資料的「方法命名原則」：
    // 1. 如果只收集其中一個，用：report...()
    // 2. 如果收集很多個，就用：collect...()
    // ----------------------------------
    
    /// ⭐ report width
    @ViewBuilder public func reportWidth() -> some View {
        self.background( GeometryReader { geo in
            Color.clear
                // ⭐ 下層回報自己的 width
                .preference(key: ViewPreference.Width.self, value: geo.size.width)
        })
    }
    
    /// ⭐ act on width reported
    /// usage: `view.actOnWidth { width in ... }`
    @ViewBuilder public func actOnWidth(
        action: @escaping (CGFloat) -> Void
    ) -> some View
    {
        onPreferenceChange(Width.self) { action($0) }
    }
    
    /// ⭐ report bounds anchor (with anchor type)
    @ViewBuilder public func report(
        bounds anchorType: ViewPreference.BoundsAnchorType
    ) -> some View {
        switch anchorType {
        case .first: 
            // ⭐ 回報 anchor preference
            anchorPreference(key: FBA.self, value: .bounds) { $0 }
        //                        ╰─ 1 ──╯         ╰──2──╯  ╰─3──╯
        // 1. 要回報的 anchor preference (K = FirstBoundsAnchor)
        // 2. 要回報的 anchor 種類 (.bounds)
        // 3. 將 Anchor<A> 轉為要回報的資料： @escaping (Anchor<A>) -> K.Value
        //    如果 Anchor<A> 本身就是要回報的資料 (K.Value)，直接使用 `$0` 就可以。
        case .last: 
            anchorPreference(key: LBA.self, value: .bounds) { $0 }
        }
    }
    
    /// ⭐ report bounds anchor
    public func reportBounds() -> some View {
        // ⭐ 回報 anchor preference
        anchorPreference(key: FBA.self, value: .bounds) { $0 }
        //                    ╰─ 1 ──╯         ╰──2──╯  ╰─3──╯
        // 1. 要回報的 anchor preference (K = FirstBoundsAnchor)
        // 2. 要回報的 anchor 種類 (.bounds)
        // 3. 將 Anchor<A> 轉為要回報的資料： @escaping (Anchor<A>) -> K.Value
        //    如果 Anchor<A> 本身就是要回報的資料 (K.Value)，直接使用 `$0` 就可以。
    }
    
    /// ⭐ background by reported bounds (with anchor type)
    @ViewBuilder public func background<V: View>(
        bounds anchorType: ViewPreference.BoundsAnchorType,
        @ViewBuilder content:  @escaping (CGRect) -> V
    ) -> some View
    {
        switch anchorType {
        case .first:
            backgroundPreferenceValue(FBA.self) { anchor in
                GeometryReader { geo in
                    if let anchor = anchor { content(geo[anchor]) }
                }
            }
        case .last:
            backgroundPreferenceValue(LBA.self) { anchor in
                GeometryReader { geo in
                    if let anchor = anchor { content(geo[anchor]) }
                }
            }
        }
    }
    
    /// ⭐ background by reported bounds
    public func backgroundByBounds<V: View>(
        @ViewBuilder content:  @escaping (CGRect) -> V
    ) -> some View
    {
        backgroundPreferenceValue(FBA.self) { anchor in
            GeometryReader { geo in
                if let anchor = anchor { content(geo[anchor]) }
            }
        }
    }
    
    /// ⭐ act on bounds reported
    @ViewBuilder public func actOnBounds(
        _ anchorType: ViewPreference.BoundsAnchorType,
        action: @escaping (Anchor<CGRect>) -> Void
    ) -> some View
    {
        switch anchorType {
        case .first:
            onPreferenceChange(FBA.self) { anchor in
                if let anchor = anchor { action(anchor) }
            }
        case .last:
            onPreferenceChange(LBA.self) { anchor in
                if let anchor = anchor { action(anchor) }
            }
        }
    }
}

```

{% endtab %}
{% endtabs %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lochiwei.gitbook.io/ios/custom/ext/color+/system/swatch_0.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
