You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift UI复古电话式PIN转盘屏:旋转选号异常问题求助

复古电话PIN码输入界面旋转数字选中错误问题修复

我开发了一个类似复古电话的PIN码输入界面,界面包含环形排列的数字,上方设有可旋转的转盘,旋转转盘时选中对应数字。但从数字2位置开始旋转时,选中的数字显示错误。以下是实现的Swift代码:

struct ContentView: View {
    var body: some View {
        VStack {
            RotaryDialView()
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct RotatableImageView: View {
    @State private var rotationAngle: Angle = .zero
    @GestureState private var dragOffset: CGSize = .zero
    
    var body: some View {
        VStack{
            ZStack{
                Image("iconr")
                    .resizable()
                    .frame(width: 400, height: 400)
                    .rotationEffect(rotationAngle, anchor: .center)
                VStack{}.frame(width: 600, height: 600)
                    .background(Color.red.opacity(0.1))
                    .gesture(DragGesture()
                        .onChanged({ value in
                            withAnimation{
                                let dragVector = CGVector(dx: value.location.x, dy: value.location.y)
                                let radius = atan2(dragVector.dy, dragVector.dx)
                                
                                var angle = radius * 180 / .pi
                                if angle < 0 {angle = 360 + angle}
                                
                                rotationAngle += Angle(radians: Double(angle / 600))
                            }
                        })
                            .onEnded({ value in
                                withAnimation{
                                    rotationAngle = .zero
                                }
                            })
                    )
            }
        }
    }
}

struct RotaryDialView: View {
    let numbers: [Int] = Array(0...9)
    @State var selectedNumber: Int = 0
    @State var pinArray: [Int] = []

    @State private var dialRotationAngle: Double = 0
    @State private var lastNumber: Int?
    @State private var initialRotationAngle: Double = 10
    
    @State private var rotationAngle: Angle = .zero
    @GestureState private var dragOffset: CGSize = .zero
    @State private var initialDragAngle: Angle = .zero
    @State var cirleSize:CGFloat = 310

    var body: some View {
        VStack {
            Text("number: \(selectedNumber) ")
            ZStack {
                Circle()
                    .stroke(style:StrokeStyle(lineWidth: 85))
                    .frame(width: cirleSize, height: cirleSize)
                    .foregroundColor(.black)
                    
                Image("iconr2")
                    .resizable()
                    .frame(width: 380, height: 380)
                    .rotationEffect(rotationAngle, anchor: .center)
                    .foregroundColor(.white)
                VStack{}.frame(width: 400, height: 400)
                    .background(Color.white.opacity(0.1))
                    
                ZStack{
                    ForEach(numbers, id: \.self) { number in
                        VStack{
                            Text("\(number)")
                                .font(.title)
                                .foregroundColor(selectedNumber == number ? .red : .white)
                                .rotationEffect(.degrees(dialRotationAngle))
                        }
                        .position(getPositionForNumber(number: number))
                        .padding(.all , 50)
                    }
                }.frame(width: 400, height: 400)
                    .rotationEffect(.degrees(16))
                    .gesture(DragGesture()
                        .onChanged({ value in
                            withAnimation{
                                if initialDragAngle == .zero {
                                    initialDragAngle = rotationAngle
                                }
                                
                                let dragVector = CGVector(dx: value.location.x, dy: value.location.y)
                                let radius = atan2(dragVector.dy, dragVector.dx)
                                
                                var angle = radius * 180 / .pi
                                if angle < 0 {angle = 360 + angle}
                                
                                rotationAngle += Angle(radians: Double(angle / 600))
                            }
                        })
                            .onEnded({ value in
                                withAnimation{
                                    let angleDifference = rotationAngle - initialDragAngle
                                    let angleInDegrees = angleDifference.degrees
                                    let numberChange = Int(angleInDegrees / 36)
                                    let finalNumber = (selectedNumber + numberChange) % 10
                                    selectedNumber = finalNumber
                                    
                                    rotationAngle = .zero
                                    initialDragAngle = .zero
                                }
                            })
                    )
            }
            .frame(width: 400, height: 400)
        }
    }

    func getPositionForNumber(number: Int) -> CGPoint {
        let angle = 2 * .pi * Double(number) / Double(numbers.count) + dialRotationAngle
        let radius: CGFloat = 160
        let x = radius * CGFloat(cos(angle)) + 150
        let y = radius * CGFloat(sin(angle)) + 150
        return CGPoint(x: x, y: y)
    }
    
    func getNumberForAngle(angle: CGFloat) -> Int {
        let angleInDegrees = angle * 180 / .pi
        var number = Int(angleInDegrees / 36)
        if number < 0 { number += 10 }
        return number % 10
    }
}

问题分析

  1. 拖拽角度计算错误:当前在onChanged中直接用拖拽点的绝对角度除以固定值累加旋转角度,没有基于拖拽的相对位移计算旋转幅度,导致旋转角度与实际拖拽操作完全不匹配。
  2. 初始偏移未补偿:数字环形容器设置了.rotationEffect(.degrees(16))的初始旋转,但在计算选中数字时没有考虑这个偏移,导致数字位置与选中逻辑的映射错位。
  3. 角度差计算逻辑缺陷:用rotationAngle - initialDragAngle得到的角度差没有处理正负方向和360度循环的问题,导致跨0度旋转时数字计算错误。

修复后的完整代码

struct ContentView: View {
    var body: some View {
        VStack {
            RotaryDialView()
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct RotatableImageView: View {
    @State private var rotationAngle: Angle = .zero
    @GestureState private var dragOffset: CGSize = .zero
    
    var body: some View {
        VStack{
            ZStack{
                Image("iconr")
                    .resizable()
                    .frame(width: 400, height: 400)
                    .rotationEffect(rotationAngle, anchor: .center)
                VStack{}.frame(width: 600, height: 600)
                    .background(Color.red.opacity(0.1))
                    .gesture(DragGesture()
                        .onChanged({ value in
                            withAnimation{
                                // 修复拖拽角度计算逻辑:基于起始点和当前点的相对角度差
                                let center = CGPoint(x: 300, y: 300)
                                let startVector = CGVector(dx: value.startLocation.x - center.x, dy: value.startLocation.y - center.y)
                                let currentVector = CGVector(dx: value.location.x - center.x, dy: value.location.y - center.y)
                                let startAngle = atan2(startVector.dy, startVector.dx)
                                let currentAngle = atan2(currentVector.dy, currentVector.dx)
                                let angleDiff = currentAngle - startAngle
                                
                                rotationAngle = Angle(radians: angleDiff)
                            }
                        })
                        .onEnded({ value in
                            withAnimation{
                                rotationAngle = .zero
                            }
                        })
                    )
            }
        }
    }
}

struct RotaryDialView: View {
    let numbers: [Int] = Array(0...9)
    @State var selectedNumber: Int = 0
    @State var pinArray: [Int] = []

    @State private var dialRotationAngle: Double = 0
    @State private var lastNumber: Int?
    // 补偿数字容器的初始旋转偏移
    private let initialContainerRotation: Double = 16
    
    @State private var rotationAngle: Angle = .zero
    @GestureState private var dragOffset: CGSize = .zero
    @State private var initialDragAngle: Angle = .zero
    @State var cirleSize:CGFloat = 310
    // 每个数字对应的角度间隔(360/10)
    private let anglePerNumber: Double = 36

    var body: some View {
        VStack {
            Text("number: \(selectedNumber) ")
            ZStack {
                Circle()
                    .stroke(style:StrokeStyle(lineWidth: 85))
                    .frame(width: cirleSize, height: cirleSize)
                    .foregroundColor(.black)
                    
                Image("iconr2")
                    .resizable()
                    .frame(width: 380, height: 380)
                    .rotationEffect(rotationAngle, anchor: .center)
                    .foregroundColor(.white)
                VStack{}.frame(width: 400, height: 400)
                    .background(Color.white.opacity(0.1))
                    
                ZStack{
                    ForEach(numbers, id: \.self) { number in
                        VStack{
                            Text("\(number)")
                                .font(.title)
                                .foregroundColor(selectedNumber == number ? .red : .white)
                                .rotationEffect(.degrees(dialRotationAngle))
                        }
                        .position(getPositionForNumber(number: number))
                        .padding(.all , 50)
                    }
                }.frame(width: 400, height: 400)
                    .rotationEffect(.degrees(initialContainerRotation))
                    .gesture(DragGesture()
                        .onChanged({ value in
                            withAnimation{
                                if initialDragAngle == .zero {
                                    initialDragAngle = rotationAngle
                                }
                                
                                // 修复拖拽角度计算:基于转盘中心的相对向量计算角度差
                                let center = CGPoint(x: 200, y: 200)
                                let startVector = CGVector(dx: value.startLocation.x - center.x, dy: value.startLocation.y - center.y)
                                let currentVector = CGVector(dx: value.location.x - center.x, dy: value.location.y - center.y)
                                
                                let startAngle = atan2(startVector.dy, startVector.dx) * 180 / .pi
                                let currentAngle = atan2(currentVector.dy, currentVector.dx) * 180 / .pi
                                
                                var angleDiff = currentAngle - startAngle
                                if angleDiff < -180 { angleDiff += 360 }
                                if angleDiff > 180 { angleDiff -= 360 }
                                
                                rotationAngle = initialDragAngle + Angle(degrees: angleDiff)
                            }
                        })
                        .onEnded({ value in
                            withAnimation{
                                // 修复选中数字计算:补偿初始偏移,处理角度循环
                                let totalRotation = rotationAngle.degrees - initialDragAngle.degrees
                                let adjustedRotation = totalRotation + initialContainerRotation
                                
                                var numberChange = Int((adjustedRotation / anglePerNumber).rounded())
                                numberChange = numberChange % 10
                                if numberChange < 0 { numberChange += 10 }
                                
                                selectedNumber = (selectedNumber + numberChange) % 10
                                if selectedNumber < 0 { selectedNumber += 10 }
                                
                                rotationAngle = .zero
                                initialDragAngle = .zero
                            }
                        })
                    )
            }
            .frame(width: 400, height: 400)
        }
    }

    func getPositionForNumber(number: Int) -> CGPoint {
        // 修复数字位置计算:加入初始容器偏移补偿
        let angle = 2 * .pi * Double(number) / Double(numbers.count) + (initialContainerRotation * .pi / 180)
        let radius: CGFloat = 160
        let center = CGPoint(x: 200, y: 200)
        let x = radius * CGFloat(cos(angle)) + center.x
        let y = radius * CGFloat(sin(angle)) + center.y
        return CGPoint(x: x, y: y)
    }
}

修复要点说明

  1. 正确计算拖拽角度差:基于转盘中心的起始点和当前点向量,计算相对角度差,确保旋转角度与拖拽操作一致。
  2. 补偿初始旋转偏移:在数字位置计算和选中逻辑中加入initialContainerRotation的补偿,解决数字位置映射错位问题。
  3. 处理角度循环逻辑:对角度差进行360度循环处理,避免跨0度旋转时的计算错误。
  4. 精确数字选中计算:使用角度差除以每个数字的间隔(36度)并取整,确保选中数字与旋转幅度匹配。

内容的提问来源于stack exchange,提问作者user22007718

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 17:17:03