본문 바로가기
Swift

[Swift] RxSwift에서 Combine으로 바꿔보기

by yangsubinn 2022. 9. 26.

Combine이 몰까 .. 몰까 ..... 하다가 ..

먼저 RxSwift + MVVM 으로 구현한 뒤 그걸 Combine으로 바꿔보자 ! 라는 생각을 했습니다

 

간략하게 많이들 이런 조합으로 사용하는 MVVM과 RxSwift에 대해 간단하게 보면,,

MVVM

= Model + View + ViewModel

 

MVVM + RxSwift 

위의 조합에서는 사용자에게 보여지는 영역과 그에 대한 반응을 Rx를 사용하여 처리하게 된답니다

대강 흐름을 보면.. 아래와 같이 진행될겁니다.. 🏃‍♀️ 

View에서 이벤트 발생
-> ViewModel에게 input 전달
-> ViewModel에서 그 input에 대한 반응으로 어떤 작업을 수행
-> ViewModel의 output으로 저장
-> View에선 그 output에 대해 응답을 받고 반응

프로젝트 실습 🛠

제가 구현하고자 하는 뷰는 아래와 같습니당

- emailTextField와 pwTextField, LoginButton 존재
- emailTextField와 pwTextField 둘 중 하나라도 빈 값이면 LoginButton 비활성화
- email == "subin"이고, pw = "0000"이면 로그인 성공🎉

 

🥚 RxSwift

고럼 이제 먼저 Rx로 구현을 해보겠습니다

 

1. ViewModelType

import Foundation
import RxSwift

protocol ViewModelType{
  associatedtype Input
  associatedtype Output
  
  func transform(from input: Input, disposeBag: DisposeBag) -> Output
}

 

2. ViewModel

import RxSwift
import RxRelay

class TestViewModel: ViewModelType {
    
    private let disposeBag = DisposeBag()
    
		// input으로 들어올 요소들 - 이메일, 비밀번호 텍스트, 로그인 버튼 탭 여부
    struct Input {
        let email: Observable<String?>
        let password: Observable<String?>
        let tapLogIn: Observable<UserData>
    }
    
		// 로그인 버튼 활성화 여부, 에러 메세지, 로그인 성공 여부
    struct Output {
        let enableLogInButton = BehaviorRelay<Bool>(value: false)
        let errorMessage = PublishRelay<String>()
        let logInSuccess = PublishRelay<Void>()
    }
    
    init() { }
}

extension TestViewModel {
    /// (CleanArchitecture에선) input에 따라 useCase로 연결
    func transform(from input: Input, disposeBag: DisposeBag) -> Output {
        let output = Output()
        
				// 두 개이상의 input을 하나의 observalbe로 합쳐서 사용
        Observable.combineLatest(input.email, input.password)
            .filter { $0.0 != nil && $0.1 != nil }
            .subscribe(onNext: { str in
                if str.0!.count > 0 && str.1!.count > 0 {
                    output.enableLogInButton.accept(true)
                } else {
                    output.enableLogInButton.accept(false)
                }
            })
            .disposed(by: disposeBag)
        
        input.tapLogIn.subscribe(onNext: { data in
            if data.email == "subin" && data.password == "0000" {
                print("로그인 경축")
            } else {
                print("로그인 실패")
            }
        })
        .disposed(by: disposeBag)
        
        return output
    }
}

 

3. ViewController

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    
    // MARK: - @IBOutlet

	...
    
    // MARK: - Properties
    
    private var loginButtonClicked = PublishSubject<UserData>()
    private var emailEditEventFinished = PublishSubject<String?>()
    private var pwEditEvendFinished = PublishSubject<String?>()
    private var disposeBag = DisposeBag()
    var viewModel = TestViewModel()
    
    // MARK: - Life Cycles
		
		...

    // MARK: - Custom Methods
			
		...
    private func setButtonAction() {
        self.loginButton.rx.tap
            .bind {
                let userData = UserData(
                    email: self.emailTextField.text ?? "-",
                    password: self.pwTextField.text ?? "--")
                self.loginButtonClicked.onNext(userData)
            }
            .disposed(by: disposeBag)
    }
    
    // MARK: - @objc
	  ...
}

extension ViewController {
		// viewModel의 input에 VC의 이벤트를 넣어주고, 그에 대한 output을 viewModel에서 가져와서 적용
    private func bindViewModels() {
        let input = TestViewModel.Input(
            email: emailEditEventFinished.asObservable(),
            password: pwEditEvendFinished.asObservable(),
            tapLogIn: loginButtonClicked)
        let output = self.viewModel.transform(from: input, disposeBag: disposeBag)
        
        output.enableLogInButton
            .subscribe(onNext: { [weak self] state in
                guard let self = self else { return }
                self.setButtonState(state)
            })
            .disposed(by: disposeBag)
    }
}

 

🔄 Combine

위에서 Rx로 구현한 프로젝트를 Combine으로 바꿔볼게욥

combine는 다음 페이지를 참고해서 공부했습니당

RxSwift vs Combine - 스펙 / 성능 / 개념 비교

 

1. ViewModelType

combine에서는 DisposeBag이라는 개념이 없기 때문에 삭제해줬습니다.

import Foundation

protocol ViewModelType{
  associatedtype Input
  associatedtype Output
  
  func transform(from input: Input) -> Output
}

 

2. ViewModel

import Combine

class TestViewModel: ViewModelType {
    
		// DisposeBag -> Set<AnyCancellable>
    private var cancelBag = Set<AnyCancellable>()
    
    struct Input {
				// Observable -> AnyPublisher
        let email: AnyPublisher<String?, Error>
        let password: AnyPublisher<String?, Error>
        let tapLogin: AnyPublisher<UserData, Error>
    }
    
    struct Output {
				// BehaviorRelay -> CurrentValueSubject
        let enableLogInButton = CurrentValueSubject<Bool, Error>.init(false)
				// PublishRelay -> PassthoroughSubject
        let errorMessage = PassthroughSubject<String, Error>()
        let logInSuccess = PassthroughSubject<Void, Error>()
    }
    
    init() { }
}

extension TestViewModel {
    // (CleanArchitecture에선) input에 따라 useCase로 연결
    func transform(from input: Input) -> Output {
        let output = Output()
        
				// subscribe -> sink
        input.email.combineLatest(input.password)
            .filter { $0.0 != nil && $0.1 != nil }
            .sink(receiveCompletion: { event in
                print("completion \(event)")
            }, receiveValue: { str in
                if str.0!.count > 0 && str.1!.count > 0 {
                    output.enableLogInButton.send(true)
                } else {
                    output.enableLogInButton.send(false)
                }
            })
            .store(in: &cancelBag) // dispose -> store
        
        input.tapLogin
            .sink(receiveCompletion: { event in
                print("completion \(event)")
            }, receiveValue: { value in
                if value.email == "subin" && value.password == "0000" {
                    print("로그인 경축")
                } else {
                    print("로그인 실패")
                }
            })
            .store(in: &cancelBag)
        
        return output
    }
}

 

3. ViewController

import UIKit
import Combine

class ViewController: UIViewController {
    ...
    
    // MARK: - Properties
    
		// PublishSubject -> PassthroughSubject
    private var loginButtonClicked = PassthroughSubject<UserData, Error>()
    private var emailEditEventFinished = PassthroughSubject<String?, Error>()
    private var pwEditEvendFinished = PassthroughSubject<String?, Error>()
		// DisposeBag -> Set<AnyCancellable>
    private var cancelBag = Set<AnyCancellable>()
    var viewModel = TestViewModel()
    
    // MARK: - Life Cycles

    ...

    // MARK: - Custom Methods

		...
    
    private func addTargets() {
        ...
        
        // combine용 (loginButton.rx.tap 대신 연결용)
        self.loginButton.addTarget(self, action: #selector(setButtonAction), for: .touchUpInside)
    }
    
		...
    
    // MARK: - @objc
    
    @objc func setButtonAction() {
        let userData = UserData(
            email: self.emailTextField.text ?? "-",
            password: self.pwTextField.text ?? "--")
        self.loginButtonClicked.send(userData)
    }
    
...
    
    @objc func textFieldDidChange(_ sender: Any?) {
				// onNext -> send
        self.emailEditEventFinished.send(self.emailTextField.text)
        self.pwEditEvendFinished.send(self.pwTextField.text)
    }
}

extension ViewController {
    private func bindViewModels() {
				
        // asObservable() -> eraseToAnyPublisher()
        let input = TestViewModel.Input(
            email: emailEditEventFinished.eraseToAnyPublisher(),
            password: pwEditEvendFinished.eraseToAnyPublisher(),
            tapLogin: loginButtonClicked.eraseToAnyPublisher())
        let output = self.viewModel.transform(from: input)
        
				// subscribe -> sink
        output.enableLogInButton
            .sink(receiveCompletion: { completion in
                print("completed")
            }, receiveValue: { state in
                self.setButtonState(state)
            })
            .store(in: &cancelBag) // disposed -> store
    }
}

 

깃허브 전체 코드👾

 

[Feat] RxSwift로 구현된 부분 Combine으로 변경 · yangsubinn/Test-iOS@783fd5f

Show file tree Showing 3 changed files with 147 additions and 54 deletions.

github.com

 

 

 

 

'Swift' 카테고리의 다른 글

[Swift] MEMORY LEAK 잡기  (2) 2023.02.17
[RxSwift] 에러 핸들링  (2) 2023.02.09
[Swift] Array 배열  (0) 2022.06.11