How can I get data from ObservedObject with onReceive in SwiftUI?

26,649

Solution 1

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.

Solution 2

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)

Solution 3

SwiftUI 2 / 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
            }
        }
    }
}
Share:
26,649
Guillaume
Author by

Guillaume

Updated on June 17, 2021

Comments

  • Guillaume
    Guillaume almost 3 years

    In my SwiftUI app, I need to get data from ObservedObject each time the value change. I understood that we could do that with .onReceive? I don't understand well the documentation of Apple about it. I don't know how I can do this.

    My code:

    import SwiftUI
    import CoreLocation
    
    struct Compass: View {
      
      @StateObject var location = LocationManager()
      @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: -CGFloat(self.angle.degreesToRadians)))
            .onReceive(location, perform: {
              withAnimation(.easeInOut(duration: 1.0)) {
                self.angle = self.location.heading
              }
            })
          
          Text(String(self.location.heading.degreesToRadians))
            .font(.system(size: 20))
            .fontWeight(.light)
            .padding(.top, 15)
        }
      }
    }
    
    struct RotationEffect: GeometryEffect {
      var angle: CGFloat
    
      var animatableData: CGFloat {
        get { angle }
        set { angle = newValue }
      }
    
      func effectValue(size: CGSize) -> ProjectionTransform {
        return ProjectionTransform(
          CGAffineTransform(translationX: -150, y: -150)
            .concatenating(CGAffineTransform(rotationAngle: angle))
            .concatenating(CGAffineTransform(translationX: 150, y: 150))
        )
      }
    }
    

    In my LocationManager class, I have a heading Published variable, this is the variable I want check.

    I need to get data each time the value of heading change to create an animation when my arrow move. For some raisons I need to use CGAffineTransform.

  • Guillaume
    Guillaume over 4 years
    Hi, thanks, I understand better now. But I have this message : Cannot convert value of type '(Double) -> ()' to expected argument type '(_) -> Void' I need to add something else?
  • user1687195
    user1687195 about 4 years
    No - objectWillChange has it's type defined by an associated type, so what @LuLuGaGa suggested is fine: var objectWillChange: Self.ObjectWillChangePublisher
  • malhal
    malhal over 3 years
    Apple say you can re-define objectWillChange to be whatever kind of Publisher you like. WWDC2020 Data essentials in SwiftUI
  • tyirvine
    tyirvine over 3 years
    I found this answer a little bit easier to follow. Thank you! (I think this should be the accepted answer)
  • rsa
    rsa over 3 years
    Thank you! I believe this was the solution to my question stackoverflow.com/questions/65874566/…. I used onReceive with @Published to have a running timer continue to publish state changes to my View.