[Разработка под iOS, Разработка мобильных приложений, Дизайн мобильных приложений] Как сделать экран подтверждения СМС-кода на iOS
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Привет, Хабр! Меня зовут Игорь, я 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 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")])
}
Полный код тестов, да и вообще весь пример можно найти тут. Требования могут слегка меняться от проекта к проекту (например, код можно отправлять по кнопке а не автоматом), но этот код достаточно несложно приспособить к подобным изменениям.Ну, а если вам понравился наш подход, вам интересны большие проекты по мобильной разработке, тогда напишите нам.
===========
Источник:
habr.com
===========
Похожие новости:
- [Веб-дизайн, Разработка веб-сайтов, Платежные системы, JavaScript, Дизайн мобильных приложений] Создаём королевскую форму для приёма банковских карт
- [Разработка мобильных приложений, API, Разработка для Office 365] 4 технических решения, которые делают API сервис успешным (перевод)
- [Разработка под iOS] Архитектурные паттерны в iOS: привет от дядюшки Боба, или Clean Architecture
- [Разработка мобильных приложений, Прототипирование, Развитие стартапа, Здоровье] Нужно ль развивать прототипирование софта в медицине, психологии и биологии?
- [Python, Разработка мобильных приложений, Лайфхаки для гиков] Как учить протоколы без чтения RFC: как сэкономить время при разработке
- [Java, .NET, Разработка мобильных приложений, C#, Kotlin] C# vs Kotlin
- [Разработка мобильных приложений, Тестирование мобильных приложений, Аналитика мобильных приложений, Мозг, Здоровье] Cognitive therapy и мобильные приложения против невротической депрессии
- [Разработка мобильных приложений, Обработка изображений, Машинное обучение, Искусственный интеллект] Ученые компании Smart Engines окончательно решили задачу распознавания паспорта РФ
- [Обработка изображений, Космонавтика, Транспорт, Астрономия] «Кьюриосити» снял редкие облака Марса
- [Разработка мобильных приложений, Интервью, IT-компании] Приключение в один день или One Day Offer от Яндекса
Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_dizajn_mobilnyh_prilozhenij (Дизайн мобильных приложений), #_ekran_blokirovki (экран блокировки), #_ios, #_blog_kompanii_agentstvo_agima (
Блог компании Агентство AGIMA
), #_razrabotka_pod_ios (
Разработка под iOS
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_dizajn_mobilnyh_prilozhenij (
Дизайн мобильных приложений
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 26-Ноя 04:02
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Привет, Хабр! Меня зовут Игорь, я Head of Mobile в компании AGIMA. Через нас проходит много проектов и оценок, функционал там зачастую повторяется, поэтому я решил показать, как мы решаем типовые задачи, и поделиться этим с вами. Начнем мы с самого начала. Как правило, началом для приложений служит авторизация. Рассмотрим классический случай с вводом номера телефона и смской и остановимся подробнее на экране подтверждения смс. Важно: в примере кода на github будет полноценный пример с вводом номера телефона и кодом, но экран ввода номера телефона совсем скучный, поэтому сегодня мы вводим код :)Выглядит не очень сложно, но, если присмотреться, функционал экрана довольно большой, а именно:
Можно, конечно, отправить всю логику про таймеры и 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() } /// Например, после успешного подтверждения кода нам могут предложить ввести перс. данные
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 } } let codeText = codeTextField.rx.text.share()
codeText .bind(to: viewModel.code) .disposed(by: disposeBag) retryButton.rx.tap .bind(to: viewModel.getNewCode) .disposed(by: disposeBag) let _codeSubject = PublishSubject<String>()
self.code = _codeSubject.asObserver() let codeObservable = _codeSubject.asObservable() let validCodeObservable = codeObservable.filter { $0.count == codeLength } let codeEvents: Observable<Result<Void, Error>> = validCodeObservable
.flatMap { (code) in authService.confirmCode(code: code, token: token).materialize() }.share()
viewModel.codeTimerIsActive
.drive(retryButton.rx.isHidden) .disposed(by: disposeBag) viewModel.codeTimerIsActive .not() .drive(timerLabel.rx.isHidden) .disposed(by: disposeBag) errors = codeEvents.errors().merge(with: fetchNewCode.errors())
.compactMap { ($0 as? ErrorType)?.localizedDescription } .asDriver(onErrorJustReturn: "") viewModel.isLoading
.not() .drive(codeTextField.rx.isEnabled) .disposed(by: disposeBag) 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)]) } 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 =========== Похожие новости:
Блог компании Агентство AGIMA ), #_razrabotka_pod_ios ( Разработка под iOS ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_dizajn_mobilnyh_prilozhenij ( Дизайн мобильных приложений ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 26-Ноя 04:02
Часовой пояс: UTC + 5