函数响应式编程与RxSwift

函数式编程

本文介绍了函数响应式编程(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))

参考资料

FRP iOS Learning resources.md

函数式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

是时候学习 RxSwift 了

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博客中的一张图:

RxSwift Workflow

简单的计算界面

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基本概念

reactivex.io

发布-订阅

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 等等 ***

RxSwiftStudy

RxSwift学习博客

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的建议,出自:

【漫谈】从项目实践走向RxSwift响应式函数编程

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
}

参考资料

LearnRxSwift

rx-sample-code

RxSwift Reactive Programming with Swift by raywenderlich.com

100-days-of-RxSwift

RxSwift-CN

iOS 架构模式 - 简述 MVC, MVP, MVVM 和 VIPER (译)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • 概述 RxSwift顾名思义是Swift的一种框架,您或许曾经听说过「响应式编程」(Reactive Progra...
    Mr大喵喵阅读 1,861评论 3 4
  • 前言 在之前用Objective-C语言做项目的时候,我习惯性的会利用MVVM模式去架构项目,在框架Reactiv...
    Tangentw阅读 21,175评论 32 123
  • (一)万年不变的开端 去年大三还在学校的时候就听说过ReactiveCocoa这一Github开源的响应式重量级框...
    Maru阅读 6,561评论 20 84
  • 前言RxSwift的目的是让数据/事件流和异步任务能够更方便的序列化处理,能够使用Swift进行响应式编程。 本文...
    努力奔跑的小男孩阅读 1,873评论 0 3
  • 寅春初寒下钱塘,雨润花红鸭先知。 两岸垂柳依水连,晚钟幕鼓南屏山。 烟雨朦胧独舟影,游人醉饮西湖水。 百里长堤断残...
    唯爱菲儿落叶阅读 123评论 0 0