Preface
前陣子花了一些時間研究了一下 RxSwift 的使用,後來也使用 RxSwift 開發了一個 Video 解碼及顯示的小項目。
然而,因為需要針對公司產品開發一個簡單上手的 Quick Start App,想了想應該可以搬出 RxSwift 來套用,跟之前 Android 版本不同的是,這次的 iOS Quick Start App,我使用了 MVVM
這套設計模式來開發。
何謂 MVVM ?
所謂 MVVM
就是 Model
+ View
+ ViewModel
的合稱,是由 MVC
的一種變型,其設計理念就是: View → ViewModel → Model
,也就是 View 引用了 ViewModel;ViewModel 引用了 Model。
為了能讓架構儘量單純化,所以 MVVM 架構不允許 View ← ViewModel ← Model
的這種使用方法。說到這邊也許會覺得奇怪,那麼如果 Model / View Model 的資料更改了,又怎麼能夠反應到 View 上呢?
這時候就可以借重這次的主題 RxSwift
來幫忙了。
RxSwift Demo
這次練習 APP 的 View 如下:
- UITextField:用來輸入目標裝置的 ID。
- UIButton:按鈕按下後將會記錄 Device ID 的值,並訪問 Web service Restful API。
- UITableView:當成功新增一筆記錄後,將會自動至 Table 中;如果使用者刪除 Table 的記錄,則會呼叫 Web service Restful API 刪除 Server 上的記錄。
View Controller
在 AppDelegate 中必須取得 Remote Notification Token,並把 Token 值放入 Global 的 deviceToken 這個 property 中。
1 | class AppDelegate: UIResponder, UIApplicationDelegate { |
在 Global 這個類別,採用 Singleton 方法實作,其中 deviceToken 這個 property 是個 Observable 物件,所以當它被指定值 (didSet) 時則會被觸發事件 (onNext) 給 Observer。
1 | class Global { |
在 ViewController 中,可以看到在 ViewDidLoad 函式中逐一將 View 中的每個控件和 ViewModel 進行綁定 (Binding),之後如果數據在 ViewModel 中有更動時,透過 RxSwift 便會自動反應在 View 上。
1 | class ViewController: UIViewController { |
ViewModel
我們把所有的程式邏輯都寫在 ViewModel 上,可以看到包含 UITextField 的輸入判斷,UIButton 的按鈕處理、Restful API 的使用等等。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94class ViewModel {
private let serverURL = "http://push.iotcplatform.com/tpns"
private let disposeBag = DisposeBag()
let list: Variable<[String]>
init(deviceToken: Observable<String>,
text: Observable<String>,
buttonTap: Observable<Void>,
tableItemRemoved: Observable<NSIndexPath>) {
// load uid from UserDefaults
if let data: [String] = NSUserDefaults.standardUserDefaults().objectForKey("UIDs") as? [String] {
list = Variable<[String]>(data)
} else {
list = Variable<[String]>([])
}
/* Send to server when retrieve device token. */
deviceToken
.filter {
$0.characters.count == 64
}
.flatMapLatest { [unowned self] token in
return self.request(self.serverURL, parameters: ["cmd": "client", "os": "ios", "appid": "com.tutk.cc.samples.tpns.ios", "udid": token, "token": token])
}
.observeOn(MainScheduler.instance)
.subscribeNext {
print($0)
}
.addDisposableTo(disposeBag)
/* Check if the device id exists and add to the list when button tapped. */
buttonTap
.flatMapLatest {
text.take(1)
}
.filter { [unowned self] s -> Bool in
return !self.list.value.contains(s)
}
.subscribeNext { [unowned self] uid in
self.list.value.append(uid)
}
.addDisposableTo(disposeBag)
/* Remove the id from list */
tableItemRemoved
.map { return $0.row }
.subscribeNext { [unowned self] idx in
self.list.value.removeAtIndex(idx)
}
.addDisposableTo(disposeBag)
/* Whenever list modified, encode the ids and send to server. */
list.asObservable()
.flatMap { [unowned self] thiz in
return Observable.combineLatest(deviceToken, self.mapsyncString(thiz)) { ($0, $1) }
}
.flatMapLatest { [unowned self] (token, mapsync) in
return self.request(self.serverURL, parameters: ["cmd": "mapsync", "appid": "com.tutk.cc.samples.tpns.ios", "udid": token, "os": "ios", "map": mapsync.base64String()])
}
.observeOn(MainScheduler.instance)
.subscribeNext {
NSUserDefaults.standardUserDefaults().setObject(self.list.value, forKey: "UIDs")
NSUserDefaults.standardUserDefaults().synchronize()
print($0)
}
.addDisposableTo(disposeBag)
}
func request(url: String, parameters: [String: String]?) -> Observable<String> {
return RxAlamofire.request(.GET, url, parameters: parameters)
.flatMapLatest {
$0
.validate(statusCode: 200 ..< 300)
.rx_string()
}
}
func mapsyncString(list: [String]) -> Observable<String> {
let array: NSMutableArray = []
for uid in list {
array.addObject(NSMutableDictionary(object: uid, forKey: "uid"))
}
let json = JSON(array)
if let s = json.rawString() {
return Observable<String>.just(s)
} else {
return Observable<String>.just("[]")
}
}
}
結論
透過 RxSwift 我們可以很輕易地把數據綁定和程式邏輯分開處理,藉由這個優勢,可以降低程式的耦合程度,大幅提高程式可維護性。
完整程式已經放在 https://github.com/cloudhsiao/ios-tpns-quickstart,有興趣的朋友可以一同研究切磋。