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))
}
}