登录rxswift_rxswift 登录 - CSDN
  • 一、前言 江湖上都在说现在就要赶紧学 swift 了,即将是 swift 的天下了。在 api 变化不大的情况下,swift 作为一门新的语言,集众家之所长,普通编码确实比 oc 要好用的多了老早就听说 MVVM 的概念及响应式函数...


    一、前言

    • 江湖上都在说现在就要赶紧学 swift 了,即将是 swift 的天下了。在 api 变化不大的情况下,swift 作为一门新的语言,集众家之所长,普通编码确实比 oc 要好用的多了
    • 老早就听说 MVVM 的概念及响应式函数式编程,微软确实厉害。自己最近没什么事,就前来入坑了

    二、学习方式

    • 参考别人写的一些博客,对于概念先有个理解,然后参考官方 example,就可以开始学习了
    • 推荐文章:

    三、自己写注册登录及 tableView 的一点理解

    • 关于观察者 和 被观察者(Observable)发出序列
      • UI 控件在 RxCocoa 下某些属性都是被观察者(Observable),都可以发出序列,常见的有
        • 控件的 text 类型是 ControlProperty<String> ,最终遵循 ObservableType协议
        • 按钮的点击 tap 类型是 ControlEvent<Void>,最终遵循 ObservableType协议
      • 对于设置 UI 控件的一些 Bool 类型的属性,如可输入,可点击,一般用 UIBindingObserver<UIElementType, Value>(遵循 ObserverType协议) 来生成观察者,对接受的数据条件进行判断是否可以输入、可点击

        // MARK: RX 扩展 计算型属性
        // textfield 根据展示验证后的结果能否输入,验证过了才能输入
        extension Reactive where Base: UITextField {
        
         var inputEnable: UIBindingObserver<Base, ValidationResult> {
            return UIBindingObserver(UIElement: base, binding: { 
                (textField, result) in
        
                textField.isEnabled = result.isValid
             })
         }
        }
      • 关于在 VM 中常用的 Subject
        • Variable、PublishSubject 是 Subject 的一种,可当观察者被 bindTo,可当序列数据源 Observable
          • Variable 它不会因为错误终止也不会正常终止, 适合做数据源,可以用于控件的 text 属性
          • PublishSubject 与普通的Subject不同,在订阅时并不立即触发订阅事件,而是允许我们在任意时刻手动调用onNext(),onError(),onCompleted来触发事件,可以用于按钮的点击
      • 关于被观察者(Observable)的一些常用的 api
        • map 不会产生新的序列
        • flatMapLatest 会产生新的序列
        • combineLatest 不会产生新的序列
    • 关于 MVVM 文件夹分类
      • 之前 MVC 的与 iOS 里的 Controller、View 一一对应,很好理解,而 MVVM 里 Controller 属于 V 了,负责处理控制器跳转和将 View 和 VM 绑定等,大部分的业务逻辑代码都在 VM 里,感觉应该是这样

      • 自始至终感觉 iOS 里的 model 这一层很轻,有时仅仅是建立了模型类而已。感觉应该是各种数据操作如数据库查询等都应该是 model 这一层的

    • 关于** 双向绑定 **
      • 首先要有一些控件,理清楚需要监听这些控件的哪些属性值
      • 然后 VM 里建立好这些属性值对应的 Subject
      • 一般控制器里生成 VM 对象,将控件的 Observable 的属性绑定到 VM 的 Subject 属性上,这样可在 VM 里监听到控件属性值的改变,此时 Subject 是 Observer,完成一次绑定
      • 在 VM 内,将 Subject 变成 Observable,生成对应 VM 可被观察者属性(用属性保存加工变换后的 Observable )。这里 Subject 是 Observable,可通过 map 、filter 等各种操作,操作的数据就是 Subject 观察到的序列,相应模块的整个业务逻辑都在此处。
      • 在控制器里,再将 VM 的可被观察者属性绑定到 UI 控件上,在此完成双向绑定
      override func viewDidLoad() {
          super.viewDidLoad()
          let regiestViewModel = RegiestViewModel()
          // 这里做绑定: UI控件 --> VM    VM -> UI控件
          // 1.UI控件 --> VM
          nameTextField.rx.text.orEmpty
              .bindTo(regiestViewModel.username)
              .addDisposableTo(disposeBag)
          pwdTextField.rx.text.orEmpty
              .bindTo(regiestViewModel.userPwd)
              .addDisposableTo(disposeBag)
          repeatPwdTextField.rx.text.orEmpty
              .bindTo(regiestViewModel.repeatPwd)
              .addDisposableTo(disposeBag)
          regiestBtn.rx.tap
              .bindTo(regiestViewModel.registerTaps)
              .addDisposableTo(disposeBag)
          // 2.VM -> UI控件
          // 显示结果的 label 上
          regiestViewModel.usernameValid
              .bindTo(nameTipLabel.rx.validResult)
              .addDisposableTo(disposeBag)
          // 绑定 密码框是否可以输入
          regiestViewModel.usernameValid
              .bindTo(pwdTextField.rx.inputEnable)
              .addDisposableTo(disposeBag)
          regiestViewModel.passwordValid
              .bindTo(pwdTipLabel.rx.validResult)
              .addDisposableTo(disposeBag)
          regiestViewModel.passwordValid
              .bindTo(repeatPwdTextField.rx.inputEnable)
              .addDisposableTo(disposeBag)
          regiestViewModel.repeatPwdValid
              .bindTo(repeatPwdTipLabel.rx.validResult)
              .addDisposableTo(disposeBag)
          // 按钮不是绑定, 按钮是 subcribe, 需要操作的
          regiestViewModel.registerButtonEnabled
              .subscribe (onNext: { [weak self]  (result) in
                  self?.regiestBtn.isEnabled = result
                  self?.regiestBtn.alpha = result ? 1 : 0.8
                  })
              .addDisposableTo(disposeBag)
          // 注册结果 : 注册成果或失败 要展示在 UI 上
          regiestViewModel.registeResult
              .subscribe(onNext:{ [weak self] result in
                  switch result {
                      case let .failed(message):
                          self?.showAlter(message: message)
                      case let .ok(message):
                          self?.showAlter(message: message)
                      case .empty:
                          self?.showAlter(message: "")
                      }
                  })
              .addDisposableTo(disposeBag)
          // 跳转到登录界面按钮的点击
          loginVcBtn.rx.tap
              .subscribe(onNext: {
                  let loginVc = LoginViewController()
                  loginVc.title = "请登录"
                  self.navigationController?.pushViewController(loginVc, animated: true)
              })
              .addDisposableTo(disposeBag)
      }
      
    • 关于 tableView
      • 这里需要 RxDataSources 这个配套的框架
      • 控制器里需要一个 dataSource

        let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, HerosItem>>()
      • 控制器里用这个 dataSource 配置 cell

        dataSource.configureCell = { (_, tableView, indexPath, item) in
            var cell = tableView.dequeueReusableCell(withIdentifier: "herosCell")
            if cell == nil {
                cell = UITableViewCell(style: .subtitle, reuseIdentifier: "herosCell")
            }
            cell!.imageView?.image = UIImage(named: item.icon)
            cell!.textLabel?.text = item.name
            cell!.detailTextLabel?.text = item.intro
            return cell!
        }
        
      • 用 VM 创建出数据 Observable,发出序列,绑定到dataSource 上,完成数据的绑定

        homeViewMode.getSearchResult()
            .bindTo(tableView.rx.items(dataSource: dataSource))
            .addDisposableTo(disposeBag)
      • 其他操作
        • tableView 的代理

          tableView.rx
          .setDelegate(self)
          .addDisposableTo(disposeBag)
        • tableView cell 的点击

          tableView.rx.itemSelected
          .map { [weak self] indexPath in
              return (indexPath, self?.dataSource[indexPath])
          }
          .subscribe(onNext: {(indexPath, item) in
              self.showAlter(item: item)
          })
          .addDisposableTo(disposeBag)
      • 感觉像一个固定的代码模式,将数据源的代码都移到 VM 里了
      • 另外如果做实时搜索的话,用双向绑定效果那是极好的,将搜索框的搜索关键字绑定到 VM 里,在用 VM 产生序列绑定到 tableView 上

    一、前言

    • 江湖上都在说现在就要赶紧学 swift 了,即将是 swift 的天下了。在 api 变化不大的情况下,swift 作为一门新的语言,集众家之所长,普通编码确实比 oc 要好用的多了
    • 老早就听说 MVVM 的概念及响应式函数式编程,微软确实厉害。自己最近没什么事,就前来入坑了

    二、学习方式

    三、自己写注册登录及 tableView 的一点理解

    • 关于观察者 和 被观察者(Observable)发出序列
      • UI 控件在 RxCocoa 下某些属性都是被观察者(Observable),都可以发出序列,常见的有
        • 控件的 text 类型是 ControlProperty<String> ,最终遵循 ObservableType协议
        • 按钮的点击 tap 类型是 ControlEvent<Void>,最终遵循 ObservableType协议
      • 对于设置 UI 控件的一些 Bool 类型的属性,如可输入,可点击,一般用 UIBindingObserver<UIElementType, Value>(遵循 ObserverType协议) 来生成观察者,对接受的数据条件进行判断是否可以输入、可点击

        // MARK: RX 扩展 计算型属性
        // textfield 根据展示验证后的结果能否输入,验证过了才能输入
        extension Reactive where Base: UITextField {
        
         var inputEnable: UIBindingObserver<Base, ValidationResult> {
            return UIBindingObserver(UIElement: base, binding: { 
                (textField, result) in
        
                textField.isEnabled = result.isValid
             })
         }
        }
      • 关于在 VM 中常用的 Subject
        • Variable、PublishSubject 是 Subject 的一种,可当观察者被 bindTo,可当序列数据源 Observable
          • Variable 它不会因为错误终止也不会正常终止, 适合做数据源,可以用于控件的 text 属性
          • PublishSubject 与普通的Subject不同,在订阅时并不立即触发订阅事件,而是允许我们在任意时刻手动调用onNext(),onError(),onCompleted来触发事件,可以用于按钮的点击
      • 关于被观察者(Observable)的一些常用的 api
        • map 不会产生新的序列
        • flatMapLatest 会产生新的序列
        • combineLatest 不会产生新的序列
    • 关于 MVVM 文件夹分类
      • 之前 MVC 的与 iOS 里的 Controller、View 一一对应,很好理解,而 MVVM 里 Controller 属于 V 了,负责处理控制器跳转和将 View 和 VM 绑定等,大部分的业务逻辑代码都在 VM 里,感觉应该是这样

      • 自始至终感觉 iOS 里的 model 这一层很轻,有时仅仅是建立了模型类而已。感觉应该是各种数据操作如数据库查询等都应该是 model 这一层的

    • 关于** 双向绑定 **
      • 首先要有一些控件,理清楚需要监听这些控件的哪些属性值
      • 然后 VM 里建立好这些属性值对应的 Subject
      • 一般控制器里生成 VM 对象,将控件的 Observable 的属性绑定到 VM 的 Subject 属性上,这样可在 VM 里监听到控件属性值的改变,此时 Subject 是 Observer,完成一次绑定
      • 在 VM 内,将 Subject 变成 Observable,生成对应 VM 可被观察者属性(用属性保存加工变换后的 Observable )。这里 Subject 是 Observable,可通过 map 、filter 等各种操作,操作的数据就是 Subject 观察到的序列,相应模块的整个业务逻辑都在此处。
      • 在控制器里,再将 VM 的可被观察者属性绑定到 UI 控件上,在此完成双向绑定
      override func viewDidLoad() {
          super.viewDidLoad()
          let regiestViewModel = RegiestViewModel()
          // 这里做绑定: UI控件 --> VM    VM -> UI控件
          // 1.UI控件 --> VM
          nameTextField.rx.text.orEmpty
              .bindTo(regiestViewModel.username)
              .addDisposableTo(disposeBag)
          pwdTextField.rx.text.orEmpty
              .bindTo(regiestViewModel.userPwd)
              .addDisposableTo(disposeBag)
          repeatPwdTextField.rx.text.orEmpty
              .bindTo(regiestViewModel.repeatPwd)
              .addDisposableTo(disposeBag)
          regiestBtn.rx.tap
              .bindTo(regiestViewModel.registerTaps)
              .addDisposableTo(disposeBag)
          // 2.VM -> UI控件
          // 显示结果的 label 上
          regiestViewModel.usernameValid
              .bindTo(nameTipLabel.rx.validResult)
              .addDisposableTo(disposeBag)
          // 绑定 密码框是否可以输入
          regiestViewModel.usernameValid
              .bindTo(pwdTextField.rx.inputEnable)
              .addDisposableTo(disposeBag)
          regiestViewModel.passwordValid
              .bindTo(pwdTipLabel.rx.validResult)
              .addDisposableTo(disposeBag)
          regiestViewModel.passwordValid
              .bindTo(repeatPwdTextField.rx.inputEnable)
              .addDisposableTo(disposeBag)
          regiestViewModel.repeatPwdValid
              .bindTo(repeatPwdTipLabel.rx.validResult)
              .addDisposableTo(disposeBag)
          // 按钮不是绑定, 按钮是 subcribe, 需要操作的
          regiestViewModel.registerButtonEnabled
              .subscribe (onNext: { [weak self]  (result) in
                  self?.regiestBtn.isEnabled = result
                  self?.regiestBtn.alpha = result ? 1 : 0.8
                  })
              .addDisposableTo(disposeBag)
          // 注册结果 : 注册成果或失败 要展示在 UI 上
          regiestViewModel.registeResult
              .subscribe(onNext:{ [weak self] result in
                  switch result {
                      case let .failed(message):
                          self?.showAlter(message: message)
                      case let .ok(message):
                          self?.showAlter(message: message)
                      case .empty:
                          self?.showAlter(message: "")
                      }
                  })
              .addDisposableTo(disposeBag)
          // 跳转到登录界面按钮的点击
          loginVcBtn.rx.tap
              .subscribe(onNext: {
                  let loginVc = LoginViewController()
                  loginVc.title = "请登录"
                  self.navigationController?.pushViewController(loginVc, animated: true)
              })
              .addDisposableTo(disposeBag)
      }
      
    • 关于 tableView
      • 这里需要 RxDataSources 这个配套的框架
      • 控制器里需要一个 dataSource

        let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, HerosItem>>()
      • 控制器里用这个 dataSource 配置 cell

        dataSource.configureCell = { (_, tableView, indexPath, item) in
            var cell = tableView.dequeueReusableCell(withIdentifier: "herosCell")
            if cell == nil {
                cell = UITableViewCell(style: .subtitle, reuseIdentifier: "herosCell")
            }
            cell!.imageView?.image = UIImage(named: item.icon)
            cell!.textLabel?.text = item.name
            cell!.detailTextLabel?.text = item.intro
            return cell!
        }
        
      • 用 VM 创建出数据 Observable,发出序列,绑定到dataSource 上,完成数据的绑定

        homeViewMode.getSearchResult()
            .bindTo(tableView.rx.items(dataSource: dataSource))
            .addDisposableTo(disposeBag)
      • 其他操作
        • tableView 的代理

          tableView.rx
          .setDelegate(self)
          .addDisposableTo(disposeBag)
        • tableView cell 的点击

          tableView.rx.itemSelected
          .map { [weak self] indexPath in
              return (indexPath, self?.dataSource[indexPath])
          }
          .subscribe(onNext: {(indexPath, item) in
              self.showAlter(item: item)
          })
          .addDisposableTo(disposeBag)
      • 感觉像一个固定的代码模式,将数据源的代码都移到 VM 里了
      • 另外如果做实时搜索的话,用双向绑定效果那是极好的,将搜索框的搜索关键字绑定到 VM 里,在用 VM 产生序列绑定到 tableView 上

    展开全文
  • 我们在使用MVC建构进行开发时,对登录页面用户名密码等...或者用textfield的代理来进行响应其实也是屎一般! 有了RxSwift,我们的响应式又多了一种可能! let usernameValid = user_nameTextField.rx.text ....

    我们在使用MVC建构进行开发时,对登录页面用户名密码等进行的处理一般是这样的,点击登录按钮判断用户框以及密码框输入的合法性,用一堆if真是屎一般!或者用textfield的代理来进行响应其实也是屎一般!

    有了RxSwift,我们的响应式又多了一种可能!

            let usernameValid = user_nameTextField.rx.text
                .map{($0?.characters.count)! >= minUsernameLength && ($0?.characters.count)! <= maxUsernameLength }  //map函数 对text进行处理
                .shareReplay(1)
            
            let passwordValid = secertTextFiled.rx.text
                .map{($0?.characters.count)! >= minPasswordLength && ($0?.characters.count)! < maxPasswordLength }  //map函数 对text进行处理
                .shareReplay(1)
            
            //
            let everythingValid = Observable.combineLatest(usernameValid, passwordValid) { (usernameValid, passwordValid) -> Bool in
                usernameValid && passwordValid
            }
            
            usernameValid
                .bindTo(usernameLabel.rx.isHidden)   //username通过验证,usernameLB警告消失
                .addDisposableTo(disposBag)
            passwordValid
                .bindTo(secertLable.rx.isHidden)
                .addDisposableTo(disposBag)
            
            usernameValid
                .bind(to: secertTextFiled.rx.isEnabled)  //username通过验证,passwordTF才可以输入
                .addDisposableTo(disposBag)
    
    
            
            everythingValid
                .bind(to: loginbtn.rx.isEnabled)   // 用户名密码都通过验证,才可以点击按钮
                .addDisposableTo(disposBag)
            
            everythingValid.bind { [weak self](isenabled) in
                if isenabled{
                    self?.loginbtn.backgroundColor = .red
                }else{
                    self?.loginbtn.backgroundColor = .gray
                }
            }.addDisposableTo(disposBag)
            
    
            loginbtn.rx.tap  //绑定button点击事件
                .bind { [weak self] in
                    self?.showAlert()
                }
                .addDisposableTo(disposBag)

    以上代码很好理解,我们用到了shareReply来共享监听,避免会创建多个序列去执行输入框的监听

    转载于:https://www.cnblogs.com/lidarui/p/8192387.html

    展开全文
  • 看了前面的文章,相信很多同学还不知道RxSwift该怎么使用,这篇文件将带领大家一起写一个 注册登录(ps:本例子采用MVVM)的例子进行实战。本篇文章是基于RxSwift3.0写的,采用的是Carthage第三方管理工具导入的...

    前言

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

    最终效果

    效果图

    下载Demo点我

    前提准备

    首先请大家新建一个swift工程,然后把RxSwift引入到项目中,然后能够编译成功就行。

    然后我们来分析下各个界面的需求:

    注册界面需求:

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

    登录界面需求:

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

    列表界面需求:

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

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

    注册界面

    大家现在storyboard中建立出下面这个样子的界面(ps:添加约束不在本篇范围内):
    图1

    创建对应的文件

    然后建立一个对应的控制器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刷新

    回到我们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)

    展开全文
  • 在学习了RxSwift官方的demo以及各种操作符后,对RxSwift会有一个大致的了解,但在实际开发过程中并不是有很多机会去使用,主要是因为使用生疏的开发技能会带来开发时间上与产品质量上的风险,为了避免”不熟悉-&...

    背景

    在学习了RxSwift官方的demo以及各种操作符后,对RxSwift会有一个大致的了解,但在实际开发过程中并不是有很多机会去使用,主要是因为使用生疏的开发技能会带来开发时间上与产品质量上的风险,为了避免”不熟悉->不敢用->用的少->不熟悉->不敢用->用的少…”的恶性循环,个人觉得一种比较好的方法是在业余时间选择一些常见的功能使用RxSwift实现一遍,一方面加深对RxSwift的理解,另一方面,在实际项目中遇到类似的业务场景时,如果打算使用RxSwift的话则不再会心中没底。

    本文选择了一个常见的注册功能作为例子,采用RxSwift+MVVM的形式去实现。文中会详细说明从需求分析到代码实现过程中的每个步骤。

    Ok, Let’s go.

    Demo 项目地址

    需求说明

    提供一个注册页面,页面中有三个输入框,分别用于输入用户名,密码,确认密码。用户在相应的输入框内输入时App需要对输入的值进行校验,校验失败时,需要在相应的输入框下方提示失败原因。
    页面中还有一个注册按钮,当用户名,密码,确认密码校验全部通过后,注册按钮启用,用户点击注册按钮
    App执行注册操作,注册成功后,则自动进行登录操作,登录成功后退出注册页面,如果注册或登录操作有任何一个失败,则给出提示。

    用户名校验逻辑
    • 长度不能小于6个字符
    • 不能包含!@#$%^&*()这些特殊字符
    • 在用户输入后停顿时长达到0.5s时才进行校验
    • 在校验中时,需要在输入框下方展示 ‘校验中…’,当校验失败时,需要在输入框下方展示失败原因
    密码校验逻辑
    • 长度不能小于6个字符
    • 不能包含!@#$%^&*()这些特殊字符
    • 如果确认密码输入框有值,则需要与确认密码保持一致
    • 在用户输入后停顿时长达到0.5s时才进行校验
    • 在校验中时,需要在输入框下方展示 ‘校验中…’,当校验失败时,需要在输入框下方展示失败原因
    确认密码校验逻辑
    • 必须与密码输入框的值相同,如果密码输入框没有内容则不进行此项校验
    • 在用户输入后停顿时长达到0.5s时才进行校验
    • 在校验中时,需要在输入框下方展示 ‘校验中…’,当校验失败时,需要在输入框下方展示失败原因

    界面UI

    MVVM 各层功能划分

    View

    • 提供注册界面UI,包含3个输入框以及注册按钮。
    • 接受用户输入和按钮点击事件并将其传递给 ViewModel
    • 处理ViewModel反馈的信息。

    ViewModel

    接收并处理View中用户的输入以及按钮点击事件,并将结果反馈给 View

    Model

    业务服务层,提供用户名校验、密码校验、注册、登录服务。

    数据流分析

    在MVVM中,ViewModel扮演着一个非常重要的角色,其作为View与Model之间的纽带,接收View传递过来的事件,然后调用Model中的服务进行处理,最后将处理结果反馈至View,View再根据反馈信息进行处理。简单点说,ViewModel实现了View与Model之间的双向绑定。在实际开发过程中,最重要的就是要分析出View中有哪些事件流需要处理,ViewModel中又会有哪些输出流需要View处理,不管是view中产生的事件还是ViewModel产生的处理结果,最终都是抽象成数据流, 下面是针对当前demo的一个数据流的分析:

    sequenceDiagram
    View->>ViewModel: '用户名' 用户输入数据流
    View->>ViewModel: '密码' 用户输入数据流
    View->>ViewModel: '确认密码' 用户输入数据流
    View->>ViewModel: '注册按钮' 用户点击事件流
    
    ViewModel->>View: '用户名'验证结果输出流
    ViewModel->>View: '密码'验证结果输出流
    ViewModel->>View: '确认密码'验证结果输出流
    ViewModel->>View: '登录'结果输出流
    ViewModel->>View: '注册按钮'可用性输出流

    ==注:上图中的’输入’与’输出’是相对于ViewModel而言的==

    开始编码

    首先,根据MVVM 各层功能划分,定义下面几个Class:
    - SignUpVC 注册页面(View)
    - SignUpVM 注册页面的 ViewModel
    - SignUpService 注册相关服务(Model)

    SignUpService: 负责提供业务服务接口,当前提供的接口如下:

    class SignUpService{
        //校验'用户名'
        static func validateUsername(username: String) -> Observable<ValidateResult>
        //校验'密码'
        static func validatePsd(username: String) -> Observable<ValidateResult>
        //执行注册操作
        static func signUp(username: String, psd: String) -> Observable<Bool>
        //执行登录操作
        static func signIn(username: String, psd: String) -> Observable<Bool>
    }

    ==注:异步操作的结果在Rx中都是可以用数据流表示的,因为异步操作的结果就和数据流中的数据一样,是不定时的产生的。只是有的异步操作只有会产生一个结果就结束,比如网络请求,而有的异步操作则会持续不断输出结果,比入网络状态监听。==

    其中,ValidateResult定义如下:

    enum ValidateFailReason{
        case emptyInput
        case other(String)
    }
    enum ValidateResult {
        case validating
        case ok
    
        case failed(ValidateFailReason)
        var isOk: Bool {
            if case ValidateResult.ok = self {
                return true
            }else{
                return false
            }
        }
    }

    **接着根据数据流向分析,我们
    SignUpVM中定义输入数据流与输出数据流:**

    class SignUpVM {
        struct Input {
            //'用户名'输入流
            let username: Observable<String>
            //'密码'输入流
            let psd: Observable<String>
            //'确认密码'输入流
            let confirmPsd: Observable<String>
            //'注册按钮点击事件'输入流
            let signUpBtnTaps: Observable<Void>
        }
        struct Output {
            //'用户名验证结果'输出流
            var usernameValidateResult: Observable<ValidateResult>!
            //'密码验证结果'输出流
            var psdValidateResult: Observable<ValidateResult>!
            //'确认密码验证结果'输出流
            var confirmPsdValidateResult: Observable<ValidateResult>!
            //'注册按钮enable设置'输出流
            var signUpEnable: Observable<Bool>!
            //'登录结果'输出流
            var signInResult: Observable<Bool>!
        }
    }

    ==注:下面代码中的outputinput分别表示struct Outputstruct Input的实例==

    这里说明一下每个输入数据流中的数据是什么:
    - input.username ‘用户名’输入流: 用户在’用户名’输入框输入的字符串
    - input.psd ‘密码’输入流: 用户在’密码’输入框输入的字符串
    - input.confirmPsd ‘确认密码’输入流: 用户在’确认密码’输入框输入的字符串
    - input.signUpBtnTaps ‘注册按钮点击事件’输入流: ‘注册按钮’点击事件

    然后我们看一下在SignUpVC中每个输入流是如何产生的,下面列出了SignUpVC中SignUpVM.Input初始化代码片段:

    ==注:SignUpVC是MVVM中的View层,负责将View中产生的事件传递给相应的ViewModel。==

    SignUpVM.Input(
                username: _usernameTf.rx.value
                                        .orEmpty
                                        .asObservable()
                                        .distinctUntilChanged()
                                        .debounce(0.5, scheduler: MainScheduler.instance),
    
                psd: _psdTf.rx.value
                              .orEmpty
                              .asObservable()
                              .distinctUntilChanged()
                              .debounce(0.5, scheduler: MainScheduler.instance),
    
                confirmPsd: _confirmPsdTf.rx.value
                                            .orEmpty
                                            .asObservable()
                                            .distinctUntilChanged()
                                            .debounce(0.5, scheduler: MainScheduler.instance),
    
                signUpBtnTaps: _signUpBtn.rx.tap.asObservable()
            ))

    代码有点长,但是很简单,我们一个个看,首先看username参数的值, 它的构成有点长,我们一段段分析:_usernameTf.rx.value 表示’用户名输入框’的值所组成的数据流,当输入框内容变化、失去焦点、初始化时其值会被数据流发射。orEmpty是为了将_usernameTf.rx.value中的nil转换空字符串,distinctUntilChanged则是为了过滤掉与上一次相同的值。debounce作用是当用户在输入框内输入字符后0.5内未再有输入,则此时输入框内的值会被数据流中发射。简单点说其作用就是为了在用户输入时降低实时校验的频率。

    psdconfirmPsdusername类似,此处就不多说了。

    signUpBtnTaps参数的值很简单:_signUpBtn.rx.tap.asObservable()表示点击事件数据流。

    接着我们分析一下SignUpVM中每个输出流是如何定义的:

    ==注:ViewModel在接收到View的数据流后,会去执行一些业务逻辑,
    产生的结果则会做为输出流再传递给View,用一个函数表达式表示就是:
    outputStream = f (inputStream)==

    • output.usernameValidateResult: ‘用户名验证结果’输出流。

    SignUpVM接收到 input.username(‘用户名’输入数据流)的数据时,SignUpVM会调用SignUpService提供的接口进行验证,验证结果作为output.usernameValidateResult输出数据流的数据,代码如下:

    output.usernameValidateResult = input.username
                .flatMapLatest { (username) -> Observable<ValidateResult> in
                    return SignUpService.validateUsername(username: username)
                }
                .share(replay: 1)

    flatMapLatest 可以简单的理解其作用为: 用’链式调用’的形式串联异步操作。这样就无需通过回调嵌套的形式处理异步操作的串联。

    图解:
    首先看下如果用 flatmap 是什么结果:
    image

    下面则是 flatmapLatest的:
    image

    由上图可见,flatMap不管校验结果什么时候返回,都会被output.usernameValidateResult数据流发射,若是flatMapLatest,output.usernameValidateResult 只会将最近一次的校验结果发射。’最近一次’是指触发校验行为时,如果先前的校验行为还未产生结果,那么先前的校验行为的结果将会被丢弃,只有此次触发的校验行为的结果才有机会被output.usernameValidateResult输出(当然,如果下次校验行为触发时,此次校验行为还未完成,那么此次校验行为的结果则同样会被丢弃,以此往复)。

    • output.psdValidateResultoutput.usernameValidateResult类似,这里就直接列出代码:
    output.psdValidateResult = input.psd
                .flatMapLatest { (psd) -> Observable<ValidateResult> in
                    return SignUpService.validatePsd(psd: psd)
                }
                .share(replay: 1)
    • confirmPsdValidateResult: ‘确认密码验证结果’输出流。
      SignUpVM会在用户输入密码确认密码时对两个密码进行比对,一致则通过校验,不一致则校验失败,代码如下:
    output.confirmPsdValidateResult = Observable<ValidateResult>
                .combineLatest(input.psd,
                               input.confirmPsd,
                               resultSelector: { (psd: String, confirmPsd: String) -> ValidateResult in
                                    if(psd.isEmpty || confirmPsd.isEmpty){
                                        return .failed(.emptyInput)
                                    }else if(psd != confirmPsd){
                                        return .failed(.other("两次密码不一致"))
                                    }else{
                                        return .ok
                                    }
                            })
            .share(replay: 1)

    因为要进行比对,所以无论此时用户是在输入密码确认密码SignUpVM都需要能够从相应的输入流(input.psdinput.confirmPsd)中获取这个两个字段当前最新的值,
    combineLatest操作符则是实现该目的的关键,该操作符会从指定的多个数据流中获取最近一次发射的数据,将这些数据传递给一个回调函数,该函数进行处理并返回一个值,这个值会被combineLatest所生成的那个数据流发射。
    combineLatest图解
    - output.signUpEnable:’注册按钮enable设置’输出流。
    用户名,密码,确认密码都通过验证后,该数据流发射’启用’信号,反之则发射’禁用’信号,代码如下:

            output.signUpEnable = Observable<Bool>
                                .combineLatest(output.usernameValidateResult,
                                               output.psdValidateResult,
                                               output.confirmPsdValidateResult,
                                               resultSelector: { (
                                                usernameValidateResult: ValidateResult,
                                                psdValidateResult: ValidateResult,
                                                confirmPsdValidateResult: ValidateResult) -> Bool in
    
                                                return usernameValidateResult.isOk
                                                    && psdValidateResult.isOk
                                                    && confirmPsdValidateResult.isOk
    
            })

    一眼看上去有点复杂,我们仔细分析一下:启用/禁用’注册’按钮是取决于用户名,密码,确认密码的验证结果的,这三个字段验的证结果数据流已经定义过了,所以此处只需要用combineLatest将三者组合,当任何一个字段有验证结果产生时,则会进行一次判断以决定启用或禁用’注册’按钮。

    • signInResult:’登录结果’输出流。
    struct UsernameAndPsd{
        let username: String
        let psd: String
    }
    
    let usernameAndPsdSeq: Observable<UsernameAndPsd>  = Observable.combineLatest(input.username, input.psd) { (username, psd) -> UsernameAndPsd in
        return UsernameAndPsd(username: username, psd: psd)
    }
    
    output.signInResult = input.signUpBtnTaps
                                .withLatestFrom(usernameAndPsdSeq)
                                .flatMapLatest {(unamePsd: UsernameAndPsd) -> Observable<(Bool,UsernameAndPsd)> in
                                    return SignUpService.signUp(username: unamePsd.username,
                                                                psd: unamePsd.psd)
                                                         .map{ (isSignSuccess) -> (Bool,UsernameAndPsd) in
                                                            return (isSignSuccess, UsernameAndPsd(username: unamePsd.username,psd: unamePsd.psd))
                                                         }
                                }.flatMapLatest{ (e: (isSignUpSuccess: Bool,unameAndPsd: UsernameAndPsd )) -> Observable<Bool> in
                                    if e.isSignUpSuccess{
                                        return SignUpService.signIn(username: e.unameAndPsd.username, psd: e.unameAndPsd.psd)
                                    }else{
                                        return Observable<Bool>.of(false)
                                    }
                                }

    恩,又是一大串,但实际上逻辑是比较清晰的,分析前先说一下UsernameAndPsd,很简单,它就是为了方便同时传递username 和 psd而做的一个封装。

    下面分析逻辑:’注册’按钮被点击后,SignUpVM通过input.signUpBtnTaps拿到点击事件,之后则是要进行’注册’操作,我们看到,’注册’的接口是需要用户名密码的,而input.signUpBtnTaps传递点击的事件并不带有任何上下文信息,所以通过input.signUpBtnTaps是无法拿到当前界面上用户输入的用户名密码,但是SignUpVM中是拥有用户名密码的输入流的,所以还是那个套路,使用combineLatest用户名密码输入流组合一下,然后每次当用户名密码的输入流产生数据时都会被发送一份到那个组合数据流中,这样,’注册’按钮点击事件发生时,我们就可以去那个组合数据流中拿用户名密码,而'拿'这个操作则是由withLatestFrom操作符实现。

    目前为止,我们完成了’注册’按钮点击事件处理以及获取用户名密码这两个步骤,接下来则是使用获取到的用户名密码进行注册,注册接口是个异步操作,所以使用flatMapLatest进行串联。接着进行’登录’操作,注意,在登录操作前对’注册’操作是否成功进行了判断,成功才会继续执行’登录‘,失败则直接抛出错误信号。

    至此,整个注册功能的核心已经完成,需要注意的是,SignUpVM初始化时的这一系列输入流到输出流的转换,并没有产生任何side effect,我们只是定义了该如何转换,换句话说,我们只是定义了 outputStream = **f** (inputStream), 而 f则是需要等到inputStream有数据产生时才会执行。

    总结

    在用响应式的思维实现业务时需要关注三个点:
    - 要处理哪些事件(比如:网络状态变更、页面滚动、页面关闭、动画执行完毕、接口请求出错等等)
    - 怎么处理
    - 处理结果怎么返回

    以上三点分别抽象为:输入流(要处理的事件),变换函数(怎么处理),输出流(处理结果)。
    所以归根结底,响应式编程就是面向数据流的编程。
    难点在于,在开始编码前就需要能够根据业务需求精确的分析出各种数据流,对于不熟悉的业务场景,确实难以下手。

    再谈RxSwift,其实RxSwift本质上就一个功能—-回调,但是通过采用’订阅数据流’的形式,能够将回调行为衍生出的各种复杂问题以一种可视化的符合人类思维逻辑的形式进行解决。

    RxSwift和响应式编程又是什么关系?
    吃饭与筷子的关系吧,你可以不用筷子吃饭,但是用筷子会更方便。

    最后,以上只是个人在学习RxSwift过程中的一些感悟,难免会有些理解错误的地方,如有发现,还望斧正。

    展开全文
  • RxSwift-MVVM

    2019-08-23 21:10:19
    MVVM核心在于数据与UI的双向绑定,数据的变化会更新UI,UI变化会更新我们的数据。...一、登录页面 先看看效果: UI页面代码省略,下面只看数据UI是如何绑定的。 1、UISwitch和UILabel的绑定 switch1.rx.isOn...
  • 今天突然想学RxSwift,看了相关只是点后,打算弄个注册界面练练手; 然后遇到了一个小问题,可能对概念没完全了解,实在无法解决前来请教,如图: ![图片说明]...
  • RxSwift 的简单使用

    2017-03-12 16:52:25
    RxSwift为ReactiveX(Reactive Extensions)旗下的Swift语言库,提供了Swift平台上进行响应式编程的解决方案。Rx的重要角色为Observable(被观察者)和Observer(观察者),Observable类似于ReactiveCocoa中的Signal...
  • RxSwift中提供了多种不同的错误处理操作符,它们可以在链式操作中相互组合以实现复杂的处理逻辑,下面先简单介绍一下RxSwift提供的错误处理操作,然后通过一些具体的例子来看看如何在实际项目中应用。这里不会详细...
  • 函数响应式编程FRP 之前学习过Objective-C下的FRP第三方开源框架ReactiveCocoa,部分学习笔记: ...它可以在MVVM架构模式中充当着View(视图)层与ViewModel(视图模型)层之间的Binder(绑定者)角色,实现两个层之间...
  • RxSwift处理Error事件

    2020-04-28 15:48:06
    如何处理RxSwift的Error事件 翻译自:How to handle errors in RxSwift 在最近这些日子里,MVVM在iOS开发中变得越来约受欢迎,RxSwfit也变得越来越流行。在RxSwift中大多数属性都是序列(Observable)。 但是,当...
  • 官方针对这个例子,介绍了用RxSwift实现MVVM,一个是使用Driver 序列(针对UI界面的),另一个是Observable 序列。所以可以总结的知识点如下: MVVM的实现 Observable序列和Driver序列的区别 功能说明还是老样子,先...
  • 更多内容,欢迎关注作者...集成 RxSwift 地址: https://github.com/ReactiveX/RxSwift/   1、下载 RxSwift 到本地 将 RxSwift 工程下载到本地,将其中根目录下的如下文件单独拷贝到一个文件夹下   RxSwift |
  • Rx 是微软出品的一个 Funtional Reactive Programming 框架,RxSwift 是它的一个 Swift 版本的实现。 RxSwift 的主要目的是能简单的处理多个异步操作的组合,和事件/数据流。 利用 RxSwift,我们可以把本来要分散...
  • RxSwift学习心得

    2017-08-30 16:17:43
    最近接触了RxSwift,有一些心得自己学习体会,有不对的地方还望各路大神多多指教,不喜勿喷,没有官方文档那么正式,根据自己的理解来的RxSwift ObserVerable 获得Observable combineLatest 简单使用 binTO 简单使用...
  • 文章目录一、 Rx 介绍1、什么是Rx2、RxSwift3、RxCocoa二、Rx 常见用法1、给 button 添加点击事件(RxCocoa)2、事件 + bind + combine3、遵循代理并实现4、闭包回调5、通知6、多任务依赖关系管理7、多任务异步并行...
  • RxSwift+Moya之项目实战 RxSwift相关基本介绍和用法可参考: RxSwift的使用详解01 RxSwift的使用详解02 一. 下面将将进行实战项目 1.登录注册功能 输入用户名要大于6个字符,不然密码不能输入 密码必须大于6个...
  • 单组数据绑定 let data = Observable<[String]>.just(["first element", "second element", "third element"]) data.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { index, model, cell in ...
  • RxSwift+Moya之项目实战

    2017-10-08 18:59:23
    RxSwift+Moya之项目实战 RxSwift相关基本介绍和用法可参考: RxSwift的使用详解01 RxSwift的使用详解02 一. 下面将将进行实战项目 1.登录注册功能 输入用户名要大于6个字符,不然密码不能输入 密码必须大于6个...
  • SimpleValidation简单的登录界面 let usernameValid = usernameOutlet.rx.text.orEmpty .map { $0.characters.count >= minimalUsernameLength } .shareReplay(1) // without this map would
1 2 3 4 5 ... 15
收藏数 281
精华内容 112
热门标签
关键字:

登录rxswift