Bài này được mình dịch từ đây.

Image credit: Stanford University CS193P, Fall-2010 Image credit: Stanford University CS193P, Fall-2010

Nếu bạn là một iOS developer hoặc là developer nói chung, bạn chắn chắn sẽ phải giải quyết vấn đề này trong hầu hết dự án: Làm thế nào để truyền data từ Model đến Controller

Bài viết này giả sử, bạn sử dụng mô hình MVC hoặc MVVM trong project của bạn. Nếu code của bạn xử lý các việc như: requesting, receiving, passing data, updating UI đều nằm trong file UIViewController duy nhất, bạn nên áp dụng một trong các mô hình kiến trúc iOS (iOS Architecture Patterns) trước đã.

Tôi sẽ mô tả 3 cách cơ bản để truyền data trở lại cho Controller của bạn:

  1. Sử dụng Callbacks
  2. Sử dụng Delegation
  3. Sử dụng Notifications

Chúng ta sẽ đi qua lần lượt khái niệm của 3 cách trên, sau đó là ví dụ step-by-step. Đến cuối bài này bạn sẽ có thể chọn cách nào phù hợp nhất cho dự án của bạn.

Bắt đầu, chúng ta sẽ tạo 1 project cơ bản có ViewControllerDataModel. Tại bước này, không quan trọng về nguồn data của bạn. Nó có thể là JSON file hay image nằm trên thư mục của app, CoreData hoặc một phản hồi HTTP. Trong mọi trường hợp, một khi bạn nhận data trong DataModel, bạn cần làm cách nào đó để truyền data này sang ViewController.

Vì vậy, bạn tạo ra 2 class: ViewControllerDataModel.

swift
1
2
3
4
5

class ViewController: UIViewController {
}
class DataModel {
}

Phần 1. Dùng Callback như là Completion Handler

Cách này rất dễ để cài đặt. Đầu tiên, chúng ta cần tạo ra 1 method requestData, nó nhận tham số là 1 completion (a block)

swift
1
2
3
4
5
6

class DataModel {
    func requestData(completion: ((_ data: String) -> Void)) {

    }
}

Completion ở đây là 1 method, nó nhận tham số là string và trả về kiểu Void

Bên trong requestData, chúng ta chạy code để yêu cầu data từ một nguồn nào đó.

swift
1
2
3
4
5
6
7

class DataModel {
    func requestData(completion: ((_ data: String) -> Void)) {
       // the data was received and parsed to String
         let data = "Data from wherever"
    }
}

Tất cả những gì chúng ta cần làm bây giờ là gọi method completion với tham số là data vừa mới nhận được.

swift
1
2
3
4
5
6
7
8

class DataModel {
   func requestData(completion: ((data: String) -> Void)) {
      // the data was received and parsed to String
      let data = "Data from wherever"
      completion(data)
   }
}

Bước tiếp theo là tạo ra một instance của DataModel trong class ViewController và gọi method requestData. Trong completion, chúng ta gọi một private method useData:

swift
1
2
3
4
5
6
7
8
9
10
11
12
13

class ViewController: UIViewController {
   private let dataModel = DataModel()
   override func viewDidLoad() {
      super.viewDidLoad()
      dataModel.requestData { [weak self] (data: String) in
            self?.useData(data: data)
      }
   }
   private func useData(data: String) {
       print(data)
   }
}

Lưu ý rằng chúng ta sử dụng self như là weak variable bên trong closure.

Và bây giờ bạn có data trong ViewController, trong khi tất cả code liên quan data đều nằm ở class DataModel. Nếu bạn build và run project, bạn sẽ thấy data được in ra trong log.


Phần 1.5. Sử dụng Callback như là một class property

Một cách khác cũng sử dụng callback để giao tiếp với ViewController là tạo ra một callback như một DataModel property.

swift
1
2
3
4

class DataModel {
      var onDataUpdate: ((_ data: String) -> Void)?
}

Bây giờ, bên trong method dataRequest, thay vì sử dụng completion handler, chúng ta có thể sử dụng callback này:

swift
1
2
3
4
5
6
7

func dataRequest() {
   // the data was received and parsed to String
      let data = "Data from wherever"

      onDataUpdate?(data)
}

Để sử dụng callback này trong class ViewController, chúng ta chỉ cần gán method cho nó (sử dụng weak self):

swift
1
2
3
4
5
6
7
8
9
10
11

class ViewController: UIViewController {
   private let dataModel = DataModel()
   override func viewDidLoad() {
      super.viewDidLoad()
      dataModel.onDataUpdate = { [weak self] (data: String) in
          self?.useData(data: data)
      }
      dataModel.requestData()
   }
}

Bạn có thể tạo ra nhiều callback property (onDataUpdate, onHTTPError, …). Tất cả callback được sử dụng tùy ý, nếu bạn không cần xử lý gì trên onHTTPError, đơn giản là bạn đừng sử dụng callback này. Đây là những điểm mạnh so với cách sử dụng trước đó.


Phần 2. Delegation

Delegation là cách phổ biến nhất để giao tiếp giữa DataModelViewController.

swift
1
2
3
4

protocol DataModelDelegate: class {
    func didRecieveDataUpdate(data: String)
}

Từ khóa class ở trên định nghĩa rằng protocol này chỉ nhận kiểu class (không phải kiểu struct hay enum). Điều này quan trọng nếu chúng ta muốn sử dụng một weak reference đến delegate. Chúng ta cần chắc rằng chúng ta không tạo ra một retain cycle giữa delegate và đối tượng được delegating, vì vậy chúng ta sử dụng weak reference đến delegate.

Bây giờ bạn cần tạo weak delegate trong DataModel:

swift
1
2

weak var delegate: DataModelDelegate?

Để gọi delegate, chúng ta sử dụng cách tương tự như cách callback:

swift
1
2
3
4
5
6
7
8
9

class DataModel {
      weak var delegate: DataModelDelegate?
      func requestData() {
         // the data was received and parsed to String
         let data = “Data from wherever”
         delegate?.didRecieveDataUpdate(data: data)
      }
}

Tạo ra một instance của DataModel trong ViewController, gán delegate của DataModel cho ViewController, và gọi method requestData:

swift
1
2
3
4
5
6
7
8
9

class ViewController: UIViewController {
      private let dataModel = DataModel()
      override func viewDidLoad() {
         super.viewDidLoad()
         dataModel.delegate = self
         dataModel.requestData()
      }
}

Bước cuối cùng, tạo một extension của ViewController, nó kế thừa protocol DataModelDelegate và sử dụng delegate method didRecieveDataUpdate.

swift
1
2
3
4
5
6

extension ViewController: DataModelDelegate {
      func didRecieveDataUpdate(data: String) {
         print(data)
      }
}

So sánh với cách dùng callback, mô hình delegation dễ dàng tái sử dụng hơn trên các ứng dụng: bạn có thể tạo một class cơ bản và nó tuân theo protocol. Tuy nhiên, delegation là khó khăn hơn khi thực hiện: bạn cần tạo một protocol, thiết lập các method trong protocol, tạo ra delegate property, gán delegate cho ViewController, và làm cho ViewController phù hợp với protocol. Ngoài ra, mặc định là delegate phải thực hiện mọi method trong protocol.

Nếu bạn muốn method trong protocol là tùy chọn, protocol của bạn phải là một @objc protocol.

Một lần nữa, build và run project bạn sẽ thấy data được in ra trong log.


Phần 3. Notification

Trong khi 2 cách trên được sử dụng rất phổ biến, cách notification này thì không rõ ràng.

Dưới đây là một trong những hoàn cảnh cụ thể, nơi mà bạn có thể sử dụng notification để giao tiếp giữa DataModelViewController.

Ví dụ: nếu bạn cần lấy nhiều hình ảnh của user được lưu trữ cục bộ và sử dụng chúng trong nhiều ViewController, sử dụng delegation sẽ đòi hỏi mọi ViewController phải tuân theo protocol.

Trong trường hợp này, sử dụng callback hoặc delegation cũng được, nhưng dùng notification sẽ thanh lịch hơn.

Đầu tiên, chúng ta sửa đổi DataModel và tạo nó thành một singleton class.

swift
1
2
3
4
5

class DataModel {
   static var sharedInstance = DataModel()
   private init() { }
}

Tiếp theo, chúng ta thêm biến cục bộ cho DataModel, nó sẽ lưu data của bạn:

swift
1
2
3
4
5
6
7

class DataModel {
   static var sharedInstance = DataModel()
   private init() { }

   private (set) var data: String?
}

Chúng ta sử dụng private(set) access modifier, vì chúng ta muốn property này chỉ đọc. Cách duy nhất để thay đổi property này là thông qua method requestData trong DataModel.

Cuối cùng, chúng ta thực hiện method requestData như trước đó:

swift
1
2
3
4
5
6
7
8
9
10

class DataModel {
   static var sharedInstance = DataModel()
   private init() { }

   private (set) var data: String?
   func requestData() {

   }
}

Một khi chúng ta nhận được data trong requestData, chúng ta lưu nó trong biến cục bộ data:

swift
1
2
3
4
5

func requestData() {
   // the data was received and parsed to String
   self.data = “Data from wherever”
}

Sau khi chúng ta cập nhật data, chúng ta muốn gửi một notification. Cách tốt nhất để làm điều này là sử dụng một property observer. Thêm property observer didSet vào biến data:

swift
1
2
3
4
5
6

private (set) var data: String? {
   didSet {

   }
}

Trước khi chúng ta gửi một notification, hãy tạo ra một cái tên ý nghĩa cho nó. Chúng ta sẽ tạo một string literal bên ngoài của class DataModel:

swift
1
2

let dataModelDidUpdateNotification = “dataModelDidUpdateNotification”

Bây giờ chúng ta đã sẵn sàng để gửi một notification:

swift
1
2
3
4
5
6
7

private (set) var data: String? {
   didSet {
      NotificationCenter.default.post(name:
NSNotification.Name(rawValue: dataModelDidUpdateNotification), object: nil)
   }
}

Dưới đây là những gì xảy ra phía sau code này: property observer (giống như tên của nó) sẽ quan sát bất kỳ sự thay đổi nào trong variable. Khi những thay đổi xuất hiện, chúng ta sẽ gửi một notification. Bây giờ chúng ta chỉ cần thêm một listener ở mọi nơi mà ViewController sử dụng data này.

swift
1
2
3
4
5
6
7

class ViewController: UIViewController {
     override func viewDidLoad() {
         super.viewDidLoad()
         NotificationCenter.default.addObserver(self, selector: #selector(getDataUpdate), name: NSNotification.Name(rawValue: dataModelDidUpdateNotification), object: nil)
     }
}

Bây giờ các observer sẽ lắng nghe bất kỳ thông tin cập nhật nào trong DataModel và gọi method getDataUpdate cho mọi sự thay đổi. Hãy thực hiện method này:

swift
1
2
3
4
5
6

@objc private func getDataUpdate() {
      if let data = DataModel.sharedInstance.data {
         print(data)
      }
}

Trong method này chúng ta đọc property DataModel.sharedInstance.data. Lưu ý rằng chúng ta không tạo ra một instance cục bộ của DataModel ở đây. Bởi vì, DataModel là một class singleton, chúng ta có thể truy cập methodproperty của nó bằng cách sử dụng sharedInstance.

So sánh với CallbackDelegation, thì Notification không thực sự truyền data từ DataModel đến ViewController: thay vì nó thông báo cho mọi người rằng data mới đã sẵn sàng. Thay vì đến mỗi ViewController và nói “Này, đây là data mới mà mày yêu cầu”, nó chỉ ngồi ở nhà và nói kiểu như: “Này mọi người, tôi có data mới rồi, hãy tới mà lấy nó về”.

Khi bạn làm việc với các notification, bạn nên nhớ 1 điều: bạn cần phải loại bỏ observer khi bạn không cần nó để lắng nghe các notification nữa. Nói cách khác, chúng ta cần chắc rằng NotificationCenter không quản lý observer mà bạn không sử dụng nữa. Để làm điều này, chúng ta cần loại bỏ các observer khi ViewController deallocated.

swift
1
2
3
4

deinit {
      NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: dataModelDidUpdateNotification), object: self)
}

Trong một số tình huống, bạn không muốn một observer lắng nghe notification nếu ViewController này vẫn còn trong Navigation Stack nhưng mà không thấy được. Ví dụ, khi bạn trình diễn ViewController thứ 2 đè lên ViewController thứ 1, cập nhật data cho cái nằm phía dưới là lãng phí tài nguyên. Trong trường hợp này, bạn có thể thêm observerviewWillAppear và loại bỏ nó ở viewWillDisappear. Điều này sẽ đảm bảo rằng ViewController của bạn chỉ lắng nghe notification khi nó hiển thị trên màn hình.

Bước cuối cùng là để gọi method requestData, sử dụng sharedInstance của DataModel:

swift
1
2
3
4
5
6
7
8

class View Controller: UIViewController {
   override func viewDidLoad() {
        super.viewDidLoad()
         NotificationCenter.default.addObserver(self, selector: #selector(getDataUpdate), name: NSNotification.Name(rawValue: dataModelDidUpdateNotification), object: nil)
        DataModel.sharedInstance.requestData()
    }
}

Build và run project kết quả sẽ giống như các lần chạy trước.

Đây là 3 cách cơ bản tôi sử dụng khi truyền data trong project.

Bạn có sử dụng bất kỳ kỹ thuật nào khác để truyền data? Hãy để lại nhận xét/câu hỏi/ý kiến/lời khuyên bên dưới.