# Slider label position

[SwiftUI](/ios/swiftui.md) ⟩ [controls](/ios/swiftui/control.md) ⟩ [Slider](/ios/swiftui/control/slider.md) ⟩ label position

{% hint style="info" %}
設定 [`Slider`](/ios/swiftui/control/slider.md) 數值<mark style="color:yellow;">標籤</mark>到滑桿<mark style="color:yellow;">調鈕</mark>的<mark style="color:yellow;">上方</mark>。
{% endhint %}

<figure><img src="/files/3sYTA3iis3nM2ejA173c" alt=""><figcaption><p>💈範例：計算標籤位置的不同方法</p></figcaption></figure>

{% tabs %}
{% tab title="💈範例" %}
{% tabs %}
{% tab title="1" %}
{% hint style="info" %}
方法 １：使用<mark style="color:yellow;">固定寬度</mark>的 [Slider](/ios/swiftui/control/slider.md)。

* <mark style="color:green;">優點</mark>：<mark style="color:yellow;">容易計算</mark>數值標籤的偏移量。
* <mark style="color:red;">缺點</mark>：寬度固定，<mark style="color:yellow;">適應性差</mark>。
  {% endhint %}

#### Slider 1 程式碼重點解說：

1. [`.background`](/ios/swiftui/view/drawing/background/.background.md) 負責將<mark style="color:orange;">子視圖</mark> ([`Text`](/ios/swiftui/control/text.md)) <mark style="color:yellow;">置中對齊</mark> (<mark style="color:blue;">`Text`</mark> 的<mark style="color:yellow;">中心點</mark>放在背景正中央)。
2. 計算 <mark style="color:blue;">`Text`</mark> 的<mark style="color:yellow;">偏移量</mark>時，須<mark style="color:yellow;">以</mark><mark style="color:red;">中央</mark><mark style="color:yellow;">為基準</mark>。
3. 用 [`.offset()`](https://developer.apple.com/documentation/swiftui/view/offset%28x:y:%29) 偏移 <mark style="color:blue;">`Text`</mark>。

```swift
// view properties
@State var t: CGFloat = 0.5
let sliderWidth: CGFloat = 300                  // ⭐️ 滑桿寬度

// view body 
// slider(fixed width) > background > text
Slider(value: $t)
    // ⭐️ 1. `.background` 負責將子視圖「置中對齊」
    .background {
        let knobWidth: CGFloat = 28             // 滑桿調鈕寬度
        let knobRange = sliderWidth - knobWidth // 調鈕活動範圍寬度
        
        // ⭐️ 2. 因為 Text 會被 .background 置中對齊
        //       所以計算偏移量時，須以中央為基準。
        let xOffset = (t-0.5) * knobRange       // 數值標籤偏移量
        
        // ⭐️ 3. 用 .offset() 偏移 Text。
        Text("\(t, specifier: "%.2f")")
            .offset(x: xOffset, y: -30)
    }// background
    .frame(width: sliderWidth)                  // ⭐️ 寬度固定
```

{% endtab %}

{% tab title="2-1" %}
{% hint style="success" %}
方法２-１：用 [<mark style="color:blue;">`.background`</mark>](/ios/swiftui/view/drawing/background/.background.md) ＋ [<mark style="color:blue;">`GeometryReader`</mark>](/ios/swiftui/view/measure/geometryreader.md) ＋ [`Slider`](/ios/swiftui/control/slider.md) 尺寸

* <mark style="color:green;">優點</mark>：可適應<mark style="color:orange;">母視圖</mark>的環境、<mark style="color:yellow;">大小可自動調整</mark>。
* <mark style="color:red;">缺點</mark>：容易忽略 <mark style="color:blue;">`GeometryReader`</mark> 以<mark style="color:red;">左上角</mark>為<mark style="color:yellow;">對齊方式</mark>，導致<mark style="color:yellow;">定位錯誤</mark>。
  {% endhint %}

#### Slider 2-1 程式碼重點解說：

1. 雖然 <mark style="color:blue;">`.background`</mark> 負責將子視圖<mark style="color:yellow;">置中對齊</mark>，但 <mark style="color:blue;">`GeometryReader`</mark> 是它<mark style="color:red;">唯一的</mark><mark style="color:orange;">子視圖</mark>，而且 <mark style="color:blue;">`GeometryReader`</mark> 會用掉<mark style="color:orange;">母視圖</mark>的<mark style="color:yellow;">所有空間</mark>，因此 <mark style="color:blue;">`.background`</mark> 的對齊等於<mark style="color:yellow;">沒有效用</mark>❗️（對齊主要是由 <mark style="color:blue;">`GeometryReader`</mark> 來接手掌控）
2. <mark style="color:red;">注意</mark>：<mark style="color:blue;">`GeometryReader`</mark> 的<mark style="color:yellow;">對齊方式</mark>與 <mark style="color:blue;">`.background`</mark> 不同，它負責將子視圖對齊「<mark style="color:red;">左上角</mark>」，因此在 <mark style="color:blue;">`GeometryReader`</mark> 內部<mark style="color:yellow;">放置子視圖的邏輯</mark>與在 <mark style="color:blue;">`.background`</mark> 內<mark style="color:yellow;">不同</mark>❗️
3. 計算<mark style="color:yellow;">標籤偏移量</mark>時，要以 <mark style="color:blue;">`GeometryReader`</mark> <mark style="color:red;">左上角</mark>考量，同時還要考量到<mark style="color:yellow;">調鈕</mark>本身有一定<mark style="color:yellow;">寬度</mark> (28)，因此<mark style="color:yellow;">調鈕的中心點</mark><mark style="color:red;">無法接觸</mark>到<mark style="color:yellow;">滑桿最左側</mark>，而是<mark style="color:yellow;">調鈕的左側</mark>接觸到滑桿的最左側，所以計算<mark style="color:yellow;">標籤偏移量</mark>時，還要先加上<mark style="color:yellow;">調鈕寬度的一半</mark>。
4. 用 `.position()` 直接設定 Text 中心點位置。\ <mark style="color:green;">優點</mark>：不會因為字體寬度變化而導致標籤位置與調鈕無法對齊。

```swift
// view's properties
@State var s: CGFloat = 100                   // 滑桿數值

let range: ClosedRange<CGFloat> = 100...300   // 數值範圍
let min = range.lowerBound                    // 最小值
let max = range.upperBound                    // 最大值

// slider > background > GeometryReader > Text
Slider(value: $s, in: range)
    // ⭐️ 1. 用 .background 搭配 GeometryReader 來取得 Slider 的尺寸
    //     - 雖然 background 負責將子視圖「置中對齊」，但 GeometryReader
    //       是它唯一的子視圖，而且 GeometryReader 會用掉背景的所有空間，
    //       因此 background 的對齊等於沒有效用❗️ 
    .background { 
        // ⭐️ 2. 注意：GeometryReader 的對齊方式與 background 不同，
        //       它負責將子視圖對齊「左上角」，因此在 GeometryReader 內部
        //       放置子視圖的邏輯與在 background 內不同❗️
        GeometryReader { geo in
        
            let s2 = (s - min)/(max - min)                // s2: 0...1
            let knobWidth: CGFloat = 28                   // 滑桿調鈕寬度
            let sliderWidth = geo.size.width              // 滑桿寬度
            let knobRange = sliderWidth - knobWidth       // 調鈕的活動寬度
            
            // ⭐️ 3. 計算標籤偏移量時，要以 GeometryReader「左上角」考量，
            //       同時還要考量到調鈕寬度。因為調鈕本身有一定寬度 (28)，
            //       因此調鈕的中心點無法接觸到滑桿的最左側，而是調鈕的「左側」
            //       接觸到滑桿的最左側，所以計算標籤偏移量時，還要先加上調鈕寬度的一半。
            let xOffset = (s2) * knobRange + knobWidth/2
            
            // ⭐️ 4. 用 .position() 直接設定 Text 中心點位置
            //       優點：不會因為字體寬度變化而導致標籤位置與調鈕無法對齊。
            Text("\(s, specifier: "%.0f")")
                .position(x: xOffset, y: -16)
                
            // ❗️ 用 .offset() 的缺點：
            //    計算 Text 偏移量時還要考慮到字體寬度變化，徒增計算困難。
            // .offset(x: xOffset, y: -30)
            
        }// GeometryReader
    }// background
}// slider2-1
```

{% endtab %}

{% tab title="2-2" %}
{% hint style="danger" %}
方法２-２：用 [<mark style="color:blue;">`.background`</mark>](/ios/swiftui/view/drawing/background/.background.md) ＋ [<mark style="color:blue;">`GeometryReader`</mark>](/ios/swiftui/view/measure/geometryreader.md) ＋ [`ZStack`](/ios/swiftui/view/layout/stacks/zstack.md) 幫忙<mark style="color:yellow;">置中</mark>

* <mark style="color:green;">優點</mark>：
  * 可適應<mark style="color:orange;">母視圖</mark>、<mark style="color:yellow;">大小可自動調整</mark>。
  * <mark style="color:blue;">`ZStack`</mark> 可幫忙<mark style="color:yellow;">置中</mark>子視圖。
* <mark style="color:red;">缺點</mark>：
  * 容易忘記將 <mark style="color:blue;">`ZStack`</mark> 的<mark style="color:yellow;">尺寸</mark>放到與 <mark style="color:blue;">`GeometryReader`</mark> <mark style="color:yellow;">一樣大</mark>，導致 <mark style="color:blue;">`GeometryReader`</mark> 依然先將<mark style="color:yellow;">整個</mark> <mark style="color:blue;">`ZStack`</mark> 放到<mark style="color:red;">左上角</mark>，然後 <mark style="color:blue;">`ZStack`</mark> 才在它的<mark style="color:red;">內部</mark>做<mark style="color:yellow;">置中對齊</mark>，這時的<mark style="color:yellow;">置中對齊</mark>就是<mark style="color:red;">無效的</mark>，因為<mark style="color:yellow;">整個</mark> <mark style="color:blue;">`ZStack`</mark> <mark style="color:yellow;">都在</mark><mark style="color:red;">左上角</mark>❗️ （看上圖 slider 2-2）
    {% endhint %}

#### Slider 2-2 程式碼重點解說：

1. 忘記將 <mark style="color:blue;">`ZStack`</mark> 的<mark style="color:yellow;">尺寸</mark>放到與 <mark style="color:blue;">`GeometryReader`</mark> <mark style="color:yellow;">一樣大</mark>❗️
2. <mark style="color:blue;">`GeometryReader`</mark> 依然先將<mark style="color:yellow;">整個</mark> <mark style="color:blue;">`ZStack`</mark> 放到<mark style="color:red;">左上角</mark>，然後 <mark style="color:blue;">`ZStack`</mark> 才在它的<mark style="color:red;">內部</mark>做<mark style="color:yellow;">置中對齊</mark>❗️&#x20;
3. 計算<mark style="color:yellow;">偏移量</mark>時，仍以「<mark style="color:yellow;">中心點</mark>」為考量，導致<mark style="color:yellow;">標籤位置</mark><mark style="color:red;">錯誤</mark>❗️

```swift
// view's properties
@State var s: CGFloat = 100                   // 滑桿數值

// view body
// 2-2. slider > overlay > GeometryReader > ZStack > Text
Slider(value: $s, in: 100...300)
    .background { 
        // ⭐️ 2. GeometryReader 依然先將整個 ZStack 放到左上角，
        //       然後 ZStack 才在它的內部做置中對齊❗️
        GeometryReader { geo in
            let s2 = (s - 100)/(300 - 100)    // s2: 0...1
            let width = geo.size.width - 28   // 調鈕的活動寬度
            
            // ⭐️ 3. 計算偏移量時，仍以「中心點」為考量，導致標籤位置錯誤❗️
            let xOffset: CGFloat = (s2 - 0.5) * width
            
            // ⭐️ 1. 忘記將 ZStack 尺寸放到與 GeometryReader 一樣大
            ZStack {
                Text("\(s, specifier: "%.0f")")
                    .offset(x: xOffset, y: -30)
            }// ZStack
            
        }// GeometryReader
    }// background
```

{% endtab %}

{% tab title="2-3" %}
{% hint style="success" %}
方法２-３：<mark style="color:yellow;">同方法２-２</mark>，但<mark style="color:yellow;">修正</mark>了忘記將 <mark style="color:blue;">`ZStack`</mark> 的<mark style="color:yellow;">尺寸</mark>放到與 <mark style="color:blue;">`GeometryReader`</mark> <mark style="color:yellow;">一樣大</mark>的問題，如此 <mark style="color:blue;">`ZStack`</mark> 才能真正接手幫忙<mark style="color:yellow;">置中對齊</mark>子視圖。
{% endhint %}

#### Slider 2-3 程式碼重點解說：

1. 使用 <mark style="color:blue;">`.overlay`</mark> + [`GeometryReader`](/ios/swiftui/view/measure/geometryreader.md) 取得 [`Slider`](/ios/swiftui/control/slider.md) <mark style="color:yellow;">尺寸</mark> (`geo.size`)
2. 先扣除 <mark style="color:blue;">`GeometryReader`</mark> <mark style="color:yellow;">左右各一半</mark>滑桿<mark style="color:yellow;">調鈕的寬度</mark>。

{% hint style="warning" %}
註：當一個 view 用到 [view modifier](/ios/swiftui/view/modifier.md) 時，<mark style="color:yellow;">母子視圖關係</mark>要<mark style="color:red;">倒過來看</mark>，例如：<mark style="color:blue;">`child.parent().grandParent()`</mark>，<mark style="color:yellow;">前面</mark>的是<mark style="color:yellow;">子視圖</mark>、<mark style="color:yellow;">後面</mark>的是<mark style="color:yellow;">母視圖</mark>❗️
{% endhint %}

3. 利用 <mark style="color:blue;">`.frame(maxWidth: .infinity, maxHeight: .infinity)`</mark> 將 <mark style="color:blue;">`ZStack`</mark> 的尺寸<mark style="color:yellow;">放到最大</mark> (<mark style="color:blue;">`GeometryReader`</mark> 扣掉<mark style="color:yellow;">調鈕寬度</mark>)
4. 此時 <mark style="color:blue;">`ZStack`</mark> 就能<mark style="color:yellow;">正常</mark>幫忙<mark style="color:yellow;">置中對齊</mark> <mark style="color:blue;">`Text`</mark>。
5. 利用 <mark style="color:blue;">`GeometryReader`</mark> 的資訊 (`geo.size`) 計算 <mark style="color:blue;">`ZStack`</mark> 的<mark style="color:yellow;">尺寸</mark>。\
   (<mark style="color:red;">註</mark>：這裡有用到 [GeometryKit](/ios/custom/package/geometrykit.md) 這個 [custom package](/ios/custom/package.md))
6. 用 <mark style="color:blue;">`.position()`</mark> 直接<mark style="color:yellow;">設定</mark>標籤<mark style="color:yellow;">中心點</mark>位置 (此時標籤會疊在調鈕正上方)。
7. 用 <mark style="color:blue;">`.offset()`</mark> 將標籤<mark style="color:yellow;">往上調</mark>。

```swift
// view's properties
let bounds: ClosedRange<CGFloat> = 100...300
let min = bounds.lowerBound
let max = bounds.upperBound

// view body
// 2-3. slider > overlay > GeometryReader > ZStack(maximized) > Text
Slider(value: $s, in: bounds)
    .overlay { 
        // ⭐️ 1. overlay + GeometryReader 取得 Slider 尺寸 (geo.size)
        GeometryReader { geo in
        
            let s2 = (s - min)/(max - min)    // s2: 0...1
            let knobWidth: CGFloat = 28
            let yOffset: CGFloat = -16
            
            // ⭐️ 5. 計算 ZStack 的大小
            let zstackSize = geo.size - CGSize(knobWidth, 0)  // GeometryKit
            
            // ⭐️ 4. ZStack 幫忙置中對齊 Text
            ZStack(alignment: .center) {
                Text("\(s, specifier: "%.0f")")
                    // ⭐️ 6. 用 .position() 直接設定標籤中心點位置，
                    //       此時標籤會疊在調鈕正上方。
                    .position(zstackSize[s2, 0])    // GeometryKit
                    // ⭐️ 7. 將標籤往上調。
                    .offset(y: yOffset)
            }// ZStack
            
            // ⭐️ 3. 將 ZStack 的尺寸放到最大
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            
            // ⭐️ 2. 先扣除 GeometryReader 左右各一半滑桿調鈕的寬度
            .padding(.horizontal, knobWidth/2)    // knob half width
            
        }//GeometryReader
        .allowsHitTesting(false)    // 防止 overlay 擋住 slider
    }// overlay
```

{% endtab %}

{% tab title="2-4" %}
{% hint style="success" %}
**方法２-４**：<mark style="color:yellow;">同方法２-３</mark>。眼尖的人應該會察覺到，雖然<mark style="color:yellow;">方法２-３</mark>中我們有用 <mark style="color:blue;">`ZStack`</mark> 來幫忙<mark style="color:yellow;">置中</mark>子視圖，它唯一的子視圖 <mark style="color:blue;">`Text`</mark> 卻選擇了 <mark style="color:blue;">`.position()`</mark> 來<mark style="color:yellow;">直接設定</mark>自己的<mark style="color:yellow;">中心點</mark>位置。換句話說，<mark style="color:blue;">`ZStack`</mark> 根本<mark style="color:yellow;">沒起任何作用</mark>，因此可以直接移除 <mark style="color:blue;">`ZStack`</mark>❗️&#x20;
{% endhint %}

🚧
{% endtab %}

{% tab title="💾 程式" %}

```swift
import SwiftUI
import GeometryKit    // CGSize as Vector2D

struct SliderValuePosition: View {
    
    @State var t: CGFloat = 0.5
    @State var s: CGFloat = 100
    
    // slider range
    let range: ClosedRange<CGFloat> = 100...300
    var min: CGFloat { range.lowerBound }
    var max: CGFloat { range.upperBound }
    var s2: CGFloat { (s - min)/(max - min) }    // s2: 0...1
    
    let knobWidth: CGFloat = 28    // slider knob width
    let yOffset: CGFloat = -16     // label y-offset
    
    var body: some View {
        ScrollView {
            VStack(spacing: 100) {
                
                Spacer()
                
                slider1
                
                HStack {
                    slider2_1
                    slider2_2
                }
                
                slider2_3
                slider2_4
                
                Spacer()
                
            }// VSTack
            .padding()
            .frame(maxWidth: 900)
        }// ScrollView
    }// body
    
    // 1. slider(fixed width) > background > text
    var slider1: some View {
        
        let sliderWidth: CGFloat = 300          // ⭐️ fixed slider width
        let knobRange = sliderWidth - knobWidth
        let xOffset = (t-0.5) * knobRange       // -0.5...0.5 * knobRange
        
        return Slider(value: $t).background {
            Text("\(t, specifier: "%.2f")")
                .offset(x: xOffset, y: -30)
        }// slider.background
        .bgColor(.green)
        .frame(width: sliderWidth)
        .borderAndTitle("slider 1 (⭐️ fixed width ⭐️)")
    }// slider1
    
    // 2-1. slider > background > GeometryReader > Text
    var slider2_1: some View {
        Slider(value: $s, in: range)
            .background { 
                GeometryReader { geo in
                    let sliderWidth = geo.size.width            // slider width
                    let knobRange = sliderWidth - knobWidth     // knob 的活動寬度
                    let xOffset = s2 * knobRange + knobWidth/2  // label x-offset
                    
                    // ⭐️ 用 .position() 的優點：
                    //    直接設定中心點位置，不會因為字體寬度變化而導致位置偏移
                    Text("\(s, specifier: "%.0f")")
                        .position(x: xOffset, y: -16)
                    
                    // ❗️ 用 .offset() 的缺點：
                    //    在 GeometryReader 內部，子視圖會對齊左上角，
                    //    因此計算 Text 的偏移量時還要考慮到字體寬度變化，
                    //    導致計算上的困難。
                    // .offset(x: xOffset, y: -30)
                    
                }// GeometryReader
                .bgColor(.yellow).allowsHitTesting(false)
            }// background
            .borderAndTitle("slider 2-1")
    }// slider2-1
    
    // 2-2. slider > background > GeometryReader > ZStack > Text
    var slider2_2: some View {
        Slider(value: $s, in: range)
            .background { 
                GeometryReader { geo in
                    let sliderWidth = geo.size.width
                    let knobRangeWidth = sliderWidth - knobWidth
                    let xOffset = (s2 - 0.5) * knobRangeWidth
                    
                    ZStack {
                        Text("\(s, specifier: "%.0f")")
                            .offset(x: xOffset, y: -30)    // move upward
                    }// ZStack
                    .bgColor(.red, opacity: 0.9)
                }// GeometryReader
                .bgColor(.yellow)
                .allowsHitTesting(false)
            }// slider.background
            .borderAndTitle("slider 2-2")
    }// slider2_2
    
    // 2-3. slider > overlay > GeometryReader > ZStack(maximized) > Text
    var slider2_3: some View {      
        Slider(value: $s, in: range)
            .overlay { 
                GeometryReader { geo in
                    let zstackSize = geo.size - CGSize(knobWidth, 0)  // GeometryKit
                    
                    ZStack {
                        Text("\(s, specifier: "%.0f")")
                            // ⭐️ position label directly
                            .position(zstackSize[s2, 0])   // GeometryKit
                            .offset(y: yOffset)            // move upward
                    }// ZStack
                    // ⭐️ maximize ZStack
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .bgColor(.red)
                    .padding(.horizontal, knobWidth/2)    // knob half width
                }//GeometryReader
                .allowsHitTesting(false)
            }// slider.overlay
            .borderAndTitle("slider 2-3")
    }// slider2_3
    
    // 2-4. slider > background > GeometryReader > Text
    var slider2_4: some View { 
        Slider(value: $s, in: range)
            .background { 
                GeometryReader { geo in
                    Text("\(s, specifier: "%.0f")")
                        // ⭐️ position label directly
                        .position(geo.size[s2, 0])    // GeometryKit
                        .offset(y: yOffset)           // move upward
                }//GeometryReader
                .padding(.horizontal, knobWidth/2)    // ⭐️ knob half width
                .bgColor(.yellow)
                .allowsHitTesting(false)
            }// background
            .borderAndTitle("slider 2-4")
    }// slider2_4
    
}// SliderValuePosition

// view extension
private extension View {
    
    /// `view.borderAndTitle("name")`
    func borderAndTitle(_ title: String) -> some View {
        self
            .border(.secondary)
            .padding()
            .border(.secondary)
            .overlay {
            Text(title)
                .offset(y: 50)
                .foregroundStyle(Color.secondary)
                .multilineTextAlignment(.center)
        }
    }
    
    /// `view.bgColor(.red)`
    func bgColor(_ color: Color, opacity: Double = 0.5) -> some View {
        self.background(color.opacity(opacity))
    }
}
```

{% endtab %}
{% endtabs %}
{% endtab %}

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

* [ GeometryReader](/ios/swiftui/view/measure/geometryreader.md)：用於取得 [Slider](/ios/swiftui/control/slider.md) 的寬度。
* [GeometryReader 的對齊方式](/ios/swiftui/view/measure/geometryreader/geometryreader-de-dui-qi-fang-shi.md) ⭐️&#x20;
  {% endtab %}

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

* [GeometryKit](/ios/custom/package/geometrykit.md)：custom package
  {% endtab %}

{% tab title="📘 手冊" %}

* [SwiftUI](https://developer.apple.com/documentation/swiftui) ⟩ [View](https://developer.apple.com/documentation/swiftui/view) ⟩&#x20;
  * [.offset(x:y:)](https://developer.apple.com/documentation/swiftui/view/offset%28x:y:%29)
  * [.position(\_:)](https://developer.apple.com/documentation/swiftui/view/position\(_:\)) ╱ [.position(x:y:)](https://developer.apple.com/documentation/swiftui/view/position\(x:y:\))
    {% 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/swiftui/control/slider/label-position.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.
