MutableProperty: выполнить метод при доступе к значению

Я использую ReactiveSwift + SDWebImage для загрузки/кэширования пользовательских аватаров API, а затем отображаю их в своих ViewControllers.

У меня есть несколько ViewController, которые хотят отображать аватар пользователя, а затем прослушивают его асинхронную загрузку.

Как мне лучше всего реализовать описанный ниже процесс?

Поток, который я хотел бы создать здесь:

  1. ViewControllerA хочу получить доступ к аватару пользователя
  2. это первый доступ к userAvatar, затем сделайте запрос API
  3. ViewControllerA прослушивает сигналы userAvatar
  4. ViewControllerA временно отображать заполнитель
  5. ViewControllerB хочу получить доступ к аватару пользователя
  6. ViewControllerB прослушивает сигналы userAvatar
  7. ViewControllerB временно отображать заполнитель
  8. API-запрос аватара пользователя завершен, затем отправьте сигнал, наблюдаемый контроллерами представления.
  9. контроллеры просмотра обновляют свои UIImageView свежим изображением

Это мой фактический код:

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)
    }
}

Я не считаю, что вызов user.loadAvatarImage() до прослушивания сигнала очень чистый...

Я знаю, что мой код не такой "реактивный", я все еще новичок в концепции реактивного. Не стесняйтесь критиковать, я пытаюсь улучшить себя

Заранее спасибо за совет.


person Toldy    schedule 29.03.2018    source источник


Ответы (1)


Лучший способ справиться с этой ситуацией — создать SignalProducer, который:

  1. если image уже загружен при запуске SignalProducer: немедленно выдает .value(image), а затем .completed

  2. если image в настоящее время загружается, когда SignalProducer запускается: когда image заканчивает загрузку, выдает .value(image), а затем .completed

  3. если image не был загружен и в настоящее время не загружается, когда SignalProducer запускается: инициирует загрузку image, а когда image загрузка заканчивается, выдается .value(image), за которым следует .completed

ReactiveSwift предоставляет нам «ручной» конструктор для производителей сигналов, который позволяет нам писать императивный код, который запускается каждый раз при запуске производителя сигналов:

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
  }
}

Чтобы сделать ваш код еще более «реактивным», вы можете использовать библиотеку ReactiveCocoa для привязки вашего avatarImageSignalProducer к пользовательскому интерфейсу:

ReactiveCocoa не поставляется со встроенным BindingTarget для UIImageView.image, поэтому мы сами пишем расширение:

import ReactiveCocoa

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

это позволяет нам использовать операторы привязки ReactiveCocoa в ViewControllers для очистки нашего кода в viewDidLoad/cellForRowAtIndexPath/etc следующим образом:

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
  }
}

Также важно думать о памяти и циклических ссылках при привязке данных к пользовательскому интерфейсу, на который не ссылается в памяти контроллер представления (например, если наш пользователь является глобальной переменной, которая остается в памяти после освобождения VC, а не свойством ВК). В этом случае мы должны явно прекратить прослушивание сигнала при освобождении VC, иначе его память никогда не будет освобождена. Вызовы .take(until: cell.reactive.prepareForReuse) и .take(during: self.reactive.lifetime) в приведенном выше коде являются примерами явной остановки сигнала для целей управления памятью.

person Evan Drewry    schedule 02.04.2018