How can I get data from ObservedObject with onReceive in SwiftUI?
First in your view you need to request the HeadingProvider to start updating heading. You need to listen to objectWillChange notification, the closure has one argument which is the new value that is being set on ObservableObject.
I have changed your Compass a bit:
struct Compass: View {
@StateObject var headingProvider = HeadingProvider()
@State private var angle: CGFloat = 0
var body: some View {
VStack {
Image("arrow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
.modifier(RotationEffect(angle: angle))
.onReceive(self.headingProvider.objectWillChange) { newHeading in
withAnimation(.easeInOut(duration: 1.0)) {
self.angle = newHeading
}
}
Text(String("\(angle)"))
.font(.system(size: 20))
.fontWeight(.light)
.padding(.top, 15)
} .onAppear(perform: {
self.headingProvider.updateHeading()
})
}
}
I have written an example HeadingProvider:
public class HeadingProvider: NSObject, ObservableObject {
public let objectWillChange = PassthroughSubject<CGFloat,Never>()
public private(set) var heading: CGFloat = 0 {
willSet {
objectWillChange.send(newValue)
}
}
private let locationManager: CLLocationManager
public override init(){
self.locationManager = CLLocationManager()
super.init()
self.locationManager.delegate = self
}
public func updateHeading() {
locationManager.startUpdatingHeading()
}
}
extension HeadingProvider: CLLocationManagerDelegate {
public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
DispatchQueue.main.async {
self.heading = CGFloat(newHeading.trueHeading)
}
}
}
Remember you need to handle asking for permission to read user's location and you need to call stopUpdatingHeading() at some point.
iOS 14+
Starting from iOS 14 we can now use the onChange
modifier that performs an action every time a value is changed:
Here is an example:
struct Compass: View {
@StateObject var location = LocationManager()
@State private var angle: CGFloat = 0
var body: some View {
VStack {
// ...
}
.onChange(of: location.heading) { heading in
withAnimation(.easeInOut(duration: 1.0)) {
self.angle = heading
}
}
}
}
You can consider using @Published in your ObservableObject. Then your onreceive can get a call by using location.$heading.
For observable object it can be
class LocationManager: ObservableObject {
@Published var heading:Angle = Angle(degrees: 20)
}
For the receive you can use
struct Compass: View {
@ObservedObject var location: LocationManager = LocationManager()
@State private var angle: Angle = Angle(degrees:0)
var body: some View {
VStack {
Image("arrow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
.modifier(RotationEffect(angle: angle))
.onReceive(location.$heading, perform: { heading in
withAnimation(.easeInOut(duration: 1.0)) {
self.angle = heading
}
})
}
}
}
The above is useful if you want to perform additional functions on object changes. In many cases you can directly use the location.heading as your state changer. And then give it an animation below it. So
.modifier(RotationEffect(angle: location.heading))
.animation(.easeInOut)