대부분의 모든 서비스에서 필요한 로그인을 예제로 들어 RxSwift와 MVVM에 대해 공부해보고자 한다.
snapkit과 then 라이브러리를 활용한 예제가 블로그에 잘 정리되어 있어서 해당 코드를 참고했다.
참고 사이트:
https://pino-day.tistory.com/15
우선, 로그인 및 회원가입에서는 유효성 검증 과정을 필요로 한다.
유효성 검증 과정의 예시로는 이메일과 패스워드 형식이 맞는지, 텍스트 필드에 입력값이 존재하는 지 등이 있겠다.
[ViewModel]
ViewModel은 View를 위한 Model이라는 뜻으로 알고 있다.
즉, 현재 우리는 로그인 기능을 만들고 있으므로, ViewModel에는 로그인 기능에 필요한 비즈니스 로직이 들어가야 한다.
유효성 검증의 예시로 이메일과 패스워드가 들어가 있는지, 형식이 맞는지를 체크하는 로직을 작성해보자.
(코드를 돌려보지 않아서 정확한 코드는 위의 참고 사이트를 참고하시면 되겠습니다)
import RxSwift
import RxRelay
class LoginViewModel {
// 1번 과정
let emailObserver = BehaviorRelay<String>(value: "")
let passwordObserver = BehaviorRelay<String>(value: "")
// 2번 과정
var isValid: Observable<Bool> {
return Observable.combineLatest(emailObserver, passwordObserver)
.map { email, password in
print("이메일 : \(email), password: \(password)")
return !email.isEmpty && email.contains("@") && email.contains(".") && password.count > 0
}
}
}
}
1번 과정:
- View는 Model을 알 수 없기 때문에 View에서 ViewModel로 입력받은 값을 전달해야 함.
- emailObserver에는 emilTextField에서 전달한 값을 View에서 전달해준다.
- passwordObserver에는 passwordTextField에서 전달한 값을 뷰에서 전달해준다.
2번 과정:
- 사용자가 값을 입력할 때마다 emailObserver, passwordObserver의 가장 마지막 값들을 내보내고,
그 값을 이용해 다음을 확인한다.
- emailTextField가 비어있는지
- emailTextField에 @가 포함되어 있는지
- emailTextField에 .이 포함되어 있는지
- 비밀번호가 1자리 이상인지
[View]
위와 같이 ViewModel의 기본적인 로직을 완성했다면, 다음은 View를 구성해야 할 시간이다.
import UIKit
import SnapKit
import Then
import RxSwift
import RxCocoa
class LoginViewController: UIViewController {
let disposeBag = DisposeBag()
let viewModel = LoginViewModel()
let userEmail = "pino-day@test.co.kr"
let userPassword = "test123"
// MARK: - Properties
private let titleLabel = UILabel().then {
$0.textColor = .black
$0.textAlignment = .center
$0.font = UIFont.boldSystemFont(ofSize: 20)
$0.text = "RxSwift & MVVM & Login Validation"
}
private let emailView = UIView().then {
$0.layer.cornerRadius = 15
$0.layer.borderWidth = 1
$0.layer.borderColor = UIColor.lightGray.cgColor
}
private let emailTextField = UITextField().then {
$0.placeholder = "이메일을 입력해주세요"
}
private let passwordView = UIView().then {
$0.layer.cornerRadius = 15
$0.layer.borderWidth = 1
$0.layer.borderColor = UIColor.lightGray.cgColor
}
private let passwordTextField = UITextField().then {
$0.placeholder = "비밀번호를 입력해주세요"
$0.isSecureTextEntry = true
}
private let loginButton = UIButton().then {
$0.setTitle("Login", for: .normal)
$0.titleLabel?.font = UIFont.boldSystemFont(ofSize: 25)
$0.titleLabel?.textColor = .white
$0.backgroundColor = #colorLiteral(red: 0.6, green: 0.8078431373, blue: 0.9803921569, alpha: 1)
$0.layer.cornerRadius = 15
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
configureUI()
setupControl()
}
// MARK: - Helpers
func configureUI() {
view.addSubview(titleLabel)
titleLabel.snp.makeConstraints {
$0.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(150)
$0.centerX.equalToSuperview()
}
view.addSubview(emailView)
emailView.snp.makeConstraints {
$0.top.equalTo(titleLabel.snp.bottom).offset(20)
$0.leading.equalToSuperview().offset(40)
$0.trailing.equalToSuperview().offset(-40)
$0.height.equalTo(50)
}
emailView.addSubview(emailTextField)
emailTextField.snp.makeConstraints {
$0.top.bottom.equalToSuperview()
$0.leading.equalToSuperview().offset(10)
$0.trailing.equalToSuperview().offset(-10)
}
view.addSubview(passwordView)
passwordView.snp.makeConstraints {
$0.top.equalTo(emailView.snp.bottom).offset(10)
$0.leading.equalTo(emailView.snp.leading)
$0.trailing.equalTo(emailView.snp.trailing)
$0.height.equalTo(50)
}
passwordView.addSubview(passwordTextField)
passwordTextField.snp.makeConstraints {
$0.top.bottom.equalToSuperview()
$0.leading.equalToSuperview().offset(10)
$0.trailing.equalToSuperview().offset(-10)
}
view.addSubview(loginButton)
loginButton.snp.makeConstraints {
$0.top.equalTo(passwordView.snp.bottom).offset(50)
$0.leading.trailing.equalTo(emailView)
}
}
func setupControl() {
emailTextField.rx.text
.orEmpty
.bind(to: viewModel.emailObserver)
.disposed(by: disposeBag)
passwordTextField.rx.text
.orEmpty
.bind(to: viewModel.passwordObserver)
.disposed(by: disposeBag)
viewModel.isValid.bind(to: loginButton.rx.isEnabled)
.disposed(by: disposeBag)
viewModel.isValid
.map { $0 ? 1 : 0.3 }
.bind(to: loginButton.rx.alpha)
.disposed(by: disposeBag)
loginButton.rx.tap.subscribe(
onNext: { [weak self] _ in
if self?.userEmail == self?.viewModel.emailObserver.value &&
self?.userPassword == self?.viewModel.passwordObserver.value {
let alert = UIAlertController(title: "로그인 성공", message: "환영합니다", preferredStyle: .alert)
let ok = UIAlertAction(title: "확인", style: .default)
alert.addAction(ok)
self?.present(alert, animated: true, completion: nil)
} else {
let alert = UIAlertController(title: "로그인 실패", message: "아이디 혹은 비밀번호를 다시 확인해주세요", preferredStyle: .alert)
let ok = UIAlertAction(title: "확인", style: .default)
alert.addAction(ok)
self?.present(alert, animated: true, completion: nil)
}
}
).disposed(by: disposeBag)
}
}
여기서 주의 깊게 살펴보아야 할 부분은 setUpControl() 이다.
func setupControl() {
// 1번 과정
emailTextField.rx.text
.orEmpty
.bind(to: viewModel.emailObserver)
.disposed(by: disposeBag)
passwordTextField.rx.text
.orEmpty
.bind(to: viewModel.passwordObserver)
.disposed(by: disposeBag)
// 2번 과정
viewModel.isValid.bind(to: loginButton.rx.isEnabled)
.disposed(by: disposeBag)
// 3번 과정
viewModel.isValid
.map { $0 ? 1 : 0.3 }
.bind(to: loginButton.rx.alpha)
.disposed(by: disposeBag)
// 4번 과정
loginButton.rx.tap.subscribe(
onNext: { [weak self] _ in
if self?.userEmail == self?.viewModel.emailObserver.value &&
self?.userPassword == self?.viewModel.passwordObserver.value {
let alert = UIAlertController(title: "로그인 성공", message: "환영합니다", preferredStyle: .alert)
let ok = UIAlertAction(title: "확인", style: .default)
alert.addAction(ok)
self?.present(alert, animated: true, completion: nil)
} else {
let alert = UIAlertController(title: "로그인 실패", message: "아이디 혹은 비밀번호를 다시 확인해주세요", preferredStyle: .alert)
let ok = UIAlertAction(title: "확인", style: .default)
alert.addAction(ok)
self?.present(alert, animated: true, completion: nil)
}
}
).disposed(by: disposeBag)
}
1번과정:
가장 먼저 emailTextField, passwordTextField에 입력된 값들을 ViewModel로 바인딩 시켜야 한다.
2번과정:
- ViewModel에서 각각 입력한 값을 통해 유효성 검증을 한 결과값(Observable<Bool>)을 로그인 버튼의
isEnabled에 바인딩 시켜준다. -> 만약 isValid의 결과값이 true이면 버튼 활성화 false면 버튼 비활성화.
3번과정:
- 2번 과정에서 활성화, 비활성화를 했지만 시각적인 변화가 없기 때문에 추가적으로 로그인 버튼의 alpha에 바인딩을 시켜줘서
isValid 결과값이 true면 alpha 1, false면 alpha 0.3으로 보이게 했다.
4번과정:
- 마지막으로 실제 서버에서 저장되어 있는 유저의 계정과 유저가 입력한 필드값과 같은지 비교하는 과정이 필요하다.
- 해당 코드에서는 실제 서버가 아닌 가상으로 사용자 계정을 가정했다고 하니 참고 바란다.
- 값을 비교해서 만약 성공이면 성공 alert를, 실패하면 실패 alert를 띄워준다.
'iOS 개발 > RxSwift' 카테고리의 다른 글
[RxSwift] - RxSwift의 개념 (0) | 2023.03.02 |
---|