Mick F
Mick F

Reputation: 7439

Can a signal observer get access to the last emitted value of a ReactiveCocoa signal?

I'm starting using ReactiveCocoa and I'm still struggling with some basic concepts:

  1. My app starts listening for geolocation data (init in my view model)
  2. My app emits a signal with my current location (didFindCurrentPosition is called)
  3. My view controller showing a map loads (viewDidLoad in my view controller)
  4. My view controller starts observing the current location signal (still viewDidLoad)

My problem is: after step 2 is achieved, if no other event is sent on the signal, my view controller doesn't get notified.

How can my view controller get access to the last value from the signal? (ie how to get access at step 3 to a value emitted at step 2?)

Thanks for your help.

PS: ReactiveCocoa looks like a great library but I'm puzzled by the state of the documentation. IMHO, it is not very clear and lacks some clear guides on how to use it.

The Code

The view model:

class MyViewModel: LocationManagerDelegate {
    let locationManager: LocationManager
    let geolocationDataProperty = MutableProperty<Geolocation?>(nil)
    let geolocationData: Signal<Geolocation?, NoError>

    init() {
        geolocationData = geolocationDataProperty.signal

        // Location Management
        locationManager = LocationManager()
        locationManager.delegate = self
        locationManager.requestLocation()
    }

    // MARK: - LocationManagerDelegate

    func didFindCurrentPosition(location: CLLocation) {
        geolocationDataProperty.value = Geolocation(location: location)
    }
}

The view controller:

class MyViewController: UIViewController {
    let viewModel = MyViewModel()

    init() {
        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.geolocationData
            .observe(on: UIScheduler())
            .observeValues { geolocation in
                debugPrint("GOT GEOLOCATION")
        }
    }
}

Upvotes: 4

Views: 2235

Answers (3)

gkaimakas
gkaimakas

Reputation: 582

You already have a Property that holds the latest value emitted. Instead of using the property's signal use the producer. That way when you start the producer you will get the current value first (in none was sent you will get nil).

Cf. the documentation:

The current value of a property can be obtained from the value getter. The producer getter returns a signal producer that will send the property’s current value, followed by all changes over time. The signal getter returns a signal that will send all changes over time, but not the initial value.

So, regarding the code in the question, the viewDidLoad method should do something like the following:

viewModel.geolocationDataProperty
        .producer
        .start(on: UIScheduler())
        .startWithValues { geolocation in
            debugPrint("GOT GEOLOCATION")
    }

Upvotes: 5

Ellen
Ellen

Reputation: 5190

If your problem is to have old value when it gets modified .You can try with Key-Value Observing . Add your property for value observation like

 geolocationDataProperty.addObserver(self, forKeyPath: "value", options: [.new, .old], context: &observerContext)

Whenever your geolocationDataProperty gets modified ,you will receive that value in delegate method

 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        let newValue = change?[.newKey] as? NSObject
         let oldValue = change?[.oldKey] as? NSObject
//Add your code
    }

Upvotes: 0

cristallo
cristallo

Reputation: 2089

Bindings from any kind of streams of values can be crated using the <~ operator but you can start to modify your code in the following way and see it is working fine, it is easier to debug :-)

class MyViewModel: LocationManagerDelegate {
    let locationManager: LocationManager
    let geolocationDataProperty = MutableProperty<Geolocation?>(nil)

    init() {
        geolocationDataProperty.signal.observeValues { value in
            //use the value for updating the UI
        }

        // Location Management
        locationManager = LocationManager()
        locationManager.delegate = self
        locationManager.requestLocation()
    }

    // MARK: - LocationManagerDelegate

    func didFindCurrentPosition(location: CLLocation) {
        geolocationDataProperty.value = Geolocation(location: location)
    }
}

then we can try to use the binary operator for implementing the MVVM pattern, since having direct reference to the UI element inside the modelview is definitely a bad idea :-)

Take a look at this article, it is really interesting.

I am not sure if your map component is directly supporting the reactive framework.

see it has the ".reactive" extension and if it can used for updating the current position property

if it is present then the <~ operator can be used writing something similar to

map.reactive.position <~ viewModel.geolocationDataProperty

if it does not have the reactive extension then you can simply move this code in your viewcontroller

viewModel.geolocationDataProperty.signal.observeValues { value in
                //use the value for updating the UI
            }

Upvotes: 0

Related Questions