Русский
Русский
English
Статистика
Реклама

Как сделать экран подтверждения СМС-кода на iOS

Привет, Хабр!

Меня зовут Игорь, я Head of Mobile в компании AGIMA.

Через нас проходит много проектов и оценок, функционал там зачастую повторяется, поэтому я решил показать, как мы решаем типовые задачи, и поделиться этим с вами. Начнем мы с самого начала. Как правило, началом для приложений служит авторизация. Рассмотрим классический случай с вводом номера телефона и смской и остановимся подробнее на экране подтверждения смс.

Важно: в примере кода на 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 successcase 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")])    }

Полный код тестов, да и вообще весь пример можно найти тут. Требования могут слегка меняться от проекта к проекту (например, код можно отправлять по кнопке а не автоматом), но этот код достаточно несложно приспособить к подобным изменениям.

Ну, а если вам понравился наш подход, вам интересны большие проекты по мобильной разработке, тогда напишите нам.

Источник: habr.com
К списку статей
Опубликовано: 03.06.2021 16:16:28
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании агентство agima

Разработка под ios

Разработка мобильных приложений

Дизайн мобильных приложений

Экран блокировки

Ios

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru