Toldy
Toldy

Reputation: 1251

MutableProperty: execute method on value access

I'm using ReactiveSwift + SDWebImage to download/cache userAvatars of an API and then I display them in my ViewControllers.

I have multiple ViewControllers which want to display the userAvatar, then they listen to its async loading.

What is the best way for me to implement the flow described below?

The flow I would like to create here is:

  1. ViewControllerA want to access userAvatar
  2. it is the first time userAvatar is accessed then make an API request
  3. ViewControllerA listens for userAvatar signals
  4. ViewControllerA temporarily display a placeholder
  5. ViewControllerB want to access userAvatar
  6. ViewControllerB listens for userAvatar signals
  7. ViewControllerB temporarily display a placeholder
  8. API request of the userAvatar is completed, then send a signal observed by the viewcontrollers
  9. viewcontrollers are refreshing their UIImageView with the fresh image

This is my actual code:

class ViewControllerA {

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // ... Cell creation

        // type(of: user) == User.self (see class User below)
        user.loadAvatarImage()
        disposable = user.image.producer
            .observe(on: UIScheduler())
            .startWithValues { image in
                // image is is either a placeholder or the real avatar
                cell.userImage.image = image
        }
    }
}

class ViewControllerB {

    override func viewDidLoad() {
        super.viewDidLoad()

        // type(of: user) == User.self (see class User below)
        user.loadAvatarImage()
        disposable = user.image.producer
            .observe(on: UIScheduler())
            .startWithValues { image in
                // image is is either a placeholder or the real avatar
                headerImageView.image = image
        }
    }
}

class User: Mappable {

    // ... User implementation

    let avatarImage = MutableProperty<UIImage?>(nil)

    // To call before accessing avatarImage.value
    func loadAvatarImage() {
        getAvatar { image in
            self.avatarImageProperty.value = image
        }
    }

    private func getAvatar(completion: @escaping ((UIImage) -> Void)) {
        // ... Async image download
        competion(image)
    }
}

I don't find that calling user.loadAvatarImage() before listening to the signal is very clean...

I know my code isn't so "Reactive", I still new with Reactive concept. Feel free to criticize, I'm trying to improve myself

Thanks in advance for your advice.

Upvotes: 1

Views: 417

Answers (1)

Evan Drewry
Evan Drewry

Reputation: 804

The best way to handle this situation is to create a SignalProducer that:

  1. if image is already downloaded when the SignalProducer is started: immediately emits .value(image) followed by .completed

  2. if image is currently downloading when the SignalProducer is started: when image is finished downloading, emits .value(image) followed by .completed

  3. if image has not been downloaded and is not currently downloading when the SignalProducer is started: initiates download of image, and when image is finished downloading emits .value(image) followed by .completed

ReactiveSwift provides us with a "manual" constructor for signal producers that allows us to write imperative code that runs every time the signal producer is started:

private let image = MutableProperty<UIImage?>(.none)
private var imageDownloadStarted = false

public func avatarImageSignalProducer() -> SignalProducer<UIImage, NoError> {
  return SignalProducer { observer, lifetime in
    //if image download hasn't started, start it now
    if (!self.imageDownloadStarted) {
      self.imageDownloadStarted = true
      self.getAvatar { self.image = $0 }
    }
    //emit .value(image) followed by .completed when the image has downloaded, or immediately if it has already downloaded
    self.image.producer //use our MutableProperty to get a signalproducer for the image download
      .skipNil() //dont send the nil value while we wait for image to download
      .take(first: 1) //send .completed after image value is sent
      .startWithSignal { $0.observe(observer) } //propogate these self.image events to the avatarImageSignalProducer
  }
}

To make your code even more "reactive" you can use the ReactiveCocoa library to bind your avatarImageSignalProducer to the UI:

ReactiveCocoa does not come with a BindingTarget for UIImageView.image built in, so we write an extension ourselves:

import ReactiveCocoa

extension Reactive where Base: UIImageView {
  public var image: BindingTarget<UIImage> {
    return makeBindingTarget { $0.image = $1 }
  }
}

this lets us use the ReactiveCocoa binding operators in the ViewControllers to clean up our code in viewDidLoad/cellForRowAtIndexPath/etc like so:

class ViewControllerA {

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // ... Cell creation

    cell.userImage <~ user.avatarImageSignalProducer()
      .take(until: cell.reactive.prepareForReuse) //stop listening to signal & free memory when cell is reused before image loads
  }
}

class ViewControllerB {

  override func viewDidLoad() {
    headerImageView.image <~ user.avatarImageSignalProducer()
      .take(during: self.reactive.lifetime) //stop listening to signal & free memory when VC is deallocated before image loads
  }
}

It is also important to think about memory & cyclical references when binding data to the UI that is not referenced in memory by the viewcontroller (e.g. if our User is a global variable that stays in memory after the VC is deallocated rather than a property of the VC). When this is the case we must explicitly stop listening to the signal when the VC is deallocated, or its memory will never be freed. The calls to .take(until: cell.reactive.prepareForReuse) and .take(during: self.reactive.lifetime) in the code above are both examples of explicitly stopping a signal for memory management purposes.

Upvotes: 1

Related Questions