函數語言程式設計
本文介紹了函式響應式程式設計(FRP)以及 RxSwift 的一些內容, 源自公司內部的一次分享.
不變狀態(immutable state)與沒有副作用(lack of side effects)
通常,一個函式儘量不要修改外部的一些變數。 函式的返回值有唯一性。
行動式思維 VS 採用宣告式思維
- (MTFilterInfoModel *)filterInfoByFilterID:(NSInteger)filterID
ofTheme:(NSString *)themeNumber {
if (themeNumber) {
NSArray <MTFilterInfoModel *> *filterModels = [self filterInfosByThemeNumber:themeNumber];
for (MTFilterInfoModel *filter in filterModels) {
if (filter.filterID == filterID) {
return filter;
}
}
}
return nil;
}
複製程式碼
vs
let filters: [MTFilterInfoModel] = filterModels.filter { filter in
return filter.filterID == filterID
}
複製程式碼
Array的filter函式可以接收一個閉包Closure型別的引數。
對陣列中的每個元素都執行一遍該Closure,根據Closure的返回值決定是否將該元素作為符合條件的元素放入查詢結果(也是一個Array)中。
Objective-C中可以使用enumerateObjectsUsingBlock。
*** 注重Action VS 注重Result ***
first class function, closure
func myFilter(filter: MTFilterInfoModel) -> Bool {
return filter.filterID == "5008"
}
let filters: [MTFilterInfoModel] = filterModels.filter(myFilter)
複製程式碼
OC中的 blocks 或 enumeratexxx 也可以做到。
在Swift中使用高階函式(map,reduce,filter等)。避免使用loop或enumeratexxx
Swift的高階函式使得其比Objective-C更適於函數語言程式設計。
柯里化
就是把一個函式的多個引數分解成多個函式,然後把函式多層封裝起來,每層函式都返回一個函式去接收下一個引數。
即:用函式生成另一個函式
“Swift 裡可以將方法進行柯里化 (Currying),也就是把接受多個引數的方法變換成接受第一個引數的方法,並且返回接受餘下的引數並且返回結果的新方法。”
// currying
func greaterThan(_ comparer: Int) -> (Int) -> Bool {
return { $0 > comparer }
}
let isGreaterThan10 = greaterThan(10);
print(isGreaterThan10(2))
print(isGreaterThan10(20))
複製程式碼
參考資料
函式式Swift - 王巍
非同步程式設計
OC中的鏈式程式碼
Masonry的寫法:
如B+中的StillCameraViewController:
[circleLoadingView mas_makeConstraints:^ (MASConstraintMaker *maker) {
maker.leading.equalTo(thumbBottom).with.offset(30);
maker.top.equalTo(thumbBottom);
maker.width.equalTo(thumbBottom);
maker.height.equalTo(thumbBottom);
}];
複製程式碼
原理:
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
- (MASConstraint * (^)(CGFloat))offset {
return ^id(CGFloat offset){
self.offset = offset;
return self;
};
}
複製程式碼
自己實現一個:
@implementation Person
- (Person * (^)(NSString *))named {
return ^id(NSString *name) {
self.name = name;
return self;
};
}
- (Person * (^)(NSInteger))withAge {
return ^id(NSInteger age) {
self.age = age;
return self;
};
}
- (Person * (^)(NSString *))liveIn {
return ^id(NSString *city) {
self.city = city;
return self;
};
}
@end
複製程式碼
使用如下:
Person *p = [[Person alloc] init];
[p.named(@"MyName").withAge(18).liveIn(@"Xiamen") doSomething];
複製程式碼
請求指定網路圖片
登入API -> 判斷token值 -> 請求API獲取真實的JSON資料 -> 解析得到圖片URL -> 請求圖片 -> 填充UIImageView
[self request:api_login success:^{
if (isTokenCorrect) {
[self request:api_json success:^(NSData *data) {
NSDictionary *json = [self parse:data];
NSString *imgURL = json[@"thumbnail"];
[SDWebImageHelper requestImg:imgURL success:^(UIImage *image, NSError *error) {
runInMainQueue {
self.imageView.image = image;
}
}];
}];
}
}];
複製程式碼
非同步程式碼,執行緒切換。
可以使用類似 Promise 的方式解決非同步程式碼問題。
狀態更新
Target-Action
Delegate
KVO
Notification
Blocks
以上是Objective-C中的幾種狀態更新方式。
響應式程式設計
Reactive programming is programming with asynchronous data streams.
響應式程式設計與以上的幾種狀態更新方式不同,關鍵在於 *** 將非同步可觀察序列物件模型化 *** 。
命令式編碼-Pull,響應式程式設計-Push。
Push的內容即為非同步資料流。
而函數語言程式設計可以非常方便地對資料流進行合併、建立、過濾、加工等操作,因此與響應式程式設計結合比較合適。
RxSwift
*** Function programming + Reactive programming + Swift -> RxSwift ***
Why
rx
btnClose.rx.tap
複製程式碼
自己構造一個類似的
struct MT<Base> {
let base: Base
init(_ base: Base) {
self.base = base
}
}
protocol MTProtocol {
associatedtype CompatibleType
var mt: MT<CompatibleType> { get set }
}
extension MTProtocol {
var mt: MT<Self> {
get {
return MT(self)
}
set {
}
}
}
extension NSObject: MTProtocol {}
extension MT where Base: UIViewController {
var size: CGSize {
get {
return base.view.frame.size
}
}
}
複製程式碼
使用如下:
print(viewController.mt.size)
複製程式碼
使用樣例 1
RxSwift Workflow
這裡引用limboy部落格中的一張圖:
簡單的計算介面
number1與number2為兩個UITextField
// 將兩個Observable繫結在一起,構成一個Observable
Observable.combineLatest(number1.rx.text, number2.rx.text) { (num1, num2) -> Int in
if let num1 = num1, num1 != "", let num2 = num2, num2 != "" {
return Int(num1)! + Int(num2)!
} else {
return 0
}
}
// Observable傳送的訊息為Int,不能與result.rx.text繫結,所以需使用map進行對映
.map { $0.description }
// Obsever為result.rx.text
.bindTo(result.rx.text)
.addDisposableTo(CS_DisposeBag)
複製程式碼
註冊登入介面
// 宣告Observable,可觀察物件
// username的text沒有太多參考意義,因此使用map來加工,得到是否可用的訊息
let userValidation = textFieldUsername.rx.text.orEmpty
// map的引數是一個closure,接收element
.map { (user) -> Bool in
let length = user.characters.count
return length >= minUsernameLength && length <= maxUsernameLength
}
.shareReplay(1)
let passwdValidataion = textFieldPasswd.rx.text.orEmpty
.map{ (passwd) -> Bool in
let length = passwd.characters.count
return length >= minUsernameLength && length <= maxUsernameLength
}
.shareReplay(1)
// 宣告Observable
// 組合兩個Observable
let loginValidation = Observable.combineLatest(userValidation, passwdValidataion) {
$0 && $1
}
.shareReplay(1)
// bind,即將Observable與Observer繫結,最終也會呼叫subscribe
// 此處是將isEnabled視為一個Observer,接收userValidation的訊息,做出響應
// 所以Observable傳送的訊息與Observer能接收的訊息要對應起來(此處是Bool)
userValidation
.bindTo(textFieldPasswd.rx.isEnabled)
.addDisposableTo(CS_DisposeBag)
userValidation
.bindTo(lbUsernameInfo.rx.isHidden)
.addDisposableTo(CS_DisposeBag)
passwdValidataion
.bindTo(lbPasswdInfo.rx.isHidden)
.addDisposableTo(CS_DisposeBag)
loginValidation
.bindTo(btnLogin.rx.isEnabled)
.addDisposableTo(CS_DisposeBag)
複製程式碼
使用樣例 2
監控UIScrollView的scroll操作。
通常:UIScrollViewDelegate
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
// scrollView 1:
//
// scrollView 2:
//
// scrollView 3:
//
}
複製程式碼
通過RxSwift:
根本:scrollView的contentOffset在變化
tableView.rx.contentOffset
.map { $0.y }
.subscribe(onNext: { (contentOffset) in
if contentOffset >= -UIApplication.shared.statusBarFrame.height / 2 {
UIApplication.shared.statusBarStyle = .lightContent
} else {
UIApplication.shared.statusBarStyle = .default
}
})
.addDisposableTo(CS_DisposeBag)
複製程式碼
使用樣例 3
對於UITextField, UISearchController,UIButton等等,常見的使用步驟如下:
init
setup Delegate,or addTargetxxx
Delegate callback
而使用RxSwift,則可以做到 *** 高聚合,低耦合 ***
btnClose.rx.tap
.subscribe(onNext: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.dismiss(animated: true, completion: nil)
})
.addDisposableTo(CS_DisposeBag)
複製程式碼
Rx基本概念
釋出-訂閱
Observable:
可觀察物件,可組合。(發射資料)
next新的事件資料,complete事件序列的結束,error異常導致結束
所以next可以多次呼叫,而complete只有最後一次。
/// Type that can be converted to observable sequence (`Observer<E>`).
public protocol ObservableConvertibleType {
/// Type of elements in sequence.
associatedtype E
/// Converts `self` to `Observable` sequence.
///
/// - returns: Observable sequence that represents `self`.
func asObservable() -> Observable<E>
}
複製程式碼
此外,還有 *** create,just,of,from *** 等一系列函式
from: Converts an array to an observable sequence.
Observer
對Observable發射的資料或資料序列做出響應,做出特定的操作。
/// Supports push-style iteration over an observable sequence.
public protocol ObserverType {
/// The type of elements in sequence that observer can observe.
associatedtype E
/// Notify observer about sequence event.
///
/// - parameter event: Event that occured.
func on(_ event: Event<E>)
}
複製程式碼
subscribe
訂閱事件。
對事件序列中的事件,如next,complete,error進行響應,
extension ObservableType {
/**
Subscribes an element handler, an error handler, a completion handler and disposed handler to an observable sequence.
- parameter onNext: Action to invoke for each element in the observable sequence.
- parameter onError: Action to invoke upon errored termination of the observable sequence.
- parameter onCompleted: Action to invoke upon graceful termination of the observable sequence.
- parameter onDisposed: Action to invoke upon any type of termination of sequence (if the sequence has
gracefully completed, errored, or if the generation is cancelled by disposing subscription).
- returns: Subscription object used to unsubscribe from the observable sequence.
*/
public func subscribe(onNext: ((E) -> Void)? = nil, onError: ((Swift.Error) -> Void)? = nil, onCompleted: (() -> Void)? = nil, onDisposed: (() -> Void)? = nil)
-> Disposable {
xxx
}
}
複製程式碼
所以:
*** Rx的關鍵在於Observer訂閱Observable,Observable將資料push給Observer,Observer自己做出對應的響應。 ***
map
進行資料對映
extension ObservableType {
/**
Projects each element of an observable sequence into a new form.
- seealso: [map operator on reactivex.io](http://reactivex.io/documentation/operators/map.html)
- parameter transform: A transform function to apply to each source element.
- returns: An observable sequence whose elements are the result of invoking the transform function on each element of source.
*/
public func map<R>(_ transform: @escaping (Self.E) throws -> R) -> RxSwift.Observable<R>
複製程式碼
bindTo
extension ObservableType {
/**
Creates new subscription and sends elements to variable.
In case error occurs in debug mode, `fatalError` will be raised.
In case error occurs in release mode, `error` will be logged.
- parameter variable: Target variable for sequence elements.
- returns: Disposable object that can be used to unsubscribe the observer.
*/
public func bindTo(_ variable: RxSwift.Variable<Self.E>) -> Disposable
}
複製程式碼
Disposable
定義了釋放資源的統一行為。
DisposeBag: 訂閱會有Disposable,自動銷燬相關的訂閱。可簡單類似autorelease機制
BahaviorSubject 與 PublishSubject
建立一個可新增新元素的Observable,讓訂閱物件能夠接收包含初始值與新值的事件。
BahaviorSubject代表了一個隨時間推移而更新的值,包含初始值。
let s = BehaviorSubject(value: "hello")
// s.onNext("hello again") // 會替換到hello訊息
s.subscribe { // 不區分訂閱事件,所以列印 next(hello)
print($0)
}
// s.subscribe(onNext: { // 僅區分訂閱事件,所以列印next事件接收的資料 hello
// print($0)
// })
.addDisposableTo(CS_DisposeBag)
s.onNext("world") // 傳送下一個事件
s.onNext("!")
s.onCompleted()
s.onNext("??") // completed之後即不能響應了
複製程式碼
PublishSubject與BehaviorSubject類似,
但PublishSubject不需要初始值,且不會將最後一個值傳送給Observer。
struct Person {
let name = PublishSubject<String>()
let age = PublishSubject<Int>()
}
let person = Person()
person.name.onNext("none")
person.age.onNext(0)
Observable.combineLatest(person.name, person.age) {
"\($0) \($1)"
}
.debug()
.subscribe {
print($0)
}
.addDisposableTo(CS_DisposeBag)
person.name.onNext("none again") // 該none again資料不會傳送
person.name.onNext("chris")
person.age.onNext(18)
person.name.onNext("ada")
複製程式碼
使用了combineLatest,則會等待需要combine的資料都準備好了才會傳送。
可以通過combineLatest來直觀感受。
使用PublishSubject的log如下:
2017-06-22 16:46:51.160: AppDelegate.swift:186 (basicRx()) -> subscribed
2017-06-22 16:46:51.161: AppDelegate.swift:186 (basicRx()) -> Event next(chris 18)
next(chris 18)
2017-06-22 16:46:51.162: AppDelegate.swift:186 (basicRx()) -> Event next(ada 18)
next(ada 18)
2017-06-22 16:46:51.162: AppDelegate.swift:165 (basicRx()) -> Event completed
completed
2017-06-22 16:46:51.162: AppDelegate.swift:165 (basicRx()) -> isDisposed
複製程式碼
operations
可以通過combineLatest來直觀感受。
除了combine,還可以使用concat,merge,zip等到操作。
zip需要兩個元素都有新值才會傳送資料。
let personZip = Person()
// zip需要兩個元素都有新值才會傳送
Observable.zip(personZip.name, personZip.age) {
"\($0) \($1)"
}
.subscribe {
print($0)
}
.addDisposableTo(CS_DisposeBag)
personZip.name.onNext("zip none") // 不會單獨傳送
personZip.name.onNext("zip chris")// 放入序列中,等待age
personZip.age.onNext(18) // 結合zip none一起傳送
personZip.name.onNext("zip ada") // 永遠不會傳送,在其之前已經有zip chris
personZip.age.onNext(20) // 結合zip chris一起傳送
personZip.name.onCompleted()
personZip.age.onCompleted()
複製程式碼
列印的log如下:
next(zip none 18)
next(zip chris 20)
completed
2017-06-23 13:50:48.234: AppDelegate.swift:172 (basicRx()) -> Event completed
completed
2017-06-23 13:50:48.235: AppDelegate.swift:172 (basicRx()) -> isDisposed
複製程式碼
zip的場景要好好體會下,為何會是這兩個輸出。
可以通過zip來直觀感受。
Variable
Variable基於BahaviorSubject封裝的類,通過asObservable()保留出其內部的BahaviorSubject的可觀察序列。
表示一個可監聽的資料結構,可以監聽資料變化,或者將其他值繫結到變數。
Variable不會發生任何錯誤事件,即將被銷燬處理的時候,會自動傳送一個completed事件。因此有些使用Variable
let v = Variable<String>("hello")
v.asObservable()
.debug()
.distinctUntilChanged() // 消除連續重複的資料
.subscribe {
print($0)
}
.addDisposableTo(CS_DisposeBag)
v.value = "world"
v.value = "world" // 不會對重複的"world"做出響應
v.value = "!"
複製程式碼
列印log如下,可以看出其傳送的可觀察序列:
2017-06-22 16:22:57.208: AppDelegate.swift:162 (basicRx()) -> subscribed
2017-06-22 16:22:57.211: AppDelegate.swift:162 (basicRx()) -> Event next(hello)
next(hello)
2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> Event next(world)
next(world)
2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> Event next(world)
2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> Event next(!)
next(!)
2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> Event completed
completed
2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> isDisposed
複製程式碼
其他
如 *** Subject, BehaviorSubject, Driver 等等 ***
RxSwift學習之旅 - Observable 和 Driver
Demos
Login
*** RxSwift 與 RxCocoa ***
兩個Observable進行combine操作:
let loginValidation = Observable.combineLatest(userValidation, passwdValidataion) {
$0 && $1
}
.shareReplay(1)
複製程式碼
構建UITableView
RxDataSources
對UITableView, UICollectionView的dataSource進行Rx的封裝。
設定資料來源
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, User>>()
複製程式碼
宣告configureCell
dataSource.configureCell = xxx
複製程式碼
bind即可
準備一個Observable<[SectionModel<String, User>]>,然後與tableView進行相關bind即可
userViewModel.getUsers()
.bindTo(tableView.rx.items(dataSource: dataSource))
.addDisposableTo(CS_DisposeBag)
複製程式碼
點選操作
tableView.rx
.modelSelected(User.self)
.subscribe(onNext: { user in
print(user)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let loginVC = storyboard.instantiateViewController(withIdentifier: "LoginViewController")
self.present(loginVC, animated: true, completion: nil)
})
.addDisposableTo(CS_DisposeBag)
複製程式碼
使用RxSwift來構建UICollectionView的步驟類似。
SearchBar
ViewModel如下:
struct Repo {
let name: String
let url: String
}
class SearchBarViewModel {
let searchText = Variable<String>("")
let CS_DisposeBag = DisposeBag()
lazy var repos: Driver<[Repo]> = {
return self.searchText.asObservable()
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest { (user) -> Observable<[Repo]> in
if user.isEmpty {
return Observable.just([])
}
return self.searchRepos(user: user)
}
.asDriver(onErrorJustReturn: [])
}()
func searchRepos(user: String) -> Observable<[Repo]> {
guard let url = URL(string: "https://api.github.com/users/\(user)/repos") else {
return Observable.just([])
}
return URLSession.shared.rx.json(url: url)
.retry(3)
.debug()
.map {
var repos = [Repo]()
if let items = $0 as? [[String: Any]] {
items.forEach {
guard let name = $0["name"] as? String,
let url = $0["url"] as? String
else { return }
repos.append(Repo(name: name, url: url))
}
}
return repos
}
}
}
複製程式碼
ViewController中的程式碼如下:
var searchBarViewModel = SearchBarViewModel()
tableView.tableHeaderView = searchVC.searchBar
searchBar.rx.text.orEmpty
.bindTo(searchBarViewModel.searchText)
.addDisposableTo(CS_DisposeBag)
searchBarViewModel.repos
.drive(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, repo, cell) in
cell.textLabel?.text = repo.name
cell.detailTextLabel?.text = repo.url
}
.addDisposableTo(CS_DisposeBag)
複製程式碼
UISearchBar中的text與ViewModel中的searchText進行繫結,
而ViewModel中的searchText是Variable型別,作為Observable會在MainScheduler中輸入間隔達到0.3s只後會觸發呼叫searchRepos函式進行搜尋。
repos作為Driver,其中元素是包含Repo的陣列。Driver同樣封裝了可觀察序列,但Driver只在主執行緒執行。
所以,做資料繫結可以使用bindTo和Driver,涉及到UI的繫結可以儘量使用Driver。
在本例中,跟repos繫結的即是tableView.rx.items,即repos直接決定了tableView中的items展示內容。
對應使用URLSession進行網路請求的場景,RxSwift也提供了非常方便的使用方式。注意各個地方Observable的型別保持一致即可。
另外,注意 *** throttle *** , *** flatMapLatest *** 及 *** distinctUntilChanged *** 的用法。
使用MVVM
ViewModel
優點
資料繫結,精簡Controller,便於單元測試。
缺點
資料繫結額外消耗,除錯困難,
*** 注意input與output即可 ***
*** 難點在於如何合理的處理ViewModel與View的資料繫結問題。***
如何寫好一個ViewModel?
此處關於寫好ViewModel的建議,出自:
View不應該存在邏輯控制,只繫結展示資料而不對其做操作
struct UserViewModel {
let userName: String
let userAge: Int
let userCity: String
}
textFieldUserName.rx.text.orEmpty
.bindTo(userViewModel.userName)
// 不推薦
textFiledUserAge.rx.text.orEmpty
.map { Int($0) }
.bindTo(userViewModel.userAge)
複製程式碼
View只能通過ViewModel知道View要做什麼
userViewModel.register()
self.btnRegister.rx.tap
.bindTo(userViewModel.register)
複製程式碼
ViewModel只暴露View顯示所需要的最少資訊
如
struct UserViewModel {
let user: UserModel
}
struct UserViewModel {
let userName: String
let userAge: String
let userCity: String
}
複製程式碼
參考資料
RxSwift Reactive Programming with Swift by raywenderlich.com