1. RxCocoa
UIKit에 UI다룰때 편한 extention들이 있는 framework
2. 간단히 UITextFiled 값 받아서 처리하기
간단하게 TextField를 추가하고 valid로직을 추가하자.
로그인할때 id에 @랑 .이 존재하는지 판단하는 간단한 valid체크다.
예시라 이렇게 하는거뿐이다.
@IBOutlet weak var idField: UITextField! // 스토리보드에서 텍스트필드 가져오기
// 간단한 체크로직 @랑.이 있으면 true반환
private func checkEmailValid(_ email:String) -> Bool {
return email.contains("@") && email.contains(".")
}
위와같이 선언해주고 textField에서 입력하는것을 rx로 받아보자.
idField.rx.text // textField에 text가 입력되는걸 스트림으로 받음
.filter { $0 != nil} // nil이 아닌경우
.map {$0!} // 강제 추출
.map(checkEmailValid) // @랑 .이 있는지 판단(함수)
.subscribe(onNext: { b in // 함수 결과값인 Bool값이 내려옴
print(b) // 그걸 출력
}).disposed(by: disposeBag)
여기에서 nil이 아닌경우 강제추출하는거 2줄이나 걸려서 보기 안좋으니 아래와 같이 바꿔보자.
idField.rx.text // textField에 text가 입력되는걸 스트림으로 받음
.orEmpty // nil이 아닐경우 강제 추출
.map(checkEmailValid) // @랑 .이 있는지 판단(함수)
.subscribe(onNext: { b in // 함수 결과값인 Bool값이 내려옴
print(b) // 그걸 출력
}).disposed(by: disposeBag)
간단하게 패스워드 체크도 5글자 이상으로 넣어보자
@IBOutlet weak var pwField: UITextField!
private func checkPasswordValid(_ password:String) -> Bool {
return password.count > 5
}
pwField.rx.text.orEmpty
.map(checkPasswordValid)
.subscribe(onNext: { b in
print(b)
}).disposed(by: disposeBag)
3. 두 입력값 조합하기
이제 아이디랑 패스워드를 받았는데 그 두 조건이 만족하면 버튼을 enable, 만족하지 않으면 버튼을 disable하려고 한다.
그럼 idField와 pwField의 stream을 동시에 받아야할 것 같다.
그런경우에 아래와 같이 쓴다.
CombineLatest : Observable중 하나라도 항목을 배출할 경우 마지막으로 배출된 항목들을 결합시켜 배출해야 할경우 사용, 두 개의 Observable 중 하나가 항목을 배출할 때 배출된 마지막 항목과 다른 한 Observable이 배출한 항목을 결합한 후 함수를 적용하여 실행 후 실행된 결과를 배출한다
id, pw두개 다 받는데 어느쪽이라도 입력하면 둘 다 결과를 받을 수 있게 사용하기 위해서 사용.
둘중에 하나라도 바뀌면 바뀐 최근의 값을 전달해준다.
그럼 현재 사용해야할것은 아래 두줄일 것이다.
idField.rx.text.orEmpty.map(checkEmailValid)
pwField.rx.text.orEmpty.map(checkPasswordValid)
이 둘을 받아서 아래와 같이 사용한다.
Observable.combineLatest( // 스트림 합침
idField.rx.text.orEmpty.map(checkEmailValid), // 첫번째 스트림
pwField.rx.text.orEmpty.map(checkPasswordValid)) // 두번째 스트림
.map { s1, s2 in s1 && s2 } // 두개의 소스를 받아 &&연산을 한 뒤 내려준다.
.subscribe(onNext: { b in // 그 결과를 b로 받음
print(b) // b 출력
self.loginButton.isEnabled = b // 버튼 enable처리
}).disposed(by: disposeBag)
여기에서 .map을 찍는게 귀찮다면 아래와 같이 쓸 수도 있다.
Observable.combineLatest( // 스트림 합침
idField.rx.text.orEmpty.map(checkEmailValid), // 첫번째 스트림
pwField.rx.text.orEmpty.map(checkPasswordValid), // 두번째 스트림
resultSelector:{s1, s2 in s1 && s2} ) // 두개의 소스를 받아 &&연산을 한 뒤 내려준다.
.subscribe(onNext: { b in // 그 결과를 b로 받음
print(b) // b 출력
self.loginButton.isEnabled = b // 버튼 enable처리
}).disposed(by: disposeBag)
뒤에 resultSelector를 사용해서 바로 연산한뒤 내려줄 수도 있다.
Zip : 두개를 주면, 둘 다 데이터가 만들어지면 전달함, 한쪽이 데이터가 바뀌어도 다른쪽이 안바뀌었다면 내려주지 않음
Merge : 두개를 주면, select못하고 들어오는대로 내려줌.
그래서 지금 상황에선 CombineLatest가 적절하다.
4. 반복되는 코드 정리하기
근데 아까 코드를 보면 코드에 반복되는 부분이 많다.
위에서 input, output을 구분하자면
input : 아이디 입력, 비번 입력
output : validView, loginButton
위의 상태에 따라 코드를 나눠보자.
먼저 비교를 위해 기본소스
idField.rx.text // textField에 text가 입력되는걸 스트림으로 받음
.orEmpty // nil이 아닐경우 강제 추출
.map(checkEmailValid) // @랑 .이 있는지 판단(함수)
.subscribe(onNext: { b in // 함수 결과값인 Bool값이 내려옴
print(b) // 그걸 출력
self.idValidView.isHidden = b
}).disposed(by: disposeBag)
pwField.rx.text.orEmpty
.map(checkPasswordValid)
.subscribe(onNext: { b in
print(b)
self.pwValidView.isHidden = b
}).disposed(by: disposeBag)
Observable.combineLatest( // 스트림 합침
idField.rx.text.orEmpty.map(checkEmailValid), // 첫번째 스트림
pwField.rx.text.orEmpty.map(checkPasswordValid), // 두번째 스트림
resultSelector:{s1, s2 in s1 && s2} ) // 두개의 소스를 받아 &&연산을 한 뒤 내려준다.
.subscribe(onNext: { b in // 그 결과를 b로 받음
print(b) // b 출력
self.loginButton.isEnabled = b // 버튼 enable처리
}).disposed(by: disposeBag)
코드를 나눈 소스
// input
let idInputOb:Observable<String> = idField.rx.text.orEmpty.asObservable() // idTextField text
let idValidOb = idInputOb.map(checkEmailValid) // idTextField가 valid한지 판별
let pwInputOb:Observable<String> = pwField.rx.text.orEmpty.asObservable() // pwTextField text
let pwValidOb = pwInputOb.map(checkPasswordValid) // pwTextField가 valid한지 판별
// output
idValidOb.subscribe(onNext:{b in self.idValidView.isHidden = b}).disposed(by: disposeBag) // valid값으로 id valid view hidden
pwValidOb.subscribe(onNext:{b in self.pwValidView.isHidden = b}).disposed(by: disposeBag) // valid값으로 pw valid view hidden
Observable.combineLatest(idValidOb, pwValidOb,resultSelector: { $0 && $1})
.subscribe(onNext:{b in self.loginButton.isEnabled = b}).disposed(by: disposeBag) // valid값 조합해서 login button hidden
이렇게 나눠서 써도 똑같이 동작한다.
코드가 훨씬 간결해지고 한눈에 들어온다.
5. Binding
현재 idValidOb를 받아 subscribe를 하고 그 코드를 한줄에서 처리하고 있다.
이를 바깥에서 받는것으로 수정하려고 한다.
// Subject : Observable인데 스스로 데이터를 만들 수 있는 놈, 데이터를 외부에서 넣어줄 수 있음
let idValid : BehaviorSubject<Bool> = BehaviorSubject(value: false) // default값 false
let pwValid : BehaviorSubject<Bool> = BehaviorSubject(value: false)
밖에 idValid, pwValid를 설정하고 이를 BehaviorSubject로 설정한다.
BehaviorSubject의 동작은 아래와 같다.
옵저버가 BehaviorSubject를 구독하기 시작하면, 옵저버는 소스 Observable이 가장 최근에 발행한 항목(또는 아직 아무 값도 발행되지 않았다면 맨 처음 값이나 기본 값)의 발행을 시작하며 그 이후 소스 Observable(들)에 의해 발행된 항목들을 계속 발행한다.
만약, 소스 Observable이 오류 때문에 종료되면 BehaviorSubject는 아무런 항목도 배출하지 않고 소스 Observable에서 발생한 오류를 그대로 전달한다.
let idValidOb = idInputOb.map(checkEmailValid) // idTextField가 valid한지 판별
idValidOb.subscribe(onNext:{b in self.idValidView.isHidden = b}).disposed(by: disposeBag)
//위의 코드를 아래와 같이 바꿀 수 있다.
idValidOb.subscribe(onNext:{b in self.idValid.onNext(b)}).disposed(by: disposeBag)
// 위의 코드를 또 간결하게 쓸 수 있다.
idValidOb.bind(to: idValid).disposed(by: disposeBag)
//그래서 이제 간결한 코드로 작성을 한다면 아래와 같다.
let idInputOb:Observable<String> = idField.rx.text.orEmpty.asObservable()
idInputOb.map(checkEmailValid).bind(to: idValid).disposed(by: disposeBag)
let pwInputOb:Observable<String> = pwField.rx.text.orEmpty.asObservable()
pwInputOb.map(checkPasswordValid).bind(to: pwValid).disposed(by: disposeBag)
위에서는 bool값만 binding하였는데 String값도 binding하면 아래와 같다.
let idInputText:BehaviorSubject<String> = BehaviorSubject(value: "")
let pwInputText:BehaviorSubject<String> = BehaviorSubject(value: "")
Subject를 선언하였으니 TextField에서 받는값도 외부로 전달하자.
// idFiled의 text를 binding
idField.rx.text.orEmpty.bind(to: idInputText).disposed(by: disposeBag)
// pwFiled의 text를 binding
pwField.rx.text.orEmpty.bind(to: pwInputText).disposed(by: disposeBag)
위를 바꿨으니 아까 설정하였던 InputOb에서 구독할 필요가 없이 inputText들을 구독하면 된다.
따라서 구독하는 코드들도 아래와 같이 바꾸자.
idInputOb.map(checkEmailValid).bind(to: idValid).disposed(by: disposeBag)
pwInputOb.map(checkPasswordValid).bind(to: pwValid).disposed(by: disposeBag)
// 위를 아래와 같이 바꾸자
idInputText.map(checkEmailValid).bind(to: idValid).disposed(by: disposeBag)
pwInputText.map(checkPasswordValid).bind(to: pwValid).disposed(by: disposeBag)
// 최종적으로 코드는 아래 4줄이 될것이다.
idField.rx.text.orEmpty.bind(to: idInputText).disposed(by: disposeBag)
idInputText.map(checkEmailValid).bind(to: idValid).disposed(by: disposeBag)
pwField.rx.text.orEmpty.bind(to: pwValid).disposed(by: disposeBag)
pwInputText.map(checkPasswordValid).bind(to: pwValid).disposed(by: disposeBag)
이럼 인풋에 대한 처리했던것이 각각 idValid, pwValid에 들어가니 output도 아래와 같이 소스를 수정할 수 있다.
// output
idValid.subscribe(onNext:{b in self.idValidView.isHidden = b}).disposed(by: disposeBag)
pwValid.subscribe(onNext:{b in self.pwValidView.isHidden = b}).disposed(by: disposeBag)
Observable.combineLatest(idValid, pwValid,resultSelector: { $0 && $1})
.subscribe(onNext:{b in self.loginButton.isEnabled = b}).disposed(by: disposeBag)
위와 같이 input과 output을 설정할 수 있는데 이러면 소스코드가 굳이 한 함수 안에 없어도 된다.
따라서 위의 Input으로 함수를 하나 만들고, 아래 output으로 함수를 만들면 좀 더 명확하게 코드 구분이 된다.
6. ViewModel
여기서 로직관련 코드들은 굳이 ViewController에 있을 필요가 없다.
그래서 ViewModel을 만들고 필요한 것만 받아서 사용할 예정이다.
일단 전체 코드를 한 번 보자.
@IBOutlet weak var idField: UITextField!
@IBOutlet weak var pwField: UITextField!
@IBOutlet weak var loginButton: UIButton!
@IBOutlet weak var idValidView: UIView!
@IBOutlet weak var pwValidView: UIView!
let idInputText:BehaviorSubject<String> = BehaviorSubject(value: "")
let pwInputText:BehaviorSubject<String> = BehaviorSubject(value: "")
let idValid : BehaviorSubject<Bool> = BehaviorSubject(value: false) // default값 false
let pwValid : BehaviorSubject<Bool> = BehaviorSubject(value: false)
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindInput()
bindOutput()
}
private func bindInput() {
// input
idField.rx.text.orEmpty.bind(to: idInputText).disposed(by: disposeBag)
idInputText.map(checkEmailValid).bind(to: idValid).disposed(by: disposeBag)
pwField.rx.text.orEmpty.bind(to: pwInputText).disposed(by: disposeBag)
pwInputText.map(checkPasswordValid).bind(to: pwValid).disposed(by: disposeBag)
}
private func bindOutput() {
// output
idValid.subscribe(onNext:{b in self.idValidView.isHidden = b}).disposed(by: disposeBag)
pwValid.subscribe(onNext:{b in self.pwValidView.isHidden = b}).disposed(by: disposeBag)
Observable.combineLatest(idValid, pwValid,resultSelector: { $0 && $1})
.subscribe(onNext:{b in self.loginButton.isEnabled = b}).disposed(by: disposeBag)
}
private func checkEmailValid(_ email:String) -> Bool {
return email.contains("@") && email.contains(".")
}
private func checkPasswordValid(_ password:String) -> Bool {
return password.count > 5
}
여기에서 email을 체크하거나, password를 체크하는 로직들은 굳이 뷰에 있을 필요가 없다.
이 코드들을 ViewModel을 만들어서 넘겨주고 모든 변화는 뷰모델을 통해 받는 식으로 수정해보자.
import UIKit
import RxSwift
import RxCocoa
class ViewModel {
private func checkEmailValid(_ email:String) -> Bool {
return email.contains("@") && email.contains(".")
}
private func checkPasswordValid(_ password:String) -> Bool {
return password.count > 5
}
}
그리고 원래 코드에서 해당 함수를 쓰는 곳들은 idValid, pwValid다.
얘도 넘겨주자.
import UIKit
import RxSwift
import RxCocoa
class ViewModel {
let idValid : BehaviorSubject<Bool> = BehaviorSubject(value: false)
let pwValid : BehaviorSubject<Bool> = BehaviorSubject(value: false)
private func checkEmailValid(_ email:String) -> Bool {
return email.contains("@") && email.contains(".")
}
private func checkPasswordValid(_ password:String) -> Bool {
return password.count > 5
}
}
또한 textField의 text 변화들도 솔직히 뷰가 알 필요는 없다.
그것도 뷰 모델에 넘겨주자.
import UIKit
import RxSwift
import RxCocoa
class ViewModel {
let idInputText:BehaviorSubject<String> = BehaviorSubject(value: "")
let pwInputText:BehaviorSubject<String> = BehaviorSubject(value: "")
let idValid : BehaviorSubject<Bool> = BehaviorSubject(value: false)
let pwValid : BehaviorSubject<Bool> = BehaviorSubject(value: false)
private func checkEmailValid(_ email:String) -> Bool {
return email.contains("@") && email.contains(".")
}
private func checkPasswordValid(_ password:String) -> Bool {
return password.count > 5
}
}
이제 ViewModel에서 InputText를 받아서 Valid체크하는 로직을 만들어야하는데, 이를 ViewModel생성할때 만들어 보자.
import UIKit
import RxSwift
import RxCocoa
class ViewModel {
var disposeBag = DisposeBag()
let idInputText:BehaviorSubject<String> = BehaviorSubject(value: "")
let pwInputText:BehaviorSubject<String> = BehaviorSubject(value: "")
let idValid : BehaviorSubject<Bool> = BehaviorSubject(value: false)
let pwValid : BehaviorSubject<Bool> = BehaviorSubject(value: false)
init() {
idInputText.map(checkEmailValid).bind(to: idValid).disposed(by: disposeBag)
pwInputText.map(checkPasswordValid).bind(to: pwValid).disposed(by: disposeBag)
}
private func checkEmailValid(_ email:String) -> Bool {
return email.contains("@") && email.contains(".")
}
private func checkPasswordValid(_ password:String) -> Bool {
return password.count > 5
}
}
그럼 이제 뷰 모델에서 valid관련 처리를 다 하기 때문에 뷰는 뷰모델에서 해당 필드들이 valid한지만 얻어오면 된다.
그래서 원래 뷰의 코드가 아래와 같이 바뀐다.
import UIKit
import RxSwift
import RxCocoa
class LogInViewController: UIViewController {
@IBOutlet weak var idField: UITextField!
@IBOutlet weak var pwField: UITextField!
@IBOutlet weak var loginButton: UIButton!
@IBOutlet weak var idValidView: UIView!
@IBOutlet weak var pwValidView: UIView!
let viewModel = ViewModel() // 뷰모델 생성
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindInput()
bindOutput()
}
private func bindInput() {
// viewModel에 text 변화 전달
idField.rx.text.orEmpty.bind(to: viewModel.idInputText).disposed(by: disposeBag)
pwField.rx.text.orEmpty.bind(to: viewModel.pwInputText).disposed(by: disposeBag)
}
private func bindOutput() {
// viewModel에서 valid변화를 구독
viewModel.idValid.subscribe(onNext:{b in self.idValidView.isHidden = b}).disposed(by: disposeBag)
viewModel.pwValid.subscribe(onNext:{b in self.pwValidView.isHidden = b}).disposed(by: disposeBag)
Observable.combineLatest(viewModel.idValid, viewModel.pwValid,resultSelector: { $0 && $1})
.subscribe(onNext:{b in self.loginButton.isEnabled = b}).disposed(by: disposeBag)
}
}
이제 뷰에서는 idField랑 pwField 관련 로직이 하나도 남아있지 않는다.
근데 bindOutput()함수를 보면 버튼 enable관련 로직이 들어가 있다.
얘도 어짜피 idValid와 pwValid가지고 판단하는 것이니 ViewModel에서 판단한 뒤 알려주자.
import UIKit
import RxSwift
import RxCocoa
class ViewModel {
var disposeBag = DisposeBag()
let idInputText:BehaviorSubject<String> = BehaviorSubject(value: "")
let pwInputText:BehaviorSubject<String> = BehaviorSubject(value: "")
let idValid : BehaviorSubject<Bool> = BehaviorSubject(value: false) // default값 false
let pwValid : BehaviorSubject<Bool> = BehaviorSubject(value: false)
let buttonEnabled : BehaviorSubject<Bool> = BehaviorSubject(value: false) // buttonEnabled추가
init() {
idInputText.map(checkEmailValid).bind(to: idValid).disposed(by: disposeBag)
pwInputText.map(checkPasswordValid).bind(to: pwValid).disposed(by: disposeBag)
Observable.combineLatest(idValid, pwValid,resultSelector: { $0 && $1}).bind(to: buttonEnabled).disposed(by: disposeBag) // binding
}
private func checkEmailValid(_ email:String) -> Bool {
return email.contains("@") && email.contains(".")
}
private func checkPasswordValid(_ password:String) -> Bool {
return password.count > 5
}
}
이제 loginButton이 enable될지 말지는 ViewModel이 알고 있다.
이제 ViewController에서는 구독만 해주자.
import UIKit
import RxSwift
import RxCocoa
class LogInViewController: UIViewController {
@IBOutlet weak var idField: UITextField!
@IBOutlet weak var pwField: UITextField!
@IBOutlet weak var loginButton: UIButton!
@IBOutlet weak var idValidView: UIView!
@IBOutlet weak var pwValidView: UIView!
let viewModel = ViewModel()
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindInput()
subscribe()
}
private func bindInput() {
// input
idField.rx.text.orEmpty.bind(to: viewModel.idInputText).disposed(by: disposeBag)
pwField.rx.text.orEmpty.bind(to: viewModel.pwInputText).disposed(by: disposeBag)
}
private func subscribe() {
// output
viewModel.idValid.subscribe(onNext:{b in self.idValidView.isHidden = b}).disposed(by: disposeBag)
viewModel.pwValid.subscribe(onNext:{b in self.pwValidView.isHidden = b}).disposed(by: disposeBag)
viewModel.buttonEnabled.subscribe(onNext:{b in self.loginButton.isEnabled = b}).disposed(by: disposeBag)
}
}
이제 뷰에서는 로직에 관련된 코드가 하나도 없게 바뀌었다.
bindOutput을 하지 않고 viewModel의 변수를 구독만 하기 때문에 함수명도 subscribe로 수정하였다.
'공부 > swift' 카테고리의 다른 글
[RxSwift] JSON파싱해서 테이블뷰에 뿌리기 (2) | 2020.09.17 |
---|---|
[swift] RxSwift Operator(SugarAPI) (0) | 2020.09.15 |
[swift] RxSwift 기본적인 사용법 (0) | 2020.09.14 |
[swift] RxSwift (0) | 2020.09.06 |