前面两篇关于RxSwift的文章都是一些概念,我估计大伙看了一定是迷迷糊糊的,还是不知道RxSwift怎么使用,那么这里俺就带领大伙一起去做一个Demo,去实战一下RxSwift,大伙耐心写完,理解透彻以后,保证大伙能掌握到RxSwift基本核心用法。
掌握了这篇内容,再回头看下前面两篇文章,保证你会豁然开朗某些概念,对RxSwift掌握的更加深入。 这篇文章没有对线程过分强调,请求都会在当前线程完成,计划会在下篇文章中讲解对线程的区别 。
Demo地址是 这里 咱们这个Demo选用了最万能的登录注册功能,先来看下Demo的一些基本演示,并未包含所有细节,
好了差不多就先搞这么些功能吧?什么你居然认为内容很多?放心东西很简单的,慢慢地参考着写呗,每天搞定一个界面就中了,技术妥妥的提升!
demo是使用的纯MVVM模式,因为RxSwift就是为MVVM而生。不懂MVVM的童鞋请看MVVM模式快速入门 ,我默认大家对MVVM有大致的了解。
另外demo使用了carthage引入的RxSwift和RxCocoa,当然你也可以使用cocoapods引入这些东西。具体怎么引入请大家看github介绍吧。
首先请大家建立一个新的Swift项目,然后把RxSwift和RxCocoa引入到项目中。为什么要引入RxCocoa?RxCocoa是对cocoa进行的Rx扩展,他已经包含了一个我们需要使用到的observable流,比如button的tap事件,已经帮我们包装成了一个observable流。一般做iOS开发的要使用到RxSwift都要用到RxCocoa的,这两个是相辅相成的。所有在所有的ViewController和ViewModel文件中引入这两个文件
import RxCocoa import RxSwift
这个界面我们主要学习使用Obserable和Subject的使用
大家先在storyboard上面建立好这个样子的界面
然后建立对应的RegisterViewController,他看起来应该是下面这样子
classRegisterViewController:UIViewController{ @IBOutlet weak var usernameTextField: UITextField! @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var passwordTextField: UITextField! @IBOutlet weak var passwordLabel: UILabel! @IBOutlet weak var repeatPasswordTextField: UITextField! @IBOutlet weak var repeatPasswordLabel: UILabel! @IBOutlet weak var registerButton: UIButton! @IBOutlet weak var loginButton: UIBarButtonItem! override funcviewDidLoad() { super.viewDidLoad() } }
另外建立一个RegisterViewModel.swift文件,一个Protocol.swift文件,一个Service.swift文件
我们先写那个比较好呢?我比较习惯先写Service,我们就先写Service吧,service文件主要负责一些网路请求,和一些数据的访问操作。然后供ViewModel去使用。
首先我们在Service文件中创建ValidationService类,最好不要继承NSObject,Swift中推荐尽量使用原生类。我们需要考虑当文本框里面内容改变的时候,我们需要把传来的username进行处理,判断是否符合我们的条件,然后返回处理结果,也就是状态。我们在protocol.swift文件中使用一个枚举表示我们处理结果:所以我们在 protocol.swift 文件中添加下面内容
// 表示我们的一些请求的结果 enumResult{ case ok(message: String) case empty case failed(message: String) }
username的处理我加了一个已存在的判断,效果如下
好了回到我们的 ValidationService 类中,我们写处理username的方法。他应该看起来是下面这样子。
classValidationService{ static let instance = ValidationService() private init() {} let minCharactersCount = 6 //这里面我们返回一个Observable对象,因为我们这个请求过程需要被监听。 funcvalidateUsername(_username: String) -> Observable<Result> { if username.characters.count == 0 {//当字符等于0的时候什么都不做 return .just(.empty) } if username.characters.count < minCharactersCount {//当字符小于6的时候返回failed return .just(.failed(message: "号码长度至少6个字符")) } if usernameValid(username) {//检测本地数据库中是否已经存在这个名字 return .just(.failed(message: "账户已存在")) } return .just(.ok(message: "用户名可用")) } // 从本地数据库中检测用户名是否已经存在 funcusernameValid(_username: String) -> Bool { let filePath = NSHomeDirectory() + "/Documents/users.plist" let userDic = NSDictionary(contentsOfFile: filePath) let usernameArray = userDic!.allKeys as NSArray if usernameArray.contains(username) { return true } else { return false } } }
下面开始写我们的 RegisterViewModel.swift ,我们声明一个username他是一个Variable的对象,为什么是一个Variable对象呢?因为username既是一个observable也是一个observer,所以我们声明为他为一个Variable对象。我们对username进行处理的应该有一个结,结果应该是需要界面去监听来改变界面,因为处理的结果不需要是一个observer,所以我们把它声明为一个Observable
classRegisterViewModel{ //input: let username = Variable<String>("") //初始值为"" // output: let usernameUsable: Observable<Result> init() { } }
然后我们在写我们的 RegisterViewController.swift 文件,viewDidLoad()看起来应该是下面这个样子
let disposeBag = DisposeBag() override funcviewDidLoad() { super.viewDidLoad() let viewModel = RegisterViewModel() usernameTextField.rx.text.orEmpty .bindTo(viewModel.username) .addDisposableTo(disposeBag) }
usernameTextField.rx.text.orEmpty
是RxCocoa库中的东西,他把TextFiled的text变成了一个Observable,后面的orEmpty我们可以Command点进去看下,他会把String?过滤nil帮我们变为String类型。 好了回到我们的 RegisterViewModel 类中,我们添加下面的代码
init() { let service = ValidationService.instance usernameUsable = username.asObservable() .flatMapLatest{ username in return service.validateUsername(username) .observeOn(MainScheduler.instance) .catchErrorJustReturn(.failed(message: "username检测出错")) } .shareReplay(1) }
因为username是Variable类型,既可以当observer也可以当observable,viewModel中我们把它当成obserable,然后对里面的元素进行监听和处理,这里面我们使用了flatMap,因为我们需要返回一个新的序列,也就是返回处理结果,因为涉及到数据库操作或者网络请求(当然是模拟的网络请求),所以这个序列需要我们去监听,这种情况我们使用flatMap(具体请参考)
后面使用.shareReplay(1)是因为我们要保证无论多少个Observer来监听我们这个序列,username的处理代码我们只执行一次,这一次请求结果供多有的observer去使用。
下面我们在 RegisterViewController 中处理我们的username请求结果。我们在ViewDidLoad中添加下列代码
viewModel.usernameUsable .bindTo(usernameLabel.rx.validationResult) .addDisposableTo(disposeBag) viewModel.usernameUsable .bindTo(passwordTextField.rx.inputEnabled) .addDisposableTo(disposeBag)
上面的validationResult,inputEnabled需要我们自己去定制,这就又用到了 RxSwift入坑解读-那些难以理解的细节 文章中的UIBindingObserver了,我们需要创建自己的监听者。具体大家可以好好参考这篇文章。
所以我们在 protocol.swift
文件中添加下列代码
extensionResult{ var isValid: Bool { switch self { case .ok: return true default: return false } } } extensionResult{ var textColor: UIColor { switch self { case .ok: return UIColor(red: 138.0 / 255.0, green: 221.0 / 255.0, blue: 109.0 / 255.0, alpha: 1.0) case .empty: return UIColor.black case .failed: return UIColor.red } } } extensionReactivewhereBase:UILabel{ var validationResult: UIBindingObserver<Base, Result> { return UIBindingObserver(UIElement: base) { label, result in label.textColor = result.textColor label.text = result.description } } } extensionReactivewhereBase:UITextField{ var inputEnabled: UIBindingObserver<Base, Result> { return UIBindingObserver(UIElement: base) { textFiled, result in textFiled.isEnabled = result.isValid } } }
好了到了这里我们就可以运行项目来看下程序的运行情况,试着去输入username尝试一下吧。激动了不?
总结一下这个过程:
输入文本框->ViewModel中username进行监听->然后username调用service进行处理->usernameUsable对处理结果进行监听->提示lable对usernameUsable进行监听。
哈哈,这就是响应式编程,看起来是不是一路的监听啊
有了上面的理解,对password的处理我们就轻车熟路了。很简单了,有些概念我就不需要解释太多了。
我们现在 Service 中添加对password的处理方法
funcvalidatePassword(_password: String) -> Result { if password.characters.count == 0 { return .empty } if password.characters.count < minCharactersCount { return .failed(message: "密码长度至少6个字符") } return .ok(message: "密码可用") } funcvalidateRepeatedPassword(_password: String, repeatedPasswordword: String) -> Result { if repeatedPasswordword.characters.count == 0 { return .empty } if repeatedPasswordword == password { return .ok(message: "密码可用") } return .failed(message: "两次密码不一样") }
然后我们在 RegisterViewModel.swift 文件中,添加需要的observale
//input: let password = Variable<String>("") let repeatPassword = Variable<String>("") //output: let passwordUsable: Observable<Result> //密码是否可用 let repeatPasswordUsable: Observable<Result> //密码确定是否正确
然后我们在 RegisterViewController 中,添加passwordTextField绑定
passwordTextField.rx.text.orEmpty .bindTo(viewModel.password) .addDisposableTo(disposeBag) repeatPasswordTextField.rx.text.orEmpty .bindTo(viewModel.repeatPassword) .addDisposableTo(disposeBag)
然后再转移到 RegisterViewModel 中,处理我们的password的输入
passwordUsable = password.asObservable() .map { password in return service.validatePassword(password) } .shareReplay(1) repeatPasswordUsable = Observable.combineLatest(password.asObservable(), repeatPassword.asObservable()) { return service.validateRepeatedPassword($0, repeatedPasswordword: $1) } .shareReplay(1)
下面转移到 RegisterViewController 中对ViewModel中的output进行处理
viewModel.passwordUsable .bindTo(passwordLabel.rx.validationResult) .addDisposableTo(disposeBag) viewModel.passwordUsable .bindTo(repeatPasswordTextField.rx.inputEnabled) .addDisposableTo(disposeBag) viewModel.repeatPasswordUsable .bindTo(repeatPasswordLabel.rx.validationResult) .addDisposableTo(disposeBag)
呼呼!好了运行下程序看看吧,输入password和确定密码感受下吧。
下面我们来这个界面的最后一个button处理吧,比较简单,贴贴代码解释解释,轻轻松松裸
首先我们写service里面的注册方法
funcregister(_username: String, password: String) -> Observable<Result> { let userDic = [username: password] let filePath = NSHomeDirectory() + "/Documents/users.plist" if (userDic as NSDictionary).write(toFile: filePath, atomically: true) { return .just(.ok(message: "注册成功")) } return .just(.failed(message: "注册失败")) }
然后我们来到 RegisterViewModel 文件中,添加需要的input和output
//input: let registerTaps = PublishSubject<Void>() // output: let registerButtonEnabled: Observable<Bool> let registerResult: Observable<Result>
进入 RegisterViewController 中,我们添加输入绑定
registerButton.rx.tap .bindTo(viewModel.registerTaps) .addDisposableTo(disposeBag)
然后回到 RegisterViewModel 文件中,对button的点击输入进行处理,我们添加下面这些代码,然后我一点点解释
registerButtonEnabled = Observable.combineLatest(usernameUsable, passwordUsable, repeatPasswordUsable) { (username, password, repeatPassword) in username.isValid && password.isValid && repeatPassword.isValid } .distinctUntilChanged() .shareReplay(1) let usernameAndPassword = Observable.combineLatest(username.asObservable(), password.asObservable()) { ($0, $1) } registerResult = registerTaps.asObservable().withLatestFrom(usernameAndPassword) .flatMapLatest { (username, password) in return service.register(username, password: password) .observeOn(MainScheduler.instance) .catchErrorJustReturn(.failed(message: "注册出错")) } .shareReplay(1)
然后回到 RegisterViewController 文件中,我们对ViewModel的output进行处理,你需要添加以下代码
viewModel.registerButtonEnabled .subscribe(onNext: { [unowned self] valid in self.registerButton.isEnabled = valid self.registerButton.alpha = valid ? 1.0 : 0.5 }) .addDisposableTo(disposeBag) viewModel.registerResult .subscribe(onNext: { [unowned self] result in switch result { case let .ok(message): self.showAlert(message: message) case .empty: self.showAlert(message: "") case let .failed(message): self.showAlert(message: message) } }) .addDisposableTo(disposeBag)
funcshowAlert(message: String) { let action = UIAlertAction(title: "确定", style: .default, handler: nil) let alertViewController = UIAlertController(title: nil, message: message, preferredStyle: .alert) alertViewController.addAction(action) present(alertViewController, animated: true, completion: nil) }
注册界面终于OK了,希望大家写完好好思考一下流程哦
这里面我们主要学习使用Driver的使用
其实Driver和Observable的使用结构是一样的只是Driver和Observable有点区别,Driver是RxSwift专门针对UI操作,而Observable是一个通用的东西,他们的区别可以参考我的p上一篇文章中的解析
首先我们在StoryBoard添加登录界面,如下,当点击登录的时候,跳转到我们的登录界面
我们仍然建立 LoginViewController.swift 和 LoginViewModel.swift 文件。
有了上面注册功能的一些代码工具,我们这边讲解就会比较轻松一点了。
首先在 service 中ValidationService添加下面的代码
funcloginUsernameValid(_username: String) -> Observable<Result> { if username.characters.count == 0 { return .just(.empty) } if usernameValid(username) { return .just(.ok(message: "用户名可用")) } return .just(.failed(message: "用户名不存在")) } funclogin(_username: String, password: String) -> Observable<Result> { let filePath = NSHomeDirectory() + "/Documents/users.plist" let userDic = NSDictionary(contentsOfFile: filePath) if let userPass = userDic?.object(forKey: username) as? String { if userPass == password { return .just(.ok(message: "登录成功")) } } return .just(.failed(message: "密码错误")) }
然后是 LoginViewModel.swift ,我们写成下面这样子
classLoginViewModel{ // output: let usernameUsable: Driver<Result> let loginButtonEnabled: Driver<Bool> let loginResult: Driver<Result> init(input: (username: Driver<String>, password: Driver<String>, loginTaps: Driver<Void>), service: ValidationService) { usernameUsable = input.username .flatMapLatest { username in return service.loginUsernameValid(username) .asDriver(onErrorJustReturn: .failed(message: "连接server失败")) } let usernameAndPassword = Driver.combineLatest(input.username, input.password) { ($0, $1) } loginResult = input.loginTaps.withLatestFrom(usernameAndPassword) .flatMapLatest { (username, password) in return service.login(username, password: password) .asDriver(onErrorJustReturn: .failed(message: "连接server失败")) } loginButtonEnabled = input.password .map { $0.characters.count > 0 } .asDriver() } }
下面我们在 LoginViewController.swift 中添加下列代码
override funcviewDidLoad() { super.viewDidLoad() viewModel = LoginViewModel(input: (username: usernameTextField.rx.text.orEmpty.asDriver(), password: passwordTextField.rx.text.orEmpty.asDriver(), loginTaps: loginButton.rx.tap.asDriver()), service: ValidationService.instance) viewModel.usernameUsable .drive(usernameLabel.rx.validationResult) .addDisposableTo(disposeBag) viewModel.loginButtonEnabled .drive(onNext: { [unowned self] valid in self.loginButton.isEnabled = valid self.loginButton.alpha = valid ? 1 : 0.5 }) .addDisposableTo(disposeBag) viewModel.loginResult .drive(onNext: { [unowned self] result in switch result { case let .ok(message): self.performSegue(withIdentifier: "container", sender: self) self.showAlert(message: message) case .empty: self.showAlert(message: "") case let .failed(message): self.showAlert(message: message) } }) .addDisposableTo(disposeBag) }
哈哈,我们已经完成了两个界面的编写了,相信你对RxSwift已经有了一个全新的认识了吧。
下面是列表界面,限于篇幅的原因,这里我只写展现,具体的搜索功能和其他tableView的展现技术,我会在下篇文章中进行分享。
在StoryBoard文件中新建列表界面
然后建立相应的ContainerViewController.swift和ContainerViewModel.swift文件。和Hero.swift文件。将所需的图片资源导入,下载Demo,Demo中有。
首先编写我们 Hero 类
classHero:NSObject{ var name: String var desc: String var icon: String init(name: String, desc: String, icon: String) { self.name = name self.desc = desc self.icon = icon } }
然后我们在 Service 文件中添加一个新的Service类,或者在原来的类中添加方法
class SearchService { static let shareInstance = SearchService() private init() {} func getHeros() -> Observable<[Hero]> { let herosString = Bundle.main.path(forResource: "heros", ofType: "plist") let herosArray = NSArray(contentsOfFile: herosString!) as! Array<[String: String]> var heros = [Hero]() for heroDic in herosArray { let hero = Hero(name: heroDic["name"]!, desc: heroDic["intro"]!, icon: heroDic["icon"]!) heros.append(hero) } return Observable.just(heros) .observeOn(MainScheduler.instance) } }
然后看下我们的 ContainerViewModel
classContainerViewModel{ // output: var models: Driver<[Hero]> init(withSearchText searchText: Observable<String>, service: SearchService) { models = searchText .debug() .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) .flatMap { text in return service.getHeros(withName: text) }.asDriver(onErrorJustReturn: []) } }
我们的 ContainerViewController 类就很简单了
override funcviewDidLoad() { super.viewDidLoad() let viewModel = ContainerViewModel(withSearchText: searchBarText, service: SearchService.shareInstance) viewModel.models .drive(tableView.rx.items(cellIdentifier: "cell", cellType: UITableViewCell.self)) { (row, element, cell) in cell.textLabel?.text = element.name cell.detailTextLabel?.text = element.desc cell.imageView?.image = UIImage(named: element.icon) } .addDisposableTo(disposeBag) }
我们点进去可以看下,一共有三个items方法,并且上面都有些使用方法,我们使用的这个是
public func items<S : Sequence, Cell : UITableViewCell, O : ObservableType where O.E == S>(cellIdentifier: String, cellType: Cell.Type = default) -> (O) -> (@escaping (Int, S.Iterator.Element, Cell) -> Swift.Void) -> Disposable
这是一个科里化的方法,不带section的时候使用这个,他有两个参数,一个是循环利用的cell的identifier,一个cell的类型。后面会返回的是一个闭包,在闭包里对cell进行设置。方法用起来比较简单,就是有点难理解。
好了现在运行你的代码吧,骚年开始在RxSwift的海洋中自由泳吧
小伙伴们如果感觉文章可以,可以关注博主博客
小伙伴们也可以关注博主微博,探索博主内心世界
如要转载请注明出处。