> 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/custom/ext/color+/system/swatch_0.md).

# 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 %}
