RxSwift 实战操做【注册登陆】

前言

看了前面的文章,相信不少同窗还不知道RxSwift该怎么使用,这篇文件将带领你们一块儿写一个 注册登陆(ps:本例子采用MVVM)的例子进行实战。本篇文章是基于RxSwift3.0写的,采用的是Carthage第三方管理工具导入的RxSwift3.0,关于Carthage的安装和使用,请参考Carthage的安装和使用html

最终效果

效果图

下载Demo点我git

前提准备

首先请你们新建一个swift工程,而后把RxSwift引入到项目中,而后可以编译成功就行。github

而后咱们来分析下各个界面的需求:数据库

注册界面需求:

  • 输入用户名必须大于等于6个字符,否则密码不能输入;
  • 密码必须大于等于6个字符,否则重复密码不能输入;
  • 重复密码和密码必须同样, 不能注册按钮不能点击;
  • 点击注册按钮,提示注册成功或者注册失败;
  • 注册成功会写进本地的plist文件,而后输入用户名会检测该用户名是否已注册

登陆界面需求:

  • 点击输入用户名,检测是否已存在,若是存在,户名可用,不然提示用户名不存在;
  • 输入密码,点击登陆,若是密码错则提示密码错误,不然进入列表界面,提示登陆成功。

列表界面需求:

  • 输入联系人的首字母进行筛选

好了,分析完上面的需求以后,是时候展现真正的技术了,let's go。swift

注册界面

你们如今storyboard中创建出下面这个样子的界面(ps:添加约束不在本篇范围内):
图1api

建立对应的文件

而后创建一个对应的控制器RegisterViewController类,另外建立一个RegisterViewModel.swift,将RegisterViewControllerstoryboard中的控制器关联,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类,最好不要继承NSObjectSwift中推荐尽可能使用原生类。咱们考虑到当文本框内容变化的时候,咱们须要把文本框的内容当作参数传递进来进行处理,判断是否符合咱们的要求,而后返回处理结果,也就是状态。基于此,咱们建立一个Protocol.swift文件,建立一个enum用于表示咱们处理结果,因此,咱们在Protocol.swift文件中添加以下代码:闭包

enum Result {
    case ok(message:String)
    case empty
    case failed(message:String)
}

username处理

先写出总结:其实就是两个流的传递过程。
UI操做 -> ViewModel -> 改变数据
数据改变 -> ViewModel -> UI刷新app

回到咱们ServiceValidationService类中,写一个检测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.orEmptyRxCocoa库中的东西,它把TextFiledtext变成了一个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)
  • ViewModelusername处理结果usernameUseable绑定到nameLabel显示文案上,根据不一样的结果显示不一样的文案;
  • ViewModelusername处理结果usernameUseable绑定到pwdTextField,根据不一样的结果判断是否能够输入。

关于上面的validationResultinputEnabled是须要咱们本身去定制的,这就用到了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结果,进行它的texttextColor显示
  • 咱们对UITextField进行了UIBingObserver,根据result结果,对它的isEnabled进行设置。

写到这里,咱们暂停一下,运行一下项目看下程序的运行状况,试着去输入username尝试一下效果,是否是很激动??

password处理

有了上面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()中初始化passwordUseablerePasswordUseable

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就是注册按钮是否可用的输出,这个其实关系到usernamepassword
  • registerResult就只最后注册结果了.

咱们在init()函数中初始化registerButtonEnabledregisterResult,在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的处理,把usernamepasswordrePassword的处理结果绑定到一块儿,返回一个总的结果流,这是个Bool值的流。
  • 咱们先将usernamepassword组合,获得一个元素是它俩组合的元祖的流。
  • 而后对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中添加登陆界面,以下,当点击登陆的时候,就跳转到登陆界面。

图2

建立一个LoginViewController.swiftLoginViewModel.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: "密码错误"))
    }
}
  • 判断用户名是否可用,若是本地plist文件中有这个用户名,就表示可使用这个用户名登陆,用户名可用;
  • 登陆方法,若是用户名和密码都正确的话,就登陆成功,不然就密码错误;

而后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元组,包括usernameDriver序列,passwordDriver序列,还有登陆按钮点击的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中添加一个控制器,布局以下图:
图3

而后创建对应的ListViewController.swiftListViewModel.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,将数据绑定到tableViewitems元素,这是RxCocoatableView的一个扩展方法。咱们能够点进去看看,一共有三个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的时候使用这个,它有两个参数,一个是循环利用的cellidentifier,一个cell的类型。后面会返回的是一个闭包,在闭包里对cell进行设置。方法用起来比较简单,就是有点难理解。

ok,到此为止,此次实战也算结束了。运行你的项目看看吧。

致谢

若是发现文章有错误的地方,欢迎指出,谢谢!!

相关文章
相关标签/搜索