2016年10月14日 星期五

iOS 三種從Model傳資料到Controller的方法

1. Using Callback
2. Using Delegation
3. Using Notification

這篇文章會探討三種方法的優缺點,在看完後你可以選擇其中一個最符合你專案的方法。


1. Callback
建立Controller 跟 DataModel
class ViewController: UIViewController {
}

class DataModel {
}

1.1 Callback as a completion handler
在 DataModel宣告一個方法requestData 
func requestData(completion: ((_ data: String) -> Void)){
    // 假資料
    let data = "Data from wherever"
        
    completion(data)
}
// completion 是一個方法,傳回值是Void跟有一個字串參數data

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)
   } 
}
在ViewController中宣告一個DataModel,requestData傳入一個定義好的useData方法,當呼叫requestData在requestData中資料相關code處理好後,執行completion,把資料傳給controller也就是useData 方法。


1.2 Callback as a class property
用另一種方法利用Property處理Callback
宣告一個屬性onDataModelUpdate
class DataModel {
      var onDataUpdate: ((_ data: String) -> Void)?
}

requestData方法中不需要參數,把completion改成剛剛的onDataModelUpdate
func requestData(){
    // 假資料
    let data = "Data from wherever"
        
    onDataUpdate?(data)
}

在ViewController中給onDataUpdate assign相應的方法
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()
   }
}
你也可以建立多個callbacks,例如onDataUpdate、onHttpError等...所有的callbacks都是optional的,假如你不需要onHttpError,你就不要assign給它任何method就好,比起第一種方法有更好的彈性。


2. Delegation
宣告一個Protocol
protocol DataModelDelegate: class {
    func didRecieveDataUpdate(data: String)
}
(特別注意到這邊的protocol用Class的原因是,之後我們要宣告delegate的變數為weak,如果沒限制為Class就不能宣告為weak,因為有可能是value type去實作這個protocol,不能宣告為weak就有可能造成retain cycle)

在DataModel 中宣告一個delegate,然後像用callback一樣
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)
      }
}

在ViewController中實作Protocol
extension ViewController: DataModelDelegate {
      func didRecieveDataUpdate(data: String) {
         print(data)
      }
}

在ViewContoller中assign 自己給Delegate,然後呼叫requestData
class ViewController: UIViewController {
      private let dataModel = DataModel()
      override func viewDidLoad() {
         super.viewDidLoad()
         dataModel.delegate = self
         dataModel.requestData()
      }
}
和Callback 方法相比,Delegation 更容易在整個App中重用,你可以建立一個base class 實作protocol delegate 避免重複的代碼。
Delegation的缺點:比較難實作,你需要創建一個protocol,宣告protocol methods,創建delegate property,assign delegate 給ViewController然後ViewController實作這個protocol,且必須實作protocol中的每個方法在ㄧ般的情況下。


3. Notification
什麼情況下會用到Notification

假設你有一個data你整個app都需要用到它,用delegation將需要每個ViewController都要實作,當然使用Callback或Delegation也可以,但使用Notification將更合適。

首先把DataModel修改為singleton class
class DataModel {
   static var sharedInstance = DataModel()
   private init() { }
}

然後宣告一個read-only local variable data
class DataModel {
   static var sharedInstance = DataModel()
   private init() { }
   
   private (set) var data: String?
}

在requestData中接到的data assign 給local variable
func requestData() {
   // the data was received and parsed to String
   self.data = "Data from wherever"
}

宣告一個notification的識別字串(outside of model class)
let dataModelDidUpdateNotification = "dataModelDidUpdateNotification"

我們想在data updated的時候發送一個通知最好的方法是用property observer 在data local variable 中增加didSet property observer,然後post a notification
private (set) var data: String? {
   didSet {
      NotificationCenter.default.post(name:  
NSNotification.Name(rawValue: dataModelDidUpdateNotification), object: nil)
   }
}

如此只要當data值改變,我們就會post a notification,現在我們只要在每個要用到data 的ViewController中增加listener
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() 
    }
}

現在我們會監聽dataModel中的data值任何改變然後呼叫getDataUpdate方法,現在我們實作getDataUpdate方法
@objc private func getDataUpdate() {
      if let data = DataModel.sharedInstance.data {
         print(data)
      }
}

跟Callback和Delegation比起來Notification事實上沒有從DataModel傳任何資料給Controller,而是像坐在家裡向Controller說:嘿~我這裡有新的data,可以進來抓囉。(有學過design pattern 觀察者模式應該很好懂)

當你處理notification時,永遠記得要remove當你不再使用時
deinit {
      NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: dataModelDidUpdateNotification), object: self)
}

舉個例,例如你在使用navigationController,如果你的一個ViewController 在 navigation stack ,但它目前是不可見的,而第二個ViewController目前是可見的,當你想update第二個ViewController,第一個ViewController如果沒remove會跟著一起update,這是浪費資源的,可以在viewWillAppear中add然後在viewWillDisapear 中remove,確保你的ViewController只有在螢幕上顯示時才會監聽。

這篇主要是參考這篇寫的,如果英文好的朋友可以直接看這篇說的比較詳細
https://medium.com/ios-os-x-development/ios-three-ways-to-pass-data-from-model-to-controller-b47cc72a4336#.a62gb92tc

沒有留言:

張貼留言