SwiftUI指南针刻度标记旋转动画不生效问题咨询
SwiftUI指南针刻度标记动画失效问题
我用SwiftUI制作了一款基于iPhone 13 Pro物理指南针数据的简易指南针视图。当前航向数值准确,视图可正常旋转,但所有动画均无法作用于刻度标记——不过旋转整个ZStack时动画表现正常。相关代码如下:
// // CompassView.swift // ExtasyCompleteNavigation // // Created by Vasil Borisov on 20.08.23. // import SwiftUI struct CompassView: View { var heading: CGFloat var body: some View { VStack{ ZStack{ GeometryReader{ geometry in let width = geometry.size.width let height = geometry.size.height let fontSize = width/13 //compass background Circle() //compass heading display and direction Text("\(-heading,specifier: \"%.0f\")°\n\(displayDirection())") .font(Font.custom("AppleSDGothicNeo-Bold", size: width/13)) .foregroundColor(.white) .position(x: width/2, y:height/2) //compass degree ring Group{ MyShape(sections: 12, lineLengthPercentage: 0.15) .stroke(Color.white, style: StrokeStyle(lineWidth: 5)) MyShape(sections: 360, lineLengthPercentage: 0.15) .stroke(Color.white, style: StrokeStyle(lineWidth: 2)) //compass arrow Text("▼") .font(Font.custom("AppleSDGothicNeo-Bold", size: fontSize)) .position(x:width/2, y: height/200 ) .foregroundColor(.red) PseudoBoat() .stroke(lineWidth: 4) .foregroundColor(.white) .scaleEffect(x: 0.30, y: 0.55, anchor: .top) .offset(y: geometry.size.height/5) } //heading values ForEach(GaugeMarkerCompass.labelSet()) { marker in CompassLabelView(marker: marker, geometry: geometry) .position(CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2)) } .rotationEffect(.init(degrees: heading)) .animation(Animation.easeInOut(duration: 3), value: heading) }.aspectRatio(1, contentMode: .fit) } } } //function can be moved in a structure with the rest of the tools in swift file func displayDirection() -> String { switch heading { case 0: return "N" case 90: return "E" case 180: return "S" case 270: return "W" case 90..<180: return "SE" case 180..<270: return "SW" case 270..<360: return "NW" default: return "NE" } } //to be moved to a swift file and keep it separate public struct CompassLabelView: View { let marker: GaugeMarker let geometry: GeometryProxy @State var fontSize: CGFloat = 12 @State var paddingValue: CGFloat = 100 public var body: some View { VStack { Text(marker.label) .foregroundColor(Color.gray) .font(Font.custom("AppleSDGothicNeo-Bold", size: geometry.size.width * 0.05)) .rotationEffect(Angle(degrees: 0)) .padding(.bottom, geometry.size.width * 0.72) }.rotationEffect(Angle(degrees: marker.degrees)) .onAppear { paddingValue = geometry.size.width * 0.72 fontSize = geometry.size.width * 0.07 } } } struct GaugeMarkerCompass: Identifiable, Hashable { let id = UUID() let degrees: Double let label: String init(degrees: Double, label: String) { self.degrees = degrees self.label = label } // adjust according to your needs static func labelSet() -> [GaugeMarker] { return [ GaugeMarker(degrees: 0, label: "N"), GaugeMarker(degrees: 30, label: "30"), GaugeMarker(degrees: 60, label: "60"), GaugeMarker(degrees: 90, label: "E"), GaugeMarker(degrees: 120, label: "120"), GaugeMarker(degrees: 150, label: "150"), GaugeMarker(degrees: 180, label: "S"), GaugeMarker(degrees: 210, label: "210"), GaugeMarker(degrees: 240, label: "240"), GaugeMarker(degrees: 270, label: "W"), GaugeMarker(degrees: 300, label: "300"), GaugeMarker(degrees: 330, label: "330") ] } } } struct CompassView_Previews: PreviewProvider { static var previews: some View { CompassView(heading: 0) .background(.white) } }
问题根源
- 不稳定的视图标识:
GaugeMarkerCompass使用UUID()作为id,每次heading变化时,labelSet()生成的新数组会包含全新标识的元素,SwiftUI会销毁旧刻度视图并重建,直接跳过动画。 - GeometryProxy传递干扰:直接将
GeometryProxy传给子视图CompassLabelView,GeometryProxy是值类型,布局变化时会生成新实例,触发子视图强制重建,打断动画流程。 - 不必要的@State变量:
CompassLabelView中的fontSize和paddingValue未实际使用,反而可能引发视图状态异常。
修复方案
1. 替换为稳定的视图ID
将GaugeMarkerCompass的id改为基于唯一的degrees值,确保视图标识稳定:
struct GaugeMarkerCompass: Identifiable, Hashable { let id: Double // 用度数作为唯一ID,每个标记度数唯一 let degrees: Double let label: String init(degrees: Double, label: String) { self.id = degrees self.degrees = degrees self.label = label } // ... 其余代码不变 }
2. 避免直接传递GeometryProxy
将子视图需要的尺寸参数从父视图计算后传递,而非整个GeometryProxy:
// 修改CompassLabelView public struct CompassLabelView: View { let marker: GaugeMarkerCompass let size: CGFloat // 直接传入计算好的尺寸 public var body: some View { VStack { Text(marker.label) .foregroundColor(Color.gray) .font(Font.custom("AppleSDGothicNeo-Bold", size: size * 0.05)) .padding(.bottom, size * 0.72) } .rotationEffect(Angle(degrees: marker.degrees)) } } // 父视图中调用时传递尺寸 ForEach(GaugeMarkerCompass.labelSet()) { marker in CompassLabelView(marker: marker, size: width) .position(CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2)) }
3. 优化动画配置
调整动画时长为更符合指南针的流畅速度,并确保动画仅作用于旋转变化:
ForEach(GaugeMarkerCompass.labelSet()) { marker in CompassLabelView(marker: marker, size: width) .position(CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2)) } .rotationEffect(.init(degrees: heading)) .animation(.easeInOut(duration: 0.3), value: heading)
4. 移除无用变量
删除CompassLabelView中未使用的@State变量:
public struct CompassLabelView: View { let marker: GaugeMarkerCompass let size: CGFloat public var body: some View { VStack { Text(marker.label) .foregroundColor(Color.gray) .font(Font.custom("AppleSDGothicNeo-Bold", size: size * 0.05)) .padding(.bottom, size * 0.72) } .rotationEffect(Angle(degrees: marker.degrees)) } }
修复后完整代码
// // CompassView.swift // ExtasyCompleteNavigation // // Created by Vasil Borisov on 20.08.23. // import SwiftUI struct CompassView: View { var heading: CGFloat var body: some View { VStack{ ZStack{ GeometryReader{ geometry in let width = geometry.size.width let height = geometry.size.height let fontSize = width/13 //compass background Circle() //compass heading display and direction Text("\(-heading,specifier: \"%.0f\")°\n\(displayDirection())") .font(Font.custom("AppleSDGothicNeo-Bold", size: width/13)) .foregroundColor(.white) .position(x: width/2, y:height/2) //compass degree ring Group{ MyShape(sections: 12, lineLengthPercentage: 0.15) .stroke(Color.white, style: StrokeStyle(lineWidth: 5)) MyShape(sections: 360, lineLengthPercentage: 0.15) .stroke(Color.white, style: StrokeStyle(lineWidth: 2)) //compass arrow Text("▼") .font(Font.custom("AppleSDGothicNeo-Bold", size: fontSize)) .position(x:width/2, y: height/200 ) .foregroundColor(.red) PseudoBoat() .stroke(lineWidth: 4) .foregroundColor(.white) .scaleEffect(x: 0.30, y: 0.55, anchor: .top) .offset(y: geometry.size.height/5) } //heading values ForEach(GaugeMarkerCompass.labelSet()) { marker in CompassLabelView(marker: marker, size: width) .position(CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2)) } .rotationEffect(.init(degrees: heading)) .animation(.easeInOut(duration: 0.3), value: heading) }.aspectRatio(1, contentMode: .fit) } } } func displayDirection() -> String { switch heading { case 0: return "N" case 90: return "E" case 180: return "S" case 270: return "W" case 90..<180: return "SE" case 180..<270: return "SW" case 270..<360: return "NW" default: return "NE" } } public struct CompassLabelView: View { let marker: GaugeMarkerCompass let size: CGFloat public var body: some View { VStack { Text(marker.label) .foregroundColor(Color.gray) .font(Font.custom("AppleSDGothicNeo-Bold", size: size * 0.05)) .padding(.bottom, size * 0.72) } .rotationEffect(Angle(degrees: marker.degrees)) } } struct GaugeMarkerCompass: Identifiable, Hashable { let id: Double let degrees: Double let label: String init(degrees: Double, label: String) { self.id = degrees self.degrees = degrees self.label = label } static func labelSet() -> [GaugeMarkerCompass] { return [ GaugeMarkerCompass(degrees: 0, label: "N"), GaugeMarkerCompass(degrees: 30, label: "30"), GaugeMarkerCompass(degrees: 60, label: "60"), GaugeMarkerCompass(degrees: 90, label: "E"), GaugeMarkerCompass(degrees: 120, label: "120"), GaugeMarkerCompass(degrees: 150, label: "150"), GaugeMarkerCompass(degrees: 180, label: "S"), GaugeMarkerCompass(degrees: 210, label: "210"), GaugeMarkerCompass(degrees: 240, label: "240"), GaugeMarkerCompass(degrees: 270, label: "W"), GaugeMarkerCompass(degrees: 300, label: "300"), GaugeMarkerCompass(degrees: 330, label: "330") ] } } } struct CompassView_Previews: PreviewProvider { static var previews: some View { CompassView(heading: 0) .background(.white) } }
内容的提问来源于stack exchange,提问作者bacata.borisov
相关产品推荐
相关产品推荐

