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 } }
问题分析
- 拖拽角度计算错误:当前在
onChanged中直接用拖拽点的绝对角度除以固定值累加旋转角度,没有基于拖拽的相对位移计算旋转幅度,导致旋转角度与实际拖拽操作完全不匹配。 - 初始偏移未补偿:数字环形容器设置了
.rotationEffect(.degrees(16))的初始旋转,但在计算选中数字时没有考虑这个偏移,导致数字位置与选中逻辑的映射错位。 - 角度差计算逻辑缺陷:用
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) } }
修复要点说明
- 正确计算拖拽角度差:基于转盘中心的起始点和当前点向量,计算相对角度差,确保旋转角度与拖拽操作一致。
- 补偿初始旋转偏移:在数字位置计算和选中逻辑中加入
initialContainerRotation的补偿,解决数字位置映射错位问题。 - 处理角度循环逻辑:对角度差进行360度循环处理,避免跨0度旋转时的计算错误。
- 精确数字选中计算:使用角度差除以每个数字的间隔(36度)并取整,确保选中数字与旋转幅度匹配。
内容的提问来源于stack exchange,提问作者user22007718
相关产品推荐
相关产品推荐

