看了前面的文章,相信不少同窗还不知道RxSwift
该怎么使用,这篇文件将带领你们一块儿写一个 注册登陆(ps:本例子采用MVVM
)的例子进行实战。本篇文章是基于RxSwift3.0
写的,采用的是Carthage
第三方管理工具导入的RxSwift3.0
,关于Carthage
的安装和使用,请参考Carthage的安装和使用。html
下载Demo点我git
首先请你们新建一个swift
工程,而后把RxSwift
引入到项目中,而后可以编译成功就行。github
而后咱们来分析下各个界面的需求:数据库
好了,分析完上面的需求以后,是时候展现真正的技术了,let's go。swift
你们如今storyboard
中创建出下面这个样子的界面(ps:添加约束不在本篇范围内):
api
而后创建一个对应的控制器RegisterViewController
类,另外建立一个RegisterViewModel.swift
,将RegisterViewController
与storyboard
中的控制器关联,RegisterViewController
看起来应该是这样子的:数组
class RegisterViewController: UIViewController { @IBOutlet weak var userNameTextField: UITextField! @IBOutlet weak var nameLabel: UILabel! @IBOutlet weak var pwdTextField: UITextField! @IBOutlet weak var pwdLabel: UILabel! @IBOutlet weak var rePwdTextField: UITextField! @IBOutlet weak var rePwdLabel: UILabel! @IBOutlet weak var registButton: UIButton! @IBOutlet weak var loginButton: UIBarButtonItem! override func viewDidLoad() { super.viewDidLoad() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } }
另外,咱们建立一个Service.swift
文件。
Service
文件主要负责一些网络请求,和一些数据访问的操做。而后供ViewModel
使用,因为本次实战没有使用到网络,因此咱们只是模拟从本地plist
文件中读取用户数据。网络
首先咱们在Service
文件中建立一个ValidationService
类,最好不要继承NSObject
,Swift
中推荐尽可能使用原生类。咱们考虑到当文本框内容变化的时候,咱们须要把文本框的内容当作参数传递进来进行处理,判断是否符合咱们的要求,而后返回处理结果,也就是状态。基于此,咱们建立一个Protocol.swift
文件,建立一个enum
用于表示咱们处理结果,因此,咱们在Protocol.swift
文件中添加以下代码:闭包
enum Result { case ok(message:String) case empty case failed(message:String) }
先写出总结:其实就是两个流的传递过程。
UI操做 -> ViewModel -> 改变数据
数据改变 -> ViewModel -> UI刷新app
回到咱们Service
中ValidationService
类中,写一个检测username
的方法。它看起来应该是这个样子的:
class ValidationService { // 单例类 static let instance = ValidationService() private init(){} let minCharactersCount = 6 func validationUserName(_ name:String) -> Observable<Result> { if name.characters.count == 0 { // 当字符串为空的时候,什么也不作 return Observable.just(Result.empty) } if name.characters.count < minCharactersCount { return Observable.just(Result.failed(message: "用户名长度至少为6位")) } if checkHasUserName(name) { return Observable.just(Result.failed(message: "用户名已存在")) } return Observable.just(Result.ok(message: "用户名可用")) } func checkHasUserName(_ userName:String) -> Bool { let filePath = NSHomeDirectory() + "/Documents/users.plist" guard let userDict = NSDictionary(contentsOfFile: filePath) else { return false } let usernameArray = userDict.allKeys as NSArray return usernameArray.contains(userName) } }
接下来该处理咱们的RegisterViewModel
了,咱们声明一个username
,指定为Variable
类型,为何是一个Variable
类型?由于它既是一个Observer
,又是一个Observable
,因此咱们声明它是一个Variable
类型的对象。咱们对username
处理应该会有一个结果,这个结果应该是由界面监听来改变界面显示,所以咱们声明一个usernameUseable
表示对username
处理的一个结果,由于它是一个Observable
,因此咱们将它声明为Observable
类型的对象,因此RegisterViewModel
看起来应该是这样子的:
class RegisterViewModel { let username = Variable<String>("") let usernameUseable:Observable<Result> init() { } }
而后咱们再写RegisterViewController
,它看起来应该是这样子的:
private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() let viewModel = RegisterViewModel() userNameTextField.rx.text.orEmpty.bind(to: viewModel.username).disposed(by: disposeBag) }
userNameTextField.rx.text.orEmpty
是RxCocoa
库中的东西,它把TextFiled
的text
变成了一个Observable
,后面的orEmpty
咱们能够Command
点进去看下,它会把String?
过滤nil
帮咱们变为String
类型。bind(to:viewModel.username)
的意思是viewModel.username
做为一个observer
(观察者)观察userNameTextField
上的内容变化。disposeBag
来盛放咱们这些监听的资源。如今,回到咱们的RegisterViewModel
中,咱们添加以下代码:
init() { let service = ValidationService.instance usernameUseable = username.asObservable().flatMapLatest{ username in return service.validationUserName(username).observeOn(MainScheduler.instance).catchErrorJustReturn(.failed(message: "userName检测出错")).shareReplay(1) } }
viewModel
中,咱们把username
当作observable
(被观察者),而后对里面的元素进行处理以后发射对应的事件。下面咱们在RegisterViewController
中处理咱们的username
请求结果。咱们在ViewDidLoad
中添加下列代码:
viewModel.usernameUseable.bind(to: nameLabel.rx.validationResult).addDisposableTo(disposeBag) viewModel.usernameUseable.bind(to: pwdTextField.rx.inputEnabled).addDisposableTo(disposeBag)
ViewModel
中username
处理结果usernameUseable
绑定到nameLabel
显示文案上,根据不一样的结果显示不一样的文案;ViewModel
中username
处理结果usernameUseable
绑定到pwdTextField
,根据不一样的结果判断是否能够输入。关于上面的validationResult
和inputEnabled
是须要咱们本身去定制的,这就用到了RxSwift 系列(九) -- 那些难以理解的概念文章中的UIBindingObserver
了。
因此,咱们在Protocol.swift
文件中添加以下代码:
extension Result { var isValid:Bool { switch self { case .ok: return true default: return false } } } extension Result { 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 } } } extension Result { var description: String { switch self { case let .ok(message): return message case .empty: return "" case let .failed(message): return message } } } extension Reactive where Base: UILabel { var validationResult: UIBindingObserver<Base, Result> { return UIBindingObserver(UIElement: base) { label, result in label.textColor = result.textColor label.text = result.description } } } extension Reactive where Base: UITextField { var inputEnabled: UIBindingObserver<Base, Result> { return UIBindingObserver(UIElement: base) { textFiled, result in textFiled.isEnabled = result.isValid } } }
Result
进行了扩展,添加了isValid
属性,若是状态是ok
,这个属性就为true
,不然为false
Result
添加了一个textColor
属性,若是状态为ok
则为绿色,不然使用红色UILabel
进行了UIBingObserver
,根据result
结果,进行它的text
和textColor
显示UITextField
进行了UIBingObserver
,根据result
结果,对它的isEnabled
进行设置。写到这里,咱们暂停一下,运行一下项目看下程序的运行状况,试着去输入username
尝试一下效果,是否是很激动??
有了上面username
的理解,相信你们对password
也就熟门熟路了,所以有些细节就不作描述了。
咱们如今对Service
中添加对password
的处理:
func validationPassword(_ password:String) -> Result { if password.characters.count == 0 { return Result.empty } if password.characters.count < minCharactersCount { return .failed(message: "密码长度至少为6位") } return .ok(message: "密码可用") } func validationRePassword(_ password:String, _ rePassword: String) -> Result { if rePassword.characters.count == 0 { return .empty } if rePassword.characters.count < minCharactersCount { return .failed(message: "密码长度至少为6位") } if rePassword == password { return .ok(message: "密码可用") } return .failed(message: "两次密码不同") }
validationPassword
处理咱们输入的密码;validationRePassword
处理咱们输入的重复密码;Result
类型的值,由于咱们外面不须要对这个过程进行监听,因此没必要返回一个新的序列。在RegisterViewModel
中添加须要的observable
:
let password = Variable<String>("") let rePassword = Variable<String>("") let passwordUseable:Observable<Result> let rePasswordUseable:Observable<Result>
而后在init()
中初始化passwordUseable
和rePasswordUseable
:
passwordUseable = password.asObservable().map { passWord in return service.validationPassword(passWord) }.shareReplay(1) rePasswordUseable = Observable.combineLatest(password.asObservable(), rePassword.asObservable()) { return service.validationRePassword($0, $1) }.shareReplay(1)
回到RegisterViewController
中,添加对应的绑定:
pwdTextField.rx.text.orEmpty.bind(to: viewModel.password).disposed(by: disposeBag) rePwdTextField.rx.text.orEmpty.bind(to: viewModel.rePassword).disposed(by: disposeBag) viewModel.passwordUseable.bind(to: pwdLabel.rx.validationResult).addDisposableTo(disposeBag) viewModel.passwordUseable.bind(to: rePwdTextField.rx.inputEnabled).addDisposableTo(disposeBag) viewModel.rePasswordUseable.bind(to: rePwdLabel.rx.validationResult).addDisposableTo(disposeBag)
😁,先放轻松一下,运行程序看看,输入用户名和密码和重复密码感觉一下。
首先咱们在Service
里面添加一个注册函数:
func register(_ username:String, password:String) -> Observable<Result> { let userDict = [username: password] if (userDict as NSDictionary).write(toFile: filePath, atomically: true) { return Observable.just(Result.ok(message: "注册成功")) }else{ return Observable.just(Result.failed(message: "注册失败")) } }
我是直接把注册信息写入到本地的plist文件,写入成功就返回ok,不然就是
failed。
回到RegisterViewModel
中添加以下代码:
let registerTaps = PublishSubject<Void>() let registerButtonEnabled:Observable<Bool> let registerResult:Observable<Result>
registerTaps
咱们使用了PublishSubject
,由于不须要有初始元素,其实前面的Variable
均可以换成PublishSubject
。大伙能够试试;registerButtonEnabled
就是注册按钮是否可用的输出,这个其实关系到username
和password
;registerResult
就只最后注册结果了.咱们在init()
函数中初始化registerButtonEnabled
和registerResult
,在init()
中添加以下代码:
registerButtonEnabled = Observable.combineLatest(usernameUseable, passwordUseable, rePasswordUseable) { (username, password, repassword) in return username.isValid && password.isValid && repassword.isValid }.distinctUntilChanged().shareReplay(1) let usernameAndPwd = Observable.combineLatest(username.asObservable(), password.asObservable()){ return ($0, $1) } registerResult = registerTaps.asObservable().withLatestFrom(usernameAndPwd).flatMapLatest { (username, password) in return service.register(username, password: password).observeOn(MainScheduler.instance).catchErrorJustReturn(Result.failed(message: "注册失败")) }.shareReplay(1)
registerButtonEnabled
的处理,把username
、password
和rePassword
的处理结果绑定到一块儿,返回一个总的结果流,这是个Bool
值的流。username
和password
组合,获得一个元素是它俩组合的元祖的流。registerTaps
事件进行监听,咱们拿到每个元组进行注册行为,涉及到耗时数据库操做,咱们须要对这个过程进行监听,因此咱们使用flatMap
函数,返回一个新的流。回到RegisterViewController
中,添加按钮的绑定:
registButton.rx.tap.bind(to: viewModel.registerTaps).disposed(by: disposeBag) viewModel.registerButtonEnabled.subscribe(onNext: { [weak self](valid) in self?.registButton.isEnabled = valid self?.registButton.alpha = valid ? 1 : 0.5 }).disposed(by: disposeBag) viewModel.registerResult.subscribe(onNext: { [weak 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) } }).disposed(by: disposeBag)
func showAlert(message:String) { let action = UIAlertAction(title: "肯定", style: .default) { [weak self](_) in self?.userNameTextField.text = "" self?.pwdTextField.text = "" self?.rePwdTextField.text = "" // 这个方法是基于点击肯定让全部元素还原才抽出的,可不搭理。 self?.setupRx() } let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) alertController.addAction(action) present(alertController, animated: true, completion: nil) }
注意:上述setupRx()
是为了点击肯定以后,界面上全部的元素还原才抽出的,具体的能够查看demo
如今,运行项目,咱们已经可以正常的注册帐号了。😊
首先咱们在storyboard
中添加登陆界面,以下,当点击登陆的时候,就跳转到登陆界面。
建立一个LoginViewController.swift
和LoginViewModel.swift
文件,有了上述注册功能的讲解,相信登陆功能也很容易了。
咱们在Service.swift
中添加以下代码:
func loginUserNameValid(_ userName:String) -> Observable<Result> { if userName.characters.count == 0 { return Observable.just(Result.empty) } if checkHasUserName(userName) { return Observable.just(Result.ok(message: "用户名可用")) } return Observable.just(Result.failed(message: "用户名不存在")) } // 登陆 func login(_ username:String, password:String) -> Observable<Result> { guard let userDict = NSDictionary(contentsOfFile: filePath), let userPass = userDict.object(forKey: username) else { return Observable.just(Result.empty) } if (userPass as! String) == password { return Observable.just(Result.ok(message: "登陆成功")) }else{ return Observable.just(Result.failed(message: "密码错误")) } }
而后LoginViewModel.swift
,像这样:
class LoginViewModel { let usernameUseable:Driver<Result> let loginButtonEnabled:Driver<Bool> let loginResult:Driver<Result> init(input:(username:Driver<String>, password:Driver<String>, loginTaps:Driver<Void>), service:ValidationService) { usernameUseable = input.username.flatMapLatest { userName in return service.loginUserNameValid(userName).asDriver(onErrorJustReturn: .failed(message: "链接server失败")) } let usernameAndPass = Driver.combineLatest(input.username,input.password) { return ($0, $1) } loginResult = input.loginTaps.withLatestFrom(usernameAndPass).flatMapLatest{ (username, password) in service.login(username, password: password).asDriver(onErrorJustReturn: .failed(message: "链接server失败")) } loginButtonEnabled = input.password.map { $0.characters.count > 0 }.asDriver() } }
Driver
类型的,第一个是username
处理结果流,第二个是登陆按钮是否可用的流,第三个是登陆结果流;init
方法,看着和刚才的注册界面不同。这种写法我参考了官方文档的写法,让你们知道有这种写法。可是我并不推荐你们使用这种方式,由于若是Controller
中的元素不少的话,一个一个传过来是很可怕的。input
元组,包括username
的Driver
序列,password
的Driver
序列,还有登陆按钮点击的Driver
序列,还有Service
对象,须要Controller
传递过来,其实Controller
不该该拥有Service
对象。Driver
,咱们再也不须要shareReplay(1)
。接下来咱们在LoginViewController.swift
中写,它看来像这样子的:
override func viewDidLoad() { super.viewDidLoad() title = "登陆" let viewModel = LoginViewModel(input: (username: usernameTextField.rx.text.orEmpty.asDriver(), password: passwordTextField.rx.text.orEmpty.asDriver(), loginTaps:loginButton.rx.tap.asDriver()), service: ValidationService.instance) viewModel.usernameUseable.drive(nameLabel.rx.validationResult).disposed(by: disposeBag) viewModel.loginButtonEnabled.drive(onNext: { [weak self] (valid) in self?.loginButton.isEnabled = valid self?.loginButton.alpha = valid ? 1.0 : 0.5 }).disposed(by: disposeBag) viewModel.loginResult.drive(onNext: { [weak self](result) in switch result { case let .ok(message): self?.performSegue(withIdentifier: "showListSegue", sender: nil) self?.showAlert(message: message) case .empty: self?.showAlert(message: "") case let .failed(message): self?.showAlert(message: message) } }).disposed(by: disposeBag) }
viewModel
传入相应的Driver序列。viewModel
中的对象进行相应的监听,若是是Driver
序列,咱们这里不使用bingTo
,而是使用的Driver
,用法和bingTo
如出一辙。Deriver
的监听必定发生在主线程,因此很适合咱们更新UI的操做。因为篇幅缘由,列表界面就不作很复杂了,简单地弄了些假数据。既然作到这里了,怎么也得把它作完吧。
let's go,在storyboard
中添加一个控制器,布局以下图:
而后创建对应的ListViewController.swift
、ListViewModel.swift
文件,由于须要model
类,因此建立了一个Contact.swift
类,而后添加了contact.plist
资源文件。
首先编写咱们的Contact.swift
类,它看来像这样子:
class Contact:NSObject { var name:String var phone:String init(name:String, phone:String) { self.name = name self.phone = phone } }
而后在Service.swift
文件中,添加一个SearchService
类,它看起来像这样:
class SearchService { static let instance = SearchService(); private init(){} // 获取联系人 func getContacts() -> Observable<[Contact]> { let contactPath = Bundle.main.path(forResource: "Contact", ofType: "plist") let contactArr = NSArray(contentsOfFile: contactPath!) as! Array<[String:String]> var contacts = [Contact]() for contactDict in contactArr { let contact = Contact(name:contactDict["name"]!, phone: contactDict["phone"]!) contacts.append(contact) } return Observable.just(contacts).observeOn(MainScheduler.instance) } }
Contact
模型;Contact
数组的Observable
流。接下来更新UI的操做要在主线程中。而后看看咱们的ListViewModel.swift
,它看起来像这样:
class ListViewModel { var models:Driver<[Contact]> init(with searchText:Observable<String>, service:SearchService){ models = searchText.debug() .observeOn(ConcurrentDispatchQueueScheduler(qos: .background)) .flatMap { text in return service.getContacts(withName: text) }.asDriver(onErrorJustReturn:[]) } }
models
是一个Driver
流,由于更新tableView
是UI操做;service
去获取数据的操做应该在后台线程去运行,因此添加了observeOn
操做;flatMap
返回新的observable
流,转换成models
对应的Driver
流。注意:由于这里是根据搜索框的内容去搜索数据,所以在SearchService
中须要添加一个函数,它看起来应该是这样子的:
func getContacts(withName name: String) -> Observable<[Contact]> { if name == "" { return getContacts() } let contactPath = Bundle.main.path(forResource: "Contact", ofType: "plist") let contactArr = NSArray(contentsOfFile: contactPath!) as! Array<[String:String]> var contacts = [Contact]() for contactDict in contactArr { if contactDict["name"]!.contains(name) { let contact = Contact(name:contactDict["name"]!, phone: contactDict["phone"]!) contacts.append(contact) } } return Observable.just(contacts).observeOn(MainScheduler.instance) }
最后,咱们的ListViewController
就简单了:
var searchBarText:Observable<String> { return searchBar.rx.text.orEmpty.throttle(0.3, scheduler: MainScheduler.instance) .distinctUntilChanged() } override func viewDidLoad() { super.viewDidLoad() title = "联系人" let viewModel = ListViewModel(with: searchBarText, service: SearchService.instance) viewModel.models.drive(tableView.rx.items(cellIdentifier: "cell", cellType: UITableViewCell.self)){(row, element, cell) in cell.textLabel?.text = element.name cell.detailTextLabel?.text = element.phone }.disposed(by: disposeBag) }
发现木有,这里咱们么有使用到DataSource
,将数据绑定到tableView
的items
元素,这是RxCocoa
对tableView
的一个扩展方法。咱们能够点进去看看,一共有三个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
进行设置。方法用起来比较简单,就是有点难理解。
ok,到此为止,此次实战也算结束了。运行你的项目看看吧。
若是发现文章有错误的地方,欢迎指出,谢谢!!