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
}
}
'Swift' 카테고리의 다른 글
[Swift] MEMORY LEAK 잡기 (2) | 2023.02.17 |
---|---|
[RxSwift] 에러 핸들링 (2) | 2023.02.09 |
[Swift] Array 배열 (0) | 2022.06.11 |