简体   繁体   中英

Async image loading with ReactiveCocoa (4.2.1) and Swift

I'm a beginner using ReactiveCocoa with Swift for the first time. I'm building an app showing a list of movies and I'm using the MVVM pattern. My ViewModel looks like this:

class HomeViewModel {

    let title:MutableProperty<String> = MutableProperty("")
    let description:MutableProperty<String> = MutableProperty("")
    var image:MutableProperty<UIImage?> = MutableProperty(nil)

    private var movie:Movie

    init (withMovie movie:Movie) {

        self.movie = movie

        title.value = movie.headline
        description.value = movie.description

        Alamofire.request(.GET, movie.pictureURL)
            .responseImage { response in

                if let image = response.result.value {
                    print("image downloaded: \(image)")
                    self.image.value = image
                }
        }

    }
}

and I would like to configure my cells in the UITableView like this:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCellWithIdentifier("MovieCell", forIndexPath: indexPath) as! MovieCell
    let movie:Movie = movieList[indexPath.row]
    let vm = HomeViewModel(withMovie: movie)

    // fill cell with data
    vm.title.producer.startWithNext { (newValue) in
        cell.titleLabel.text = newValue
    }

    vm.description.producer.startWithNext { (newValue) in
        cell.descriptioLabel.text = newValue
    }

    vm.image.producer.startWithNext { (newValue) in
        if let newValue = newValue {
            cell.imageView?.image = newValue as UIImage
        }
    }

    return cell
}

Is this the right approach for Reactive Cocoa? Do I need to declare Title and description as Mutable or just image (being the only one changing). I think I could use binding but I'm not sure how to proceed.

to do this using Reactive Cocoa + MVVM patterns i would first move all the logic to configure the cell from its viewmodel into the cell class itself. and then remove the MutableProperties from the viewModel (they aren't actually mutable and we dont need those signals). and for the image expose a signal producer that will perform the network request to fetch the image when start() is called, rather than implicitly fetching it when init is called on the ViewModel, giving us something like

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("MovieCell", forIndexPath: indexPath) as! MovieCell
  cell.viewModel = self.viewModelForIndexPath(indexPath)
  return cell
}

private func viewModelForIndexPath(indexPath: NSIndexPath) -> MovieCellViewModel {
  let movie: Movie = movieList[indexPath.row]
  return HomeViewModel(movie: movie)
}

and then

class MovieCell: UITableViewCell
  @IBOutlet weak var titleLabel: UILabel
  @IBOutlet weak var descriptionLabel: UILabel
  @IBOutlet weak var imageView: UIImageView

  var viewModel: MovieCellViewModel {
    didSet {
      self.configureFromViewModel()
    }
  }

  private func configureFromViewModel() {
    self.titleLabel.text = viewModel.title
    self.descriptionLabel.text = viewModel.description
    viewModel.fetchImageSignal()
      .takeUntil(self.prepareForReuseSignal()) //stop fetching if cell gets reused
      .startWithNext { [weak self] image in
        self?.imageView.image = image
      }
  }

  //this could also go in a UITableViewCell extension if you want to use it other places
  private func prepareForReuseSignal() -> Signal<(), NoError> {
    return Signal { observer in
      self.rac_prepareForReuseSignal // reactivecocoa builtin function
        .toSignalProducer() // obj-c RACSignal -> swift SignalProducer
        .map { _ in () } // AnyObject? -> Void
        .flatMapError { _ in .empty } // NSError -> NoError
        .start(observer)
    }
  }
}

and in the ViewModel

struct HomeViewModel {
  private var movie: Movie

  var title: String {
    return movie.headline
  }

  var description: String {
    return movie.description
  }

  func fetchImageSignal() -> SignalProducer<UIImage, NSError> {
    return SignalProducer { observer, disposable in
      Alamofire.request(.GET, movie.pictureURL)
        .responseImage { response in
          if let image = response.result.value {
            print("image downloaded: \(image)")
            observer.sendNext(image) //send the fetched image on the signal
            observer.sendCompleted()
          } else {
            observer.sendFailed( NSError(domain: "", code: 0, userInfo: .None)) //send your error
          }
        }
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM