이글은 Learning RxSwift's Github Sample( http://zh-wang.github.io/blog/2015/10/27/learning-rxswifts-github-sample/ ) 에 대한 번역입니다.
RwSwift 의 첫 예제는 Github 가입 view controller를 흉내냅니다. 이 예제는 사용자 이름, 패스워드에 대한 유효성을 확인합니다. 그런 다음 가입 절차를 가상으로 수행합니다.
- RxSwift 의 예제들은 여기에서 찾을 수 있습니다. https://github.com/ReactiveX/RxSwift/tree/master/RxExample/RxExample/Examples
Rx framework로 하기 가장 쉬운 일입니다. RxCocoa에 정의 되어 있는 UITextField 의 rx_text 필드가 여러분이 원하는 그것입니다.
let username = usernameOutlet.rx_text
rx_text는 Observable에 발생하는 모든 변경사항을 감싸는 textViewDidChange를 추적합니다. Observable 은 Observer에 의해서 관찰될(Observed) 수 있는 기초 이벤트 sequence 입니다.
각각의 입력 이벤트를 E라고 합시다. 그러면 이 이벤트 sequence는 다음과 같이 표시될 수 있습니다.
---E---E-E-----E-----
버튼에 대한 tap을 처리하는 것은 다음과 같이 쉽니다.
let signupSampler = self.signupOutlet.rx_tap
일반적인 가입 절차에서 우리는 다음을 확인해야 합니다.
1.사용자 이름이 비어있거나, 허용되지 않는 글자가 포함되어 있다.
2.사용자 이름이 이미 가입되어 있다.
3.패스워드 와 패스워드 재입력이 같다.
4.요청이 적절하게 처리될 수 있다 혹은 그렇지 않다.
이것은 Observable에 대한 flatmap 함수 입니다.(flatmap 에 대한 정의는 이 글( http://zh-wang.github.io/blog/2015/10/09/functor-monad-applicative-in-swift/ )에서 찾을 수 있습니다.)
typealias ValidationResult = (valid: Bool?, message: String?)
func validateUsername(username: String) -> Observable<ValidationResult> {
if username.characters.count == 0 {
return just((false, nil))
}
// this obviously won't b
if username.rangeOfCharacterFromSet(NSCharacterSet.alphanumericCharacterSet().invertedSet) != nil {
return just((false, "Username can only contain numbers or digits"))
}
let loadingValue = (valid: nil as Bool?, message: "Checking availabilty ..." as String?)
return API.usernameAvailable(username)
.map { available in
if available {
return (true, "Username available")
}
else {
return (false, "Username already taken")
}
}
.startWith(loadingValue)
}
Observable에 감싸진 값은 Bool 과 String 의 짝(pair)입니다.
첫 두 if 절은 각기 비었거나 허용되지 않은 문자들에 대한 확인을 위한 것입니다.
나머지 부분은 짧은 시간 후에 결과를 돌려주는 http 요청입니다.
사용자 입력 이벤트 sequence 에 각각의 이벤트에 'validateUsername'를 call 를 한다면,
let usernameValidation = username
.map { username in
return validationService.validateUsername(username)
}
이벤트 sequence는 이렇게 될 겁니다.(V는 유효성 체크, R은 결과)
---+---+-+-----+-----
| | | |
V V V V
| | | |
R | | |
R | R
R
유효성 체크들이 순서대로 호출되지만, 결과들은 네트워크 상태에 따라 무작위 순서로 리턴됩니다. 그리고 실제로 우리는 가장 최신 유효성 체크의 결과만 필요합니다.
그래서 우리는 여기서 switch 메소드를 사용합니다. switchlatest는 switch의 구현체 중 하나 입니다. switchlatest 는 항상 가장 최근에 발생한 이벤트로 전환되고 이전의 이벤트들은 폐기할 겁니다. switch 에 대한 소개 ( http://www.introtorx.com/content/v1.0.10621.0/12_CombiningSequences.html#Switch )
그리고 shareReplay(1) 는 이 Observer가 새로운 구독(subscription)들을 나중에 하게되더라도 단 하나의 할당만 유지하게 됩니다. RxSwift replay( https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#sharing-subscription-and-sharereplay-operator )
let usernameValidation = username
.map { username in
return validationService.validateUsername(username)
}
.switchLatest()
.shareReplay(1)
func validatePassword(password: String) -> ValidationResult {
let numberOfCharacters = password.characters.count
if numberOfCharacters == 0 {
return (false, nil)
}
if numberOfCharacters < minPasswordCount {
return (false, "Password must be at least \(minPasswordCount) characters")
}
return (true, "Password acceptable")
}
func validateRepeatedPassword(password: String, repeatedPassword: String) -> ValidationResult {
if repeatedPassword.characters.count == 0 {
return (false, nil)
}
if repeatedPassword == password {
return (true, "Password repeated")
}
else {
return (false, "Password different")
}
}
이들 두개의 Observable들을 combineLatest를 이용해서 합칩니다.(Combine) 이름 그대로 작동합니다.
let repeatPasswordValidation = combineLatest(password, repeatPassword) { (password, repeatedPassword) in
validationService.validateRepeatedPassword(password, repeatedPassword: repeatedPassword)
}
.shareReplay(1)
let signingProcess = combineLatest(username, password) { ($0, $1) }
.sampleLatest(signupSampler)
.map { (username, password) in
return API.signup(username, password: password)
}
.switchLatest()
.startWith(SignupState.InitialState)
.shareReplay(1)
signup 메소드는 단지 지연된(dalayed) Observable입니다. 2초후에 true 혹은 false를 리턴합니다.
func signup(username: String, password: String) -> Observable<SignupState> {
// 이 가입 처리는 단지 가짜로 동작하는 겁니다.
let signupResult = SignupState.SignedUp(signedUp: arc4random() % 5 == 0 ? false : true)
return [just(signupResult), never()]
.concat()
.throttle(2, MainScheduler.sharedInstance)
.startWith(SignupState.SigningUp)
}
let signupEnabled = combineLatest(
usernameValidation,
passwordValidation,
repeatPasswordValidation,
signingProcess) { un, p, pr, signingState in
return (un.valid ?? false) && (p.valid ?? false) && (pr.valid ?? false) && signingState != SignupState.SigningUp
}
func bindValidationResultToUI(source: Observable<(valid: Bool?, message: String?)>,
validationErrorLabel: UILabel) {
source
.subscribeNext { v in
let validationColor: UIColor
if let valid = v.valid {
validationColor = valid ? okColor : errorColor
}
else {
validationColor = UIColor.grayColor()
}
validationErrorLabel.textColor = validationColor
validationErrorLabel.text = v.message ?? ""
}
.addDisposableTo(disposeBag)
}
그런 다음 Observer들을 outlet들에 연결합니다.
bindValidationResultToUI(
usernameValidation,
validationErrorLabel: self.usernameValidationOutlet
)
bindValidationResultToUI(
passwordValidation,
validationErrorLabel: self.passwordValidationOutlet
)
bindValidationResultToUI(
repeatPasswordValidation,
validationErrorLabel: self.repeatedPasswordValidationOutlet
)
viennakanon 에 의해서 작성됨.