Tham gia ngay khoá học lập trình iOS, hình thức học tập rất linh hoạt cho bạn lựa chọn và sẽ có mức học phí khác nhau tuỳ theo bạn chọn học Online, Offline hoặc FlipLearning(Kết hợp giữa Online và Offline). Ngoài ra bạn có thể tham gia thực tập toàn thời gian tại Techmaster để rút ngắn thời gian học và tăng cơ hội việc làm.

Ở phần 2 này chúng ta sẽ sử dụng RxSwift để thực hiện binding dữ liệu: Chúng ta đã thấy những lợi ích của mô hình MVVM ở phần đầu tiên nhưng về phần hiển thị dữ liệu và việc tự cập nhật giao diện khi dữ liệu thay đổi thì chúng ta sẽ cùng tìm hiểu ở bài này.

Nên dùng gì?

Trong iOS thì điều đâu tiên tôi nghĩ đến là KVO(Key-Value Observing), nhưng chúng ta sẽ phải viết rất nhiều các khuôn mẫu với nhiều trường hợp vậy phải làm như nao?

Vì dụ tôi muốn sử dụng khái niệm lập trình Functional reactive programming. Nếu bạn không biết về nó thì bạn đơn giản bạn chỉ cần google cái là ra rất nhiều kết quả.

Có rất nhiều framework sử dụng tư tưởng FRP và trong đó có ReactiveCocoa và RxSwift.

Trước đây với nhưng dự án sử dụng Objective-C, tôi cũng có biết về ReactiveCocoa, nhưng với dự án về Swift thì tôi khuyên bạn nên sử dụng RxSwift và trong bài này chúng ta sẽ sử dụng RxSwift. Để giải thích việc nó hoạt động thì chúng ta sẽ đi thẳng vào ví dụ.

Cài đặt RxSwift

Chắc bạn đã quen thuộc với việc cài đặt một framework sử dụng pod file rồi:

use_frameworks!
 
def rx_swift
  pod 'RxSwift'
  pod 'RxCocoa'
end
 
target 'MVVM' do
  rx_swift
end
 
target 'MVVMTests' do
  rx_swift
end
 
target 'MVVMUITests' do
 
end

Chạy pod install và ở file MVVM.xcworkspace.

Reactive với CarViewModel

Dĩ nhiên với vì dụ này chúng ta sẽ cho class CarViewModel reactive. Car model sẽ không thay đổi, nó sẽ không giao tiếp trực tiếp với các đối tượng UI, nó chỉ có tác dụng lưu dữ liệu.

Tiếp theo chúng ta sẽ làm việc với signals, observes, và observables, chúng ta cần chắc chắc cần clean các đối tượng khi không sử dụng nữa tránh trường hợp, một đối tượng không sử dụng nữa mà vẫn tồn tại nó sẽ là nguyên nhân gây ra hiện tượng memory leaks.

Điều đầu tiên chúng ta sẽ them vào một dispose bag.

import RxSwift
import RxCocoa
 
// class CarViewModel {
// ...
 
  let disposeBag = DisposeBag()
 
// ...
// }

Chúng ta sẽ thay đổi các thuộc tính ở CarViewModel và kiểu BehaviorSubject phù hợp trong trường hợp này:

// class CarViewModel {
// ...
 
  var modelText: BehaviorSubject<String>
  var makeText: BehaviorSubject<String>
  var horsepowerText: BehaviorSubject<String>
  var kilowattText: BehaviorSubject<String>
  var titleText: BehaviorSubject<String>
 
// ...
// }

Ở đây tôi thêm một thuộc tính mới là kilowattText, và ở class CarViewModel chúng ta cần chỉnh lại như sau:

// class CarViewModel {
// ...
 
init(car: Car) {
  self.car = car
  
  // 1
  modelText = BehaviorSubject<String>(value: car.model) // initializing with the current value of car.model
  modelText.subscribeNext { (model) in
    car.model = model                                   // subscribing to changes in modelText which will be reflected in CarViewModel's car
  }.addDisposableTo(disposeBag)
  
  // 2
  makeText = BehaviorSubject<String>(value: car.make)
  makeText.subscribeNext { (make) in
    car.make = make
  }.addDisposableTo(disposeBag)
  
  // 3
  titleText = BehaviorSubject<String>(value: "\(car.make) \(car.model)")
  [makeText, modelText].combineLatest { (carInfo) -> String in
    return "\(carInfo[0]) \(carInfo[1])"
  }.bindTo(titleText).addDisposableTo(disposeBag)
  
  // 4
  horsepowerText = BehaviorSubject(value: "0")
  kilowattText = BehaviorSubject(value: String(car.kilowatts))
  kilowattText.map({ (kilowatts) -> String in
    let kw = Int(kilowatts) ?? 0
    let horsepower = max(Int(round(Double(kw) * CarViewModel.horsepowerPerKilowatt)), 0)
    return "\(horsepower) HP"
  }).bindTo(horsepowerText).addDisposableTo(disposeBag)
 
}
 
// ...
// }

Ở //1 và //2 đơn giản tôi khởi tạo BehaviorSubject với giá trị hiện tại của Car.

Với //3 tôi kết hợp 2 giá trị của car.make và car.model.

Cuối cùng //4 tôi gọi đến method map() để tính toán horsepower từ kilowatts. Ở đây tôi sẽ ép kiểu string thành kiểu int và tính toán.

Bạn cần chú ý add disposeBag vào các biến ở trên.

Nếu bây giờ bạn chạy project thì nó sẽ có lỗi ở 2 dòng trong TableViewController:

// override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// ...
 
  cell.textLabel?.text = carViewModel.titleText
  cell.detailTextLabel?.text = carViewModel.horsepowerText
 
// ...
// }

Vì titleText và horsepowerText không còn là kiểu String nữa, để giải quyết vấn đề này chúng ta đến phần tiếp theo.

ReactiveTableViewController

Đầu tiên bạn xoá TableViewController.swift hiện tại vào tạo class mới đặt tên là ReactiveTableViewController có kiểu UIViewCOntroller như sau:

Chọn viewcontroller chúng ta mới thêm và chọn class ReactiveTableViewController là class implement:

Chú ý là ở đây tôi sẽ thêm một navigation controller Editor > Embed In > Navigation Controller.

Vì ReactiveTableViewController có kiểu UIViewController nên chúng ta cần thêm một tableview, tôi sẽ layout cho nó full screen như sau:

Tiếp theo các bạn ánh xạ tableview vào ReactiveTableViewController.

Custom tableview cell

Kéo uiimageview và uilabel vào để hiển thị:

Thêm class CarTableViewCell:

Tiếp theo chọn CarTableViewCell là class mà các cells trên tableview implement theo và liên kết các outlets như sau:

Ở class CarTableViewCell chúng ta sẽ cần thêm một DisposeBag và 1 biến để lưu giá trị của CarViewModel:

import RxSwift // Don't forget this!
 
// class CarTableViewCell: UITableViewCell {
// ...
  
  let disposeBag = DisposeBag()
  var carViewModel: CarViewModel?
 
// ...
// }

Thêm các đoạn mã reactive cho class ReactiveTableViewController

Ở đây như thường lệ chúng ta sẽ cần dữ liệu và một DisposeBag

import RxSwift // Don't forget this!
 
// class ReactiveTableViewController: UIViewController {
// ...
  
  var cars: Variable<[CarViewModel]> = Variable((UIApplication.sharedApplication().delegate as! AppDelegate).cars)
  let disposeBag = DisposeBag()
 
// ...
// }

Để gán chiều cao cho cell tôi dùng lếnh sau:

// class ReactiveTableViewController: UIViewController {
// ...
 
  override func viewDidLoad() {
    super.viewDidLoad()
 
    tableView.estimatedRowHeight = 80 // cells will be 80pt high
 }
 
// ...
// }

Vậy reacnative là ở đâu?

Các bạn thêm đoạn mã sau vào viewDidLoad

// class ReactiveTableViewController: UIViewController {
// ...
//   override func viewDidLoad() {
//     ..
 
    cars.asObservable().bindTo(tableView.rx_itemsWithCellIdentifier("CarCell", cellType: CarTableViewCell.self)) { (index, carViewModel: CarViewModel, cell) in
      cell.carViewModel = carViewModel
    }.addDisposableTo(disposeBag)
 
    // Is this the real life? Is this just fantasy?
 
//     ..
//   }
// ...
// }

Nhưng bây giờ khi bạn chạy thì nó vẫn chưa đúng đâu vậy chúng ta cần tìm hiểu cách hiển thị dữ liệu lên Cell.

Reactive tableview Cell

Hiện tại class CarTableViewCell sẽ như sau:

import UIKit
import RxSwift
 
class CarTableViewCell: UITableViewCell {
 
  @IBOutlet weak var carPhotoView: UIImageView!
  @IBOutlet weak var carTitleLabel: UILabel!
  @IBOutlet weak var carPowerLabel: UILabel!
 
  let disposeBag = DisposeBag()
  var carViewModel: CarViewModel?
}

Ở trên khi chúng ta gán giá trị cho carViewModel vào một cell, nhưng nó lại không hiển thị dư liệu, để cho nó hiển thị dữ liệu chúng ta sẽ thêm đoạn mã sau:

// class CarTableViewCell: UITableViewCell {
// ...
 
  var carViewModel: CarViewModel? {
    didSet {
      guard let cvm = carViewModel else {
        return
      }
      
      cvm.titleText.bindTo(carTitleLabel.rx_text).addDisposableTo(self.disposeBag)
      cvm.horsepowerText.bindTo(carPowerLabel.rx_text).addDisposableTo(self.disposeBag)
    }
  }
 
// ...
// }

Nếu bạn còn nhớ những gì chúng ta thay đổi CarViewModel, chúng ta đã đã lien kết titleText và horseText với Car model vậy bất kể lúc nào nó thay đổi, thì những thuộc tính này sẽ kich hoạt một signal, và như thường lệ chúng ta cần thêm disposeBag ở cuối.

Khi chạy thì nó sẽ như sau:

Rõ ràng là ảnh của Car chưa hiển thị. Ở phần đầu chúng ta sử dụng async để lấy dữ liệu NSData(contentsOfURL:). Nhưng hiện tại chúng ta đa code reactive và chúng ta có thể làm nó tốt hơn như sau:

// var carViewModel: CarViewModel? {
// ...
 
  guard let photoURL = cvm.photoURL else {
    return
  }
 
  NSURLSession.sharedSession().rx_data(NSURLRequest(URL: photoURL)).subscribeNext({ (data) in
    dispatch_async(dispatch_get_main_queue(), {
      self.carPhotoView.image = UIImage(data: data)
      self.carPhotoView.setNeedsLayout()
    })
  }).addDisposableTo(self.disposeBag)
 
// ...
// }

Bây giờ chạy project thì sẽ nhận được kết quả như sau:

CarViewController

Bây giờ chúng ta sẽ tạo view để hiển thị Car có tên CarViewController và subclass là CarViewController.

Ở CarViewController chúng ta thêm 3 text fields, vào nó đươc kết nối với ReactiveTableViewController với một segue có tên "showCar".

Tiếp theo chúng ta ánh xạ các text fields này như các phần trên.

Chúng ta cần một biến để chứa giá trị CarViewModel và một dispose bag:

import RxSwift // Don't forget this
 
// class CarViewController: UIViewController {
// ...
 
  var carViewModel: CarViewModel?
  let disposeBag = DisposeBag()
 
// ...
// }

Tiếp theo chúng ta sẽ viết các đoạn mã để hiển thị và cập nhật dữ liệu vào viewDidLoad. Nếu bạn nếu như cách code cũ bạn sẽ cần gọi đến delegate của UITextField và kiểm tra nếu người dùng thay đổi thì sẽ cập nhật lên giao diện nhưng với MVVM như sau:

override func viewDidLoad() {
  super.viewDidLoad()
  
  // We're using guard to make sure we've got a carViewModel assigned
  guard let carViewModel = carViewModel else {
    return
  }   
 
  // Assigning carViewModel's properties to our three text fields
  carViewModel.makeText.bindTo(makeField.rx_text).addDisposableTo(disposeBag)
  carViewModel.modelText.bindTo(modelField.rx_text).addDisposableTo(disposeBag)
  carViewModel.kilowattText.bindTo(kilowattField.rx_text).addDisposableTo(disposeBag)
 
  // Binding whatever the input is in our three text fields to our carViewModel's properties
  makeField.rx_text.bindTo(carViewModel.makeText).addDisposableTo(disposeBag)
  modelField.rx_text.bindTo(carViewModel.modelText).addDisposableTo(disposeBag)
  kilowattField.rx_text.filter({ (string) -> Bool in
   // Validate we are only passing integer or empty strings (which result in 0 HP)
   return Int(string) != nil || string.isEmpty
  }).bindTo(carViewModel.kilowattText).addDisposableTo(disposeBag)
 
  // Assigning the titleText to our View Controller title
  carViewModel.titleText.subscribeNext { (title) in
    self.navigationItem.title = title
  }.addDisposableTo(disposeBag)
}

Tiếp theo để bắt sự kiện khi người dùng chọn 1 cell nó sẽ gọi đến Segue với tên là “showCar” ta làm như sau:

// class ReactiveTableViewController: UIViewController {
// ...
//   override func viewDidLoad() {
//   ...
 
      tableView.rx_itemSelected.subscribeNext { (indexPath) in
        self.performSegueWithIdentifier("showCar", sender: indexPath)
        self.tableView.deselectRowAtIndexPath(indexPath, animated: true)
      }.addDisposableTo(disposeBag)
 
//   ...
//   }
// ...
// }

Tôi sẽ giải thích gắn gọn như sau: Bất kỳ khi nào một tiem được chọn(rxItemSlected), thì subscribeNext sẽ được thực thi và chúng ta sẽ gọi đến segue “showCar” và deselect row đó đi(đừng quên deselect row).

Nếu bạn chạy project thì CarViewController vẫn được hiển thị nhưng các text fields lại không có dữ liệu, nó có nghĩa là chúng ta vẫn chưa chuyền carViewModel đã được chọn sang.

Vậy ta làm như sau:

// class ReactiveTableViewController: UIViewController {
// ...
 
  override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    guard let indexPath = sender as? NSIndexPath, carVC = segue.destinationViewController as? CarViewController else {
      return
    }
    carVC.carViewModel = cars.value[indexPath.row]
  }
 
// ...
// }

Ở đây tôi sử dụng guard để chắc chắn là view đich đến tồn tại và sender là indexPath. Và truyền carViewModel từ ReactiveTableViewController sang CarViewController, các bạn có thể chạy và xem kết quả.

Tóm lại với ví dụ nhỏ này tôi nghĩ các bạn đã phần nào nắm được về mô hình MVVM, cảm ơn các bạn đã đọc.

Nguồn bài viết: Link

Tham gia ngay khoá học lập trình iOS, hình thức học tập rất linh hoạt cho bạn lựa chọn và sẽ có mức học phí khác nhau tuỳ theo bạn chọn học Online, Offline hoặc FlipLearning(Kết hợp giữa Online và Offline). Ngoài ra bạn có thể tham gia thực tập toàn thời gian tại Techmaster để rút ngắn thời gian học và tăng cơ hội việc làm.