일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
26 | 27 | 28 | 29 | 30 | 31 |
- NotificationCenter
- 코딩테스트
- EventKit
- Kotlin
- swiftUI
- ios15
- SwiftUI_Preview_Provider
- ViewModifier
- kakaomap
- 백준
- android
- Alert
- SWIFT
- MapKit
- snapkit
- alamofire
- AsyncImage
- segue
- programmers
- UIStackView
- ios
- Appearance변경
- cocoapods
- autolayout
- CoreLocation
- Java
- pod install
- UserDefaults
- image
- format형식
- Today
- Total
감자주먹밥
MVVM - UIKit + Combine 본문
MVVM 아키텍처를 처음 공부했을 땐 RxSwift로 시작을 했었는데, Combine을 알고 나서 UIKit에서 Combine으로 MVVM을 할 수 있지 않을까 라는 생각에 찾아보고 시도해봤다.
유튜브 예제는 통신 예제에 버튼을 누르면 랜덤한 글이 나오게 하는 것이고, 내가 한 건 로그인 화면을 구현한 것이다.
프로젝트 세팅은 Storyboard로 진행했고 Combine을 제외한 라이브러리는 사용하지 않았다.
간단한 로그인 회원가입 화면을 만들고 MVVM을 적용해봤다.
final class SignInViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var signInButton: UIButton!
private let vm = SignViewModel()
private let input: PassthroughSubject<SignViewModel.Input, Never> = .init()
private var bag = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
@IBAction func passwordFieldChanged(_ sender: UITextField) {}
@IBAction func idTextFieldChanged(_ sender: UITextField) {}
@IBAction func signInButtonTapped(_ sender: UIButton) {}
}
먼저 회원가입, 로그인 뷰는 다음과 같이 만들었다. 로그인 화면에 SignUp 버튼은 SignUp 화면으로 넘어가게 해 놨다.
연결한 ViewModel과 연결한 후 이벤트를 보낼 PassthroughSubject를 미리 정의해 놓는다. combine을 사용하면서 Cancellable을 관리하기 위해 Set<AnyCancellable> 타입의 변수를 생성한다. 참고로 AnyCancellable로 wrap된 Cancellable은 사용이 종료되면 자동으로 cancel된다.
ViewModel에는 사용자의 입력이 들어왔을 때 이벤트와, 데이터를 처리해서 화면에 보여줄 때의 이벤트를 정의해야한다.
final class SignViewModel {
enum Input {
case viewApear
case idTextChanged(id: String)
case passwordTextChanged(password: String)
case againTextChanged(again: String)
case signInButtonTapped
case signUpButtonTapped
}
enum Output {
case toggleButton(isEnable: Bool)
case signUpButtonTapped
case signInButtonTapped
}
}
하나의 ViewModel에 회원가입, 로그인 이벤트를 모두 넣었다.
화면이 보여졌을 때 이벤트 부터 텍스트필드 값이 변경될 때, 버튼이 눌렸을 때 이벤트를 Input으로 정의하고
Output으로는 로직을 처리한 후 ViewController에 보낼 이벤트를 정의했다.
Input, Output은 바인딩을 통해 ViewController와 ViewModel을 연결해 줄 것이다.
private var bag = Set<AnyCancellable>()
private let output: PassthroughSubject<Output, Never> = .init()
private var userID: PassthroughSubject<String, Never> = .init()
private var userPassword: PassthroughSubject<String, Never> = .init()
private var againPassword: PassthroughSubject<String, Never> = .init()
func transform(input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never> {
input.receive(on: DispatchQueue.main)
.sink { [weak self] event in
switch event {
case .viewApear(let type):
if type == .signIn {
self?.signInCheck()
} else {
self?.signUpCheck()
}
case .idTextChanged(let id):
self?.userID.send(id)
case .passwordTextChanged(let password):
self?.userPassword.send(password)
case .againTextChanged(let again):
self?.againPassword.send(again)
case .signInButtonTapped:
self?.signInButtonTapped()
case .signUpButtonTapped:
self?.signUpButtonTapped()
}
}
.store(in: &bag)
return output.eraseToAnyPublisher()
}
먼저 transform 함수를 정의한다. (viewApear 이벤트에서
이 함수는 ViewController에서 Input 이벤트를 받았을 때의 동작을 구독하고, ViewModel에서 받을 Output을 Publisher 타입으로 전달한다.
텍스트필드의 값이 변경되면 input으로 Value와 함께 이벤트를 받고 받은 id, password 들을 publisher에 전달 해 놓는다.
변경시 마다 버튼을 활성화 할지 결정하는 코드는 viewApear 이벤트에 signInCheck 메서드에 정의했다.
private func signInCheck() {
output.send(.toggleButton(isEnable: false))
userID.combineLatest(userPassword) { id, password in
if id.count >= 5 && password.count >= 5 {
return true
}
return false
}
.sink { [weak self] isEnable in
self?.output.send(.toggleButton(isEnable: isEnable))
}
.store(in: &bag)
}
viewapear 이벤트를 받으면 먼저 로그인 버튼을 비활성화 한다. id, password가 정상적으로 입력됐을 때만 활성화 할 것이다.
TextChanged 이벤트가 Input으로 들어왔을 때 사용한 Publisher를 구독한다.
combineLatest를 사용해 id와 password가 둘이 같이 값이 들어왔을 때와 최소길이를 만족하는지를 확인해 Bool 타입을 넘긴다.
구독에서는 ViewController가 받을 output이벤트를 날려주면 정상적으로 동작을 한다.
private func bind() {
let output = vm.transform(input: input.eraseToAnyPublisher())
output
.receive(on: DispatchQueue.main)
.sink { [weak self] event in
switch event {
case .toggleButton(let isEnable):
self?.signInButton.isEnabled = isEnable
case .signInButtonTapped:
self?.showAlert()
default: break
}
}
.store(in: &bag)
}
ViewController에 바인딩을 위한 함수를 작성한다. ViewModel에 만든 transform 함수를 사용해 Publisher를 보내고 output을 받는다.
반환받은 output Publisher를 구독하고 들어올 이벤트에 대한 동작을 정의한다. 이렇게 하면 VM <-> VC 양방향 통신을 정의한 것이다.
이렇게 연결된 후 ViewController에서는 input 프로퍼티를 사용해 이벤트가 발생했음을 알려줄 수 있다.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
input.send(.viewApear)
}
물론 bind 함수가 실행 된 후 동작한다.
ViewModel에서는 transform에서 정의 한 것 처럼 input에 이벤트가 send되면 구독한 부분에서 이벤트를 잡아 동작을 하게 된다.
ViewModel에서 output으로 이벤트를 보내는 방법은 ViewController에서 보내는 방법과 똑같다.
private func signInButtonTapped() {
//유저 정보 확인
output.send(.signInButtonTapped)
}
이렇게 이벤트를 보내면 VC에서는 구독해놨던 이벤트를 잡아 Alert를 띄우게 된다.
https://github.com/JustHm/LoginExample-Combine-MVVM
설명이 좀 난리난거 같아 전체 코드도 같이 첨부했다.
'Design Pattern' 카테고리의 다른 글
MVP 알아보기 (간단한 테스트 작성) (0) | 2023.01.27 |
---|---|
MVVM 개념과 MVVM + Rxswift (0) | 2023.01.11 |