Swift로 좋은 느낌의 ViewModel을 만드는 메모
(이 앱 자체는 아무래도 좋은 것이지만)
동기 부여
전제가 되는 생각
ViewController.swift
viewDidLoaded
RxCocoa.Drive
는 UI의 세계 거주자이기 때문에ViewController에 변환한다.viewModel = ViewModel(
emailText: emailField.rx.text.orEmpty.asObservable(),
passwordText: passwordField.rx.text.orEmpty.asObservable(),
submitButtonTap: submitButton.rx.tap.asObservable()
)
viewModel.nextViewPushing.asDriver(onErrorJustReturn: ())
.drive(onNext: pushToHome)
.addDisposableTo(disposeBag)
viewModel.requestError.asObservable()
.filter { $0 != nil }
.observeOn(MainScheduler.instance)
.subscribe(onNext: showError(error:))
.addDisposableTo(disposeBag)
ViewModel
의존하는 입력값의 Observable는 이니셜라이저의 인수나 tuple로 꽂는다.
이니셜라이저 경유가 아니라
PublishSubject<T>
를 공개하고 밖에서 bindTo
하는 패턴과 어느 쪽이 좋은지 헤매었다.let nextViewPushing: Observable<Void>
let requestError = Variable<Swift.Error?>(nil)
init(
emailText: Observable<String> = Observable.empty(),
passwordText: Observable<String> = Observable.empty(),
submitButtonTap: Observable<Void> = Observable.empty(),
signIn: SignIn = SignInImpl(httpClient: URLSession.shared)
) {
let requestError = self.requestError
let fields = Observable.combineLatest(emailText, passwordText)
nextViewPushing = submitButtonTap.withLatestFrom(fields)
.flatMapLatest { statusCode, password -> Observable<String> in
return signIn
.sendRequest(email: statusCode, password: password)
.catchError { e in
requestError.value = e
return Observable.empty()
}
}
.do(onNext: { debugPrint($0) })
.map { _ in () }
}
let requestError = Variable<Swift.Error?>(nil)
이마이치라고 생각합시다.
I/O
HTTPClient는 Cocoa의 세계 거주자이므로 ViewModel에 직접 등장하고 싶지 않습니다.
테스트 코드로 HTTP 액세스 결과를 스텁하고 싶기 때문에 프로토콜로 취급한다.
protocol SignIn {
func sendRequest(email: String, password: String) -> Observable<String>
}
struct SignInImpl: SignIn {
let httpClient: URLSession
func sendRequest(email: String, password: String) -> Observable<String> {
let url = URL(string: "https://httpbin.org/status/\(email)")!
let req = URLRequest(url: url)
return httpClient.rx.data(request: req)
.map { String(data: $0, encoding: .utf8) ?? "" }
}
}
SignInTests.swift
이니셜라이저로 입력이 완결되어 있으므로 RxBlocking으로 테스트하고 싶은 ViewModel의 목적의 값을 꺼내기만 하면 됩니다.
시간축이 있는 연속적인 조작을 상정한 테스트를 쓰려면 RxTest의 TestScheduler를 사용할 필요가 나올지도 모른다
func testSigInSuccess() {
let viewModel = ViewModel(
emailText: Observable.just("200"),
passwordText: Observable.just("pass"),
submitButtonTap: Observable.just(),
signIn: SignInMock()
)
let tapCount = try! viewModel.nextViewPushing.toBlocking(timeout: 1).toArray()
XCTAssertEqual(tapCount.count, 1)
}
func testSigInFailure() {
let viewModel = ViewModel(
emailText: Observable.just("401"),
passwordText: Observable.just("pass"),
submitButtonTap: Observable.just(),
signIn: SignInMock()
)
viewModel.nextViewPushing.subscribe().disposed(by: self.disposeBag)
let error = viewModel.requestError.value as? NSError
XCTAssertEqual(error, SignInMock.error)
}
미래 전개
Reference
이 문제에 관하여(Swift로 좋은 느낌의 ViewModel을 만드는 메모), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://qiita.com/laiso/items/9e113e4aecb546300360텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)