在 SwiftUI 中创建一个环形 Slider

前言

Slider 控件是一种答应用户从一系列值中挑选一个值的 UI 控件。在 SwiftUI 中,它通常呈现为直线上的拇指挑选器。有时将这种类型的挑选器呈现为一个圆圈,拇指绕着圆周移动可能会更好。本文介绍如安在 SwiftUI 中界说一个环形的 Slider。

初始化环形概括

ZStack中的三个圆环开始。一个灰色的圆环代表滑块的途径概括,一个淡红色的圆弧代表沿着圆环的进展,一个圆圈代表当时光标或拇指的方位。将滑块的范围设置为0.0到1.0,并硬编码一个直径和一个的当时方位进展 – 0.33。

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

在 SwiftUI 中创建一个环形 Slider

将进展值和拇指方位绑定

将进展变量更改为状态变量并添加默许 Slider。这个 Slider 用于修改进展值,并在圆形滑块上实现满足的代码以使拇指和进展弧呼应。当时值显现在环形 Slider 的中心。

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

在 SwiftUI 中创建一个环形 Slider

添加接触手势

DragGesture 被添加到滑块圆圈,而且运用临时文本视图显现拖动手势的当时方位。能够看到 x 和 y 坐标围绕包含环形 Slider 的方位中心的改变情况。

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

在 SwiftUI 中创建一个环形 Slider

为不同的坐标值设置滑块方位

圆形滑块上有两个表示进展的值,用于显现进展弧度的progress值和用于显现滑块光标的rotationAngle。应该只有一个特点来保存滑块进展。视图被提取到一个单独的结构中,该结构具有圆形滑块上进展的一个绑定值。

滑块的range的可选参数也是可用的。这需要对进展进行一些调整,以核算已设置的视点以及拇指在圆形滑块上方位的旋转视点。别的调用onAppear根据View呈现前的进展值核算旋转视点。

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 视图的不同功能。

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

在 SwiftUI 中创建一个环形 Slider

总结

本文展示了怎么界说呼应拖动手势的圆环滑块控件。能够设置滑块视图的大小,而且滑块按预期作业。能够向控件添加更多参数以设置颜色或圆环内显现的值的格式。

本文正在参加「金石计划」