본문 바로가기

공부/swift

[swift] RxCocoa

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