在 SwiftUI 中創建一個環形 Slider

語言: CN / TW / HK

前言

Slider 控件是一種允許用户從一系列值中選擇一個值的 UI 控件。在 SwiftUI 中,它通常呈現為直線上的拇指選擇器。有時將這種類型的選擇器呈現為一個圓圈,拇指繞着圓周移動可能會更好。本文介紹如何在 SwiftUI 中定義一個環形的 Slider。

初始化環形輪廓

ZStack中的三個圓環開始。一個灰色的圓環代表滑塊的路徑輪廓,一個淡紅色的圓弧代表沿着圓環的進度,一個圓圈代表當前光標或拇指的位置。將滑塊的範圍設置為0.0到1.0,並硬編碼一個直徑和一個的當前位置進度 - 0.33。

```swift struct CircularSliderView1: View { let progress = 0.33 let ringDiameter = 300.0

private var rotationAngle: Angle {
    return Angle(degrees: (360.0 * progress))
}

var body: some View {
    VStack {
        ZStack {
            Circle()
                .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
            Circle()
                .trim(from: 0, to: progress)
                .stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
                        style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
                )
                .rotationEffect(Angle(degrees: -90))
            Circle()
                .fill(Color.white)
                .frame(width: 21, height: 21)
                .offset(y: -ringDiameter / 2.0)
                .rotationEffect(rotationAngle)
        }
        .frame(width: ringDiameter, height: ringDiameter)

        Spacer()
    }
    .padding(80)
}

}

```

將進度值和拇指位置綁定

將進度變量更改為狀態變量並添加默認 Slider。這個 Slider 用於修改進度值,並在圓形滑塊上實現足夠的代碼以使拇指和進度弧響應。當前值顯示在環形 Slider 的中心。

```swift struct CircularSliderView2: View { @State var progress = 0.33 let ringDiameter = 300.0

private var rotationAngle: Angle {
    return Angle(degrees: (360.0 * progress))
}

var body: some View {
    ZStack {
        Color(hue: 0.58, saturation: 0.04, brightness: 1.0)
            .edgesIgnoringSafeArea(.all)

        VStack {
            ZStack {
                Circle()
                    .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
                    .overlay() {
                        Text("\(progress, specifier: "%.1f")")
                            .font(.system(size: 78, weight: .bold, design:.rounded))
                    }
                Circle()
                    .trim(from: 0, to: progress)
                    .stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
                            style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
                    )
                    .rotationEffect(Angle(degrees: -90))
                Circle()
                    .fill(Color.white)
                    .shadow(radius: 3)
                    .frame(width: 21, height: 21)
                    .offset(y: -ringDiameter / 2.0)
                    .rotationEffect(rotationAngle)
            }
            .frame(width: ringDiameter, height: ringDiameter)


            VStack {
                Text("Progress: \(progress, specifier: "%.1f")")
                Slider(value: $progress,
                       in: 0...1,
                       minimumValueLabel: Text("0.0"),
                       maximumValueLabel: Text("1.0")
                ) {}
            }
            .padding(.vertical, 40)

            Spacer()
        }
        .padding(.vertical, 40)
        .padding()
    }
}

} ```

添加觸摸手勢

DragGesture 被添加到滑塊圓圈,並且使用臨時文本視圖顯示拖動手勢的當前位置。可以看到 x 和 y 座標圍繞包含環形 Slider 的位置中心的變化情況。

```swift struct CircularSliderView3: View { @State var progress = 0.33 let ringDiameter = 300.0

@State var loc = CGPoint(x: 0, y: 0)

private var rotationAngle: Angle {
    return Angle(degrees: (360.0 * progress))
}

private func changeAngle(location: CGPoint) {
    loc = location
}

var body: some View {
    ZStack {
        Color(hue: 0.58, saturation: 0.04, brightness: 1.0)
            .edgesIgnoringSafeArea(.all)

        VStack {
            ZStack {
                Circle()
                    .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
                    .overlay() {
                        Text("\(progress, specifier: "%.1f")")
                            .font(.system(size: 78, weight: .bold, design:.rounded))
                    }
                Circle()
                    .trim(from: 0, to: progress)
                    .stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
                            style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
                    )
                    .rotationEffect(Angle(degrees: -90))
                Circle()
                    .fill(Color.blue)
                    .shadow(radius: 3)
                    .frame(width: 21, height: 21)
                    .offset(y: -ringDiameter / 2.0)
                    .rotationEffect(rotationAngle)
                    .gesture(
                        DragGesture(minimumDistance: 0.0)
                            .onChanged() { value in
                                changeAngle(location: value.location)
                            }
                    )
            }
            .frame(width: ringDiameter, height: ringDiameter)

            Spacer().frame(height:50)

            Text("Location = (\(loc.x, specifier: "%.1f"), \(loc.y, specifier: "%.1f"))")

            Spacer()
        }
        .padding(.vertical, 40)
        .padding()
    }
}

} ```

為不同的座標值設置滑塊位置

圓形滑塊上有兩個表示進度的值,用於顯示進度弧度的progress值和用於顯示滑塊光標的rotationAngle。應該只有一個屬性來保存滑塊進度。視圖被提取到一個單獨的結構中,該結構具有圓形滑塊上進度的一個綁定值。

滑塊的range的可選參數也是可用的。這需要對進度進行一些調整,以計算已設置的角度以及拇指在圓形滑塊上位置的旋轉角度。另外調用onAppear根據View出現前的進度值計算旋轉角度。

```swift struct CircularSliderView: View { @Binding var progress: Double

@State private var rotationAngle = Angle(degrees: 0)
private var minValue = 0.0
private var maxValue = 1.0

init(value progress: Binding<Double>, in bounds: ClosedRange<Int> = 0...1) {
    self._progress = progress

    self.minValue = Double(bounds.first ?? 0)
    self.maxValue = Double(bounds.last ?? 1)
    self.rotationAngle = Angle(degrees: progressFraction * 360.0)
}

private var progressFraction: Double {
    return ((progress - minValue) / (maxValue - minValue))
}

private func changeAngle(location: CGPoint) {
    // 為位置創建一個向量(在 iOS 上反轉 y 座標系統)
    let vector = CGVector(dx: location.x, dy: -location.y)

    // 計算向量的角度
    let angleRadians = atan2(vector.dx, vector.dy)

    // 將角度轉換為 0 到 360 的範圍(而不是負角度)
    let positiveAngle = angleRadians < 0.0 ? angleRadians + (2.0 * .pi) : angleRadians

    // 根據角度更新滑塊進度值
    progress = ((positiveAngle / (2.0 * .pi)) * (maxValue - minValue)) + minValue
    rotationAngle = Angle(radians: positiveAngle)
}

var body: some View {
    GeometryReader { gr in
        let radius = (min(gr.size.width, gr.size.height) / 2.0) * 0.9
        let sliderWidth = radius * 0.1

        VStack(spacing:0) {
            ZStack {
                Circle()
                    .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9),
                            style: StrokeStyle(lineWidth: sliderWidth))
                    .overlay() {
                        Text("\(progress, specifier: "%.1f")")
                            .font(.system(size: radius * 0.7, weight: .bold, design:.rounded))
                    }
                // 取消註釋以顯示刻度線
                //Circle()
                //    .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.6),
                //            style: StrokeStyle(lineWidth: sliderWidth * 0.75,
                //                               dash: [2, (2 * .pi * radius)/24 - 2]))
                //    .rotationEffect(Angle(degrees: -90))
                Circle()
                    .trim(from: 0, to: progressFraction)
                    .stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
                            style: StrokeStyle(lineWidth: sliderWidth, lineCap: .round)
                    )
                    .rotationEffect(Angle(degrees: -90))
                Circle()
                    .fill(Color.white)
                    .shadow(radius: (sliderWidth * 0.3))
                    .frame(width: sliderWidth, height: sliderWidth)
                    .offset(y: -radius)
                    .rotationEffect(rotationAngle)
                    .gesture(
                        DragGesture(minimumDistance: 0.0)
                            .onChanged() { value in
                                changeAngle(location: value.location)
                            }
                    )
            }
            .frame(width: radius * 2.0, height: radius * 2.0, alignment: .center)
            .padding(radius * 0.1)
        }

        .onAppear {
            self.rotationAngle = Angle(degrees: progressFraction * 360.0)
        }
    }
}

}

```

CircularSliderView 的三種不同視圖被添加到View中以測試和演示 Circular Slider 視圖的不同功能。

```swift struct CircularSliderView5: View { @State var progress1 = 0.75 @State var progress2 = 37.5 @State var progress3 = 7.5

var body: some View {
    ZStack {
        Color(hue: 0.58, saturation: 0.06, brightness: 1.0)
            .edgesIgnoringSafeArea(.all)

        VStack {
            CircularSliderView(value: $progress1)
                .frame(width:250, height: 250)

            HStack {
                CircularSliderView(value: $progress2, in: 1...10)

                CircularSliderView(value: $progress3, in: 0...100)
            }

            Spacer()
        }
        .padding()
    }
}

} ```

總結

本文展示瞭如何定義響應拖動手勢的圓環滑塊控件。可以設置滑塊視圖的大小,並且滑塊按預期工作。可以向控件添加更多參數以設置顏色或圓環內顯示的值的格式。

本文正在參加「金石計劃」