I am making a simple compass view with just basic things for a view and I would like to have it animated. I am using the compass of my physical device (iPhone 13 PRO). So, everything seems to be fine - heading is correct, view is rotating but... the animation does not work, actually any of the animation types. However if I use it to rotate the whole ZStack is fine. It doesn't work once I am trying to rotate the gauge markers. Please see code below:
//
// 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)
}
}
Try this approach, where you attach
the rotation and the animation to the VStack
,
as shown in the example code, or if you prefer,
you can attach
the rotationEffect and animation to the
CompassView
itself in ContentView
.
Also rename GaugeMarkerCompass
to GaugeMarker
struct ContentView: View {
@State var heading: CGFloat = 0.0 // <-- here
var body: some View {
VStack (alignment: .leading, spacing: 44) {
// for testing
Button("change heading") {
heading += 10.0
}.padding(10).buttonStyle(.bordered)
CompassView(heading: heading) // <-- here
// .rotationEffect(.init(degrees: heading)) // <--- here
// .animation(.linear(duration: 2), value: heading) // <--- here
}
}
}
struct CompassView: View {
let heading: CGFloat // <-- here
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(GaugeMarker.labelSet()) { marker in // <-- here
CompassLabelView(marker: marker, geometry: geometry)
.position(CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2))
}
// <--- NOT here
}.aspectRatio(1, contentMode: .fit)
}
// --- here to stop the fading of labels
.transaction { transaction in
transaction.animation = nil
}
}
.rotationEffect(.init(degrees: heading)) // <--- here
.animation(Animation.linear(duration: 2), value: heading) // <--- here
}
//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"
}
}
}