> 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/selected-button-with-underline.md).

# selected button with underline

* [SwiftUI](/ios/swiftui.md) ⟩ [Data Flow](/ios/swiftui/data-flow.md) ⟩ [View Preferences](/ios/swiftui/data-flow/preferences.md) ⟩ selected button with underline

{% hint style="info" %}
本例使用 [Anchor Preferences](/ios/swiftui/data-flow/preferences/anchor-preferences.md) 與 [Matched Geometry Effect](/ios/swiftui/shapes/matched-geometry-effect.md) 來達到同樣的效果。
{% endhint %}

{% tabs %}
{% tab title="⭐️ 重點" %}
{% hint style="success" %}
⭐️ 從此例可看出：

"<mark style="color:red;">**source view**</mark>" 才是 [matched geometry effect](/ios/swiftui/shapes/matched-geometry-effect.md) 的「<mark style="color:red;">移動目標</mark>」，其他 <mark style="color:orange;">**non-source view**</mark> (<mark style="color:purple;">`isSource`</mark>`:`<mark style="color:red;">`false`</mark>) 都會移動到 <mark style="color:red;">**source view**</mark> 所在的位置 (<mark style="color:purple;">**frame**</mark>)。
{% endhint %}

{% hint style="info" %}
此例的 <mark style="color:orange;">**non-source view**</mark> 都是 <mark style="color:purple;">**overlay**</mark>，可以看出它們的「<mark style="color:red;">**圖層關係**</mark>」：

<img src="https://1830103165-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M5-JmwCZMKh_d7RfBaN%2Fuploads%2Fo7vR4kcGLYVg3FnSm69L%2FMGE.gif?alt=media&amp;token=82055959-f124-42d4-b116-b6509f6bdce2" alt="" data-size="original">

* 第一個 button (World) 有<mark style="color:orange;">**三個 overlay**</mark>，所以幾乎看不到。
* 第二個 button (Alarm) 有<mark style="color:orange;">**兩個 overlay**</mark>，所以可以看到一些，而且第一個 button 的 overlay 位在第二個 button 之下。
* 第三個 button (Bedtime) 只有<mark style="color:orange;">**一個 overlay**</mark>，所以可以很清楚看出來。
  {% endhint %}
  {% endtab %}

{% tab title="💾 程式" %}
{% embed url="<https://youtu.be/FvfHysucxj4>" %}

⬆️ 需要： [Text(symbol:)](/ios/custom/ext/text+/text-symbol.md), [StackForEach](/ios/swiftui/view/layout/stacks/stackforeach.md)

```swift
// 2022.02.24
// 2022.02.25 (r) + different colors/heights to reveal non-source views.

import SwiftUI
import PlaygroundSupport
import CustomViews

PlaygroundPage.current.setLiveView(ContentView())
    
// ----------------------------------------
//     🔸 Bounds (Anchor PreferenceKey)
// ----------------------------------------

private enum Bounds: PreferenceKey {
    public typealias Value = Anchor<CGRect>?
    public static var defaultValue: Value { nil }
    public static func reduce(value: inout Value, nextValue: () -> Value) {
        value = value ?? nextValue()   // ⭐️ first non-nil (if any)
    }
}

// 🌀 給設定與調用 PreferenceKey 的方法獨特的名字，配合程式碼的目的，
//    可提高程式碼的可讀性。
private extension View {
    
    /// 🔸 set selected bounds for views
    func setSelectedBounds(
        at index: Int, 
        selected: @escaping (Int) -> Bool
    ) -> some View 
    {
        anchorPreference(key: Bounds.self, value: .bounds) { 
            // ⭐️ tranform `anchor` to nil if not `selected`
            anchor in selected(index) ? anchor : nil 
        }
    }
    
    /// 🔸 underline the selected bounds
    func underlineSelectedBounds() -> some View {
        backgroundPreferenceValue(Bounds.self) { anchor in 
            GeometryReader { proxy in 
                let bounds = proxy[anchor!]    // ⭐️ force-unwrap is OK❗️ 
                Color.green 
                    .frame(width: bounds.width, height: 1)
                    .offset(x: bounds.minX, y: bounds.height)
            }
        }
    }
}

// -------------------
//     ContentView 
// -------------------

struct ContentView: View {
    
    // ⭐️ current index
    @State private var selected = 0
    
    // ⭐️ for matched geometry effect
    @Namespace private var ns
    
    var body: some View {
        VStack {
            Text("**Anchor Preference**")
                .font(.title3)
                .foregroundColor(.secondary)
            buttons1
                .padding()
                .border(.white.opacity(0.3))
            Text("**Matched Geometry Effect**")
                .font(.title3)
                .foregroundColor(.secondary)
            buttons2
                .padding()
                .border(.white.opacity(0.3))
        }
    }
}

extension ContentView {
    
    /// tabs
    var tabs: [Text] {
        [
            label("World", "globe.asia.australia", .blue),
            label("Alarm", "alarm", .pink),
            label("Bedtime", "bed.double.fill", .orange),
        ]
    }
    
    /// button for tab i
    func button(_ i: Int) -> some View {
        Button { 
            withAnimation {
                self.selected = i    // ⭐️ set current index on click
            }
        } label: { 
            self.tabs[i] 
        }
        .foregroundColor(.white)
    }
    
    /// custom label
    func label(_ text: String, _ symbol: String, _ color: Color) -> Text {
        Text(symbol: symbol).foregroundColor(color) + Text(text) 
    }
    
    // -------------------------
    //     anchor preference
    // -------------------------
    var buttons1: some View {
        HStackForEach(tabs.indices) { i in
            button(i)
                // ⭐️ set selected bounds
                .setSelectedBounds(at: i) { self.selected == $0 }
        }
        // ⭐️ underline "the selected bounds"
        .underlineSelectedBounds()
    }
    
    // -------------------------------
    //     matched geometry effect
    // -------------------------------
    var buttons2: some View {
        HStackForEach(tabs.indices) { i in
            button(i)
                // ⭐️ source view
                // ❓ "source view" is the "target" for matched geometry effect❓ 
                //    ❗️totally confused❗️
                .matchedGeometryEffect(id: i, in: ns)
                .overlay {
                    colors[i]
                        .opacity(0.9)
                        .frame(height: 10.0 * (3-i), alignment: .bottom)
                        .shadow(radius: 4)
                        // ⭐️ non-source view
                        .matchedGeometryEffect(id: selected, in: ns, isSource: false)
                }
        }
    }
    
    var colors: [Color] {
        [.red, .green, .blue]
    }
}
```

{% endtab %}

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

* Thinking in SwiftUI, Ch.5 - Layout, Anchors (p.100)
  {% endtab %}

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

* example of [Anchor Preferences](/ios/swiftui/data-flow/preferences/anchor-preferences.md).
* can be done by [Matched Geometry Effect](/ios/swiftui/shapes/matched-geometry-effect.md).
* [dynamic underline](/ios/swiftui/view/state/value/state/underline.md) - use [＠State](/ios/swiftui/view/state/value/state.md) var.
  {% endtab %}
  {% endtabs %}

## History

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

1. 2022.02.24
   {% endtab %}

{% tab title="1" %}
{% embed url="<https://youtu.be/C3_4gzWVa4M>" %}

```swift
import SwiftUI
import PlaygroundSupport
import CustomViews

PlaygroundPage.current.setLiveView(ContentView())

/// 🔸 Bounds (Anchor PreferenceKey)
private enum Bounds: PreferenceKey {
    public typealias Value = Anchor<CGRect>?
    public static var defaultValue: Value { nil }
    public static func reduce(value: inout Value, nextValue: () -> Value) {
        value = value ?? nextValue()   // ⭐️ first non-nil (if any)
    }
}

struct ContentView: View {
    
    // ⭐️ current index
    @State private var selected = 0
    
    var body: some View {
        HStackForEach(tabs.indices) { i in
            Button { 
                self.selected = i    // ⭐️ set current index on click
            } label: { 
                self.tabs[i] 
            }
            .foregroundColor(.white)
            // ⭐️ set anchor preference (.bounds) for each button
            .anchorPreference(key: Bounds.self, value: .bounds) { 
                // ⭐️ tranform `anchor` to nil if not `selected`
                anchor in self.selected == i ? anchor : nil 
            }
        }
        // ⭐️ set background based on "the selected bounds"
        .backgroundPreferenceValue(Bounds.self) { anchor in 
            GeometryReader { proxy in 
                
                let bounds = proxy[anchor!]    // ⭐️ force unwrap❗️ 
                
                Color.indigo 
                    .frame(width: bounds.width, height: 4)
                    .offset(x: bounds.minX, y: bounds.height - 2) 
                    .animation(.default)
            }
        }
        .padding()
        .border(.white.opacity(0.3))
    }
}

extension ContentView {
    var tabs: [Text] {
        [ Text("World Clock"), Text("Alarm"), Text("Bedtime") ]
    } 
}
```

{% endtab %}
{% endtabs %}
