Welcome to FutureAppLaboratory

v=(*^ワ^*)=v

Learning RxSwift’s Github Sample

| Comments

Introduction

The first example RxSwift mock Github sign-up view controller. It checks availability of user name, password. Then simulate a sign-up process.

Listen User Input

This is easy to do with Rx framework. The rx_text field of UITextField is defined in RxCocoa is just what you want.

1
let username = usernameOutlet.rx_text

It tracks textViewDidChange wrap all changes into Observable, which is an basic event sequence can be Observed by Observer.

1
2
let each user input event as E, then this event sequence can be illustrated as
    ---E---E-E-----E-----

And handling tapping on a button is as easy as

1
let signupSampler = self.signupOutlet.rx_tap

Build Basic Observables

In a common sign-up process, we have to check

  1. User name is empty or includes illegal character
  2. User name has been signed up
  3. Password and Password Repeat are the same
  4. Request can be handled correctly or not

We build 4 observables, i.e. event stream to handle them separately.

1. Check User Name

This is a flatmap function for Observable. (Definition on flatmap can be found in this post. Functor, Monad, Applicative in Swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 be
    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)
}

The wrapped value in Observable is a Bool and String pair.

The first two if clause are for checking empty and illegal characters, respectively. The result will be returned immediately because they are local process.

The last part is a http request which returns result after a short period of time.

If we call ‘validateUsername’ at each event in user input event sequence,

1
2
3
4
let usernameValidation = username
.map { username in
    return validationService.validateUsername(username)
}

Event sequence will become (V for validation, R for result)

1
2
3
4
5
6
7
---+---+-+-----+-----
   |   | |     |
   V   V V     V
   |   | |     |
   R   | |     |
       R |     R
         R

Note that even validations are called in order, results are returned in random order according to network state. And actually we only need the latest validation’s result.

So we use switch method here. switchlatest is one of switch’s implementation, which will always switch to the latest event occurred and dispose former events. Intro_to_rx_switch.

And shareReplay(1) will keep only 1 allocation even this observer gets new subscriptions later. Rxswift_replay

1
2
3
4
5
6
let usernameValidation = username
.map { username in
    return validationService.validateUsername(username)
}
.switchLatest()
.shareReplay(1)

2. Check Password & Repeated Password

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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")
    }
}

Combine these two Observables using combineLatest. It does what exactly it says.

1
2
3
4
let repeatPasswordValidation = combineLatest(password, repeatPassword) { (password, repeatedPassword) in
        validationService.validateRepeatedPassword(password, repeatedPassword: repeatedPassword)
    }
    .shareReplay(1)

3. Signing Process

1
2
3
4
5
6
7
8
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)

The signup method is just a delayed Observable, which return true or false after 2 seconds,

1
2
3
4
5
6
7
8
func signup(username: String, password: String) -> Observable<SignupState> {
    // this is also just a mock
    let signupResult = SignupState.SignedUp(signedUp: arc4random() % 5 == 0 ? false : true)
    return [just(signupResult), never()]
        .concat()
        .throttle(2, MainScheduler.sharedInstance)
        .startWith(SignupState.SigningUp)
}

4. Sign-up Enabled

Just combine validations of user name, password, repeat password

1
2
3
4
5
6
7
8
9
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

}

Bind Observer to UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
}

Then bind Observables to outlets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bindValidationResultToUI(
    usernameValidation,
    validationErrorLabel: self.usernameValidationOutlet
)

bindValidationResultToUI(
    passwordValidation,
    validationErrorLabel: self.passwordValidationOutlet
)

bindValidationResultToUI(
    repeatPasswordValidation,
    validationErrorLabel: self.repeatedPasswordValidationOutlet
)

Comments