iOSでSMS確認画面を作る方法

こんにちは、ハブルです!





私の名前は Igor です。AGIMA のモバイル部門の責任者です。 





多くのプロジェクトと評価が私たちを通過し、機能はそこで繰り返されることが多いため、典型的なタスクをどのように解決するかを示し、それをあなたと共有することにしました。最初から始めます。通常、アプリケーションは承認から始まります。電話番号と SMS を入力する古典的なケースを考えて、SMS 確認画面について詳しく見てみましょう。





重要: github のサンプル コードには、電話番号とコードを入力する完全な例がありますが、電話番号の入力画面はかなり退屈なので、今日はコードを入力します:)





それほど難しくはないように見えますが、よく見ると、画面の機能は非常に大きいです。





  • コードをサーバーに送信します。





  • タイマーの再送信と視覚的な表示を有効にします。





  • タイマーの終了後、「再送信」ボタンを表示します。





  • ;





  • ;





  • .





UI , .





, , isLoading View , . , MVVM+Rx ( ), . .





ViewModel «» : input output ( ). «- », , .





UI :





final class ConfirmCodeViewController: BaseViewController {

  ///   
  private lazy var codeTextField = CodeTextField()

  ///     
  private lazy var errorLabel = UILabel()

  ///            
  private lazy var loader = UIActivityIndicatorView()

  ///        
  private lazy var timerLabel = UILabel()

  ///    
  private lazy var retryButton = UIButton(type: .system)

  ///     
  private lazy var stackView = UIStackView()
}
      
      



ViewModel   :





/// ,         . 
enum AuthResult {
	case success
	case needPersonalData
}

protocol ConfirmCodeViewModelProtocol {
    ///     
    var code: AnyObserver<String> { get }
    
    ///    « »
    var getNewCode: AnyObserver<Void> { get }
    
    ///   
    var didAuthorize: Driver<AuthResult> { get }
    
    ///        
    var isLoading: Driver<Bool> { get }
    
    ///       
    var errors: Driver<String> { get }
    
    ///    
    var newCodeTimer: Driver<Int> { get }
    
    ///       « »
    var didRequestNewCode: Driver<Void> { get }
  
    ///     
    var codeTimerIsActive: Driver<Bool> { get }
}
      
      



, PublishSubject, BehaviourRelay , input output ViewModel.  .





View :





let codeText = codeTextField.rx.text.share()

codeText
    .bind(to: viewModel.code)
    .disposed(by: disposeBag)

retryButton.rx.tap
    .bind(to: viewModel.getNewCode)
    .disposed(by: disposeBag)
      
      



ViewModel - ( ) , , .





ViewModel , .





ViewModel « »:





let _codeSubject = PublishSubject<String>()
self.code = _codeSubject.asObserver()

let codeObservable = _codeSubject.asObservable()
let validCodeObservable = codeObservable.filter { $0.count == codeLength }
      
      



_codeSubject



 — textfield .





validCodeObservable



 — , .





,   PublishSubject



, AnyObserver



, Observable



, , , . : AnyObserver



Observable PublishSubject



.





let codeEvents: Observable<Result<Void, Error>> = validCodeObservable
    .flatMap { (code) in
        authService.confirmCode(code: code, token: token).materialize()
    }.share()
      
      



, :) .materialize()



. Observable



, . materialize Result<Value, Error>



- .





RxAction, , isLoading.





. , , . , , . , ( ), true



false



isLoading



.





didAuthorize = codeEvents.elements()...







.elements(



) codeEvents . , codeEvents



Result<Void, Error>



, RxSwiftExt.





  :





  • (validCodeObservable.mapTo(Void()))



    ;





  • (didRequestNewCode)



    ;





  • (.startWith(Void()))



    .





Observable.merge...



RxSwift. take(while:)



, 0. 





«» / , :





viewModel.codeTimerIsActive
    .drive(retryButton.rx.isHidden)
    .disposed(by: disposeBag)
        
viewModel.codeTimerIsActive
    .not()
    .drive(timerLabel.rx.isHidden)
    .disposed(by: disposeBag)
      
      



errors.







errors = codeEvents.errors().merge(with: fetchNewCode.errors())
            .compactMap { ($0 as? ErrorType)?.localizedDescription }
            .asDriver(onErrorJustReturn: "")
      
      



, , :





viewModel.isLoading
    .not()
    .drive(codeTextField.rx.isEnabled)
    .disposed(by: disposeBag)
      
      



ViewModel - , ! , , ViewModel . , . , RxTest!





class ConfirmCodeViewModelTests: XCTestCase {
    
// properties
// methods
 
    //MARK:- Helpers
    private func bindCodeInputEvents(
        _ events: [Recorded<Event<String>>] = [.next(100, "1"), .next(200, "11"), .next(300, "111"), .next(400, "1111")])
    {
        codeInputEvents = scheduler.createHotObservable(events)
        codeInputEvents.bind(to: viewModel.code).disposed(by: disposeBag)
    }
}

      
      



, — :





   func test_timerInvokedAutomatically() {
        let sut = scheduler.start(created: 0, subscribed: 0, disposed: 1000) { self.viewModel.newCodeTimer }
        XCTAssertEqual(sut.events, [.next(1, 2), .next(2, 1), .next(3, 0)])
    }
      
      



: , UI





 func test_errorEmmitedValueAtFailure() throws {
        bindCodeInputEvents()
        setConfirmCodeResult(.error(0, MockError.confirmFailure))
 
        let sut = scheduler.start { self.viewModel.errors }
        XCTAssertEqual(sut.events, [.next(400, "confirmFailure")])
    }
      
      



, . (, ), .





, , ,   .








All Articles