---
description: ╱🚧 under construction
---

# Slider label position

[SwiftUI](../..) ⟩ [controls](..) ⟩ [Slider]() ⟩ label position

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

<figure><img src="https://1830103165-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M5-JmwCZMKh_d7RfBaN%2Fuploads%2FU7rHo3drsXlRtGlA1Mm1%2Fslider%20value%20label.jpeg?alt=media&#x26;token=c39525f7-bc36-4223-bcb9-249c53ce8a18" alt=""><figcaption><p>💈範例：計算標籤位置的不同方法</p></figcaption></figure>

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

* <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`](../../view/drawing/background/.background) 負責將<mark style="color:orange;">子視圖</mark> ([`Text`](../text)) <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\(x:y:\)) 偏移 <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>](../../view/drawing/background/.background) ＋ [<mark style="color:blue;">`GeometryReader`</mark>](../../view/measure/geometryreader) ＋ [`Slider`]() 尺寸

* <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>](../../view/drawing/background/.background) ＋ [<mark style="color:blue;">`GeometryReader`</mark>](../../view/measure/geometryreader) ＋ [`ZStack`](../../view/layout/stacks/zstack) 幫忙<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`](../../view/measure/geometryreader) 取得 [`Slider`]() <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](../../view/modifier) 時，<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](../../../custom/package/geometrykit "mention") 這個 [custom package](../../../custom/package))
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](../../view/measure/geometryreader "mention")：用於取得 [](<> "mention") 的寬度。
* [geometryreader-de-dui-qi-fang-shi](../../view/measure/geometryreader/geometryreader-de-dui-qi-fang-shi "mention") ⭐️&#x20;
{% endtab %}

{% tab title="⬆️ 需要" %}
* [geometrykit](../../../custom/package/geometrykit "mention")：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\(x:y:\))
  * [.position(\_:)](https://developer.apple.com/documentation/swiftui/view/position\(_:\)) ╱ [.position(x:y:)](https://developer.apple.com/documentation/swiftui/view/position\(x:y:\))
{% endtab %}
{% endtabs %}

