Cara membuat layar konfirmasi SMS di iOS

Halo, Habr!





Nama saya Igor, saya Head of Mobile di AGIMA. 





Banyak proyek dan evaluasi melewati kami , fungsinya sering diulang di sana, jadi saya memutuskan untuk menunjukkan bagaimana kami menyelesaikan tugas-tugas umum dan membaginya dengan Anda. Kita akan mulai dari awal. Biasanya, aplikasi dimulai dengan otorisasi. Mari kita pertimbangkan kasus klasik memasukkan nomor telepon dan SMS dan membahas lebih detail di layar konfirmasi SMS.





Penting: contoh kode di github akan memiliki contoh lengkap dengan memasukkan nomor telepon dan kode, tetapi layar entri nomor telepon cukup membosankan, jadi hari ini kami memasukkan kode :)





Tampilannya tidak terlalu sulit, namun jika diperhatikan lebih dekat, fungsi layarnya cukup besar, yaitu:





  • kirim kode ke server;





  • aktifkan kirim ulang timer + tampilan secara visual;





  • setelah penghitung waktu berakhir, tunjukkan tombol "kirim lagi";





  • ;





  • ;





  • .





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