4

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")
        }
    }
}
Mick F
  • 7,312
  • 6
  • 51
  • 98
  • I guess that you are using a SignalProducer (cold signal) for getting the geolocation data. Are you using MutableProperty for implementing the data binding? – cristallo Jul 01 '17 at 08:50
  • @cristallo: i'm not using Reactive concepts to get the geolocation data, just regular CoreLocation delegate callbacks. I'm using a MutableProperty to store the location, and derive a signal from it. I added the code in my question to make things clearer. – Mick F Jul 03 '17 at 08:09

3 Answers3

5

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")
    }
Mick F
  • 7,312
  • 6
  • 51
  • 98
gkaimakas
  • 574
  • 3
  • 17
  • Thanks. That's it. I'll update your answer with a link to the doc referring to this + code. – Mick F Oct 04 '17 at 08:22
0

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
            }
cristallo
  • 1,951
  • 2
  • 25
  • 42
  • That's pretty much what I do already (cf. updated code). My problem is that the geolocation is found before viewDidLoad is called. And after viewDidLoad is called, the observer is never triggered as no update happened and I can't find a way to get the latest value the signal had seen (ie before viewDidLoad was called) – Mick F Jul 03 '17 at 09:21
  • The signal should be triggered every time that the didFindCurrentPosition is called. Is didFindCurrentPosition method called multiple times for updating your position during the app execution? – cristallo Jul 03 '17 at 09:26
  • I am pretty confused about this assignment "geolocationData = geolocationDataProperty.signal". it should not be needed since the mutable property has its own signaling system – cristallo Jul 03 '17 at 09:28
  • It's inspired by Kickstarter's MVVM implementation. It helps hiding implementation details of the view model and just exposes a signal output. – Mick F Jul 03 '17 at 09:50
  • In my tests, didFindCurrentPosition has always been called once (before viewDidLoad is called), and never afterwards. During my debugging process, I called it explicitely (after viewDidLoad) with dummy values to check that my bindings were OK and it worked fine (= the observer was called) – Mick F Jul 03 '17 at 09:51
  • it means that the RAC is properly configured... no signal will be triggered if the geolocationDataProperty is not modified. if you need the latest value received then you have only to read the value of the geolocationDataProperty from the viewcontroller. if didFindCurrentPosition is not triggered then the problem is not not in the RAC but it is related to the locationmanager configuration – cristallo Jul 03 '17 at 10:04
  • probably you have to use "startUpdatingLocation()" instead of "requestLocation()", if you need to update your position continuously – cristallo Jul 03 '17 at 10:13
  • If the app is missing only the first change it means that the observer is attached after the property modification. Did you try to move the observevalues inside the init of the viewcontroller (removing it from the didload)? – cristallo Jul 03 '17 at 10:23
  • It will sure work but my question is "can you read past values of the signal? (and keep observing new ones)" :) – Mick F Jul 04 '17 at 08:40
  • No, the Reactive framework does not provide any built-in history for the observed property. You have to implement your own history system that collect the old values. – cristallo Jul 04 '17 at 14:34
0

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
    }
Ellen
  • 5,180
  • 1
  • 12
  • 16
  • Thanks Ellen. But using KVO is somewhat defeating the purpose of investing into ReactiveCocoa. – Mick F Oct 04 '17 at 08:19