아이디, 비밀번호를 입력했는지 감지하는 기능
사용한 라이브러리: UIKit, SnapKit, RxSwift, RxCocoa (storyboard 사용 X)
설명: 아이디 비밀번호를 입력한 경우 버튼 문구가 '로그인'으로 변경된다. 둘 중 하나라도 입력하지 않은 경우 '정보 부족'을 출력한다.
□ View
import UIKit
import SnapKit
import RxSwift
class ViewController: UIViewController {
/*
UI 생성 코드
*/
//ID Label
lazy var IdLabel: UILabel = {
let l1 = UILabel()
l1.font = .systemFont(ofSize: 20)
l1.text = "아이디"
return l1
}()
// PW Label
lazy var pwLabel: UILabel = {
let l1 = UILabel()
l1.font = .systemFont(ofSize: 20)
l1.text = "비밀번호"
return l1
}()
//ID TextField
lazy var idInput: UITextField = {
let idInput = UITextField()
idInput.placeholder = "id를 입력해주세요"
idInput.backgroundColor = .white
idInput.layer.cornerRadius = 3
idInput.font = .systemFont(ofSize: 20)
return idInput
}()
//PW TextField
lazy var pwInput: UITextField = {
let pwInput = UITextField()
pwInput.placeholder = "password를 입력해주세요"
pwInput.backgroundColor = .white
pwInput.font = .systemFont(ofSize: 20)
pwInput.layer.cornerRadius = 3
pwInput.isSecureTextEntry = true //비밀번호 숨기기
return pwInput
}()
// 확인 버튼
lazy var checkBtn1: UIButton = {
let checkBtn1 = UIButton()
checkBtn1.setTitle("정보 부족", for: .normal)
checkBtn1.backgroundColor = .green
checkBtn1.titleLabel?.font = .systemFont(ofSize: 20)
checkBtn1.addTarget(self, action: #selector(checkBtnAction), for: .touchUpInside)
checkBtn1.isEnabled = false
return checkBtn1
}()
// 버튼 눌렀을 때 실행되는 함수
@objc func checkBtnAction(_ sender: Any){
print("clicked button")
}
/*
UI 적용 코드
*/
private func addUI(){
self.view.addSubview(IdLabel)
self.view.addSubview(idInput)
self.view.addSubview(pwLabel)
self.view.addSubview(pwInput)
self.view.addSubview(checkBtn1)
}
/*
AutoLayout 코드
*/
private func addAutoLayout(){
IdLabel.snp.makeConstraints({ make in
make.top.equalTo(100)
make.leading.equalTo(30)
})
pwLabel.snp.makeConstraints({ make in
make.top.equalTo(IdLabel.snp.bottom).offset(10)
make.leading.equalTo(30)
})
idInput.snp.makeConstraints({ make in
make.top.equalTo(100)
make.leading.equalTo(IdLabel).offset(80)
make.trailing.equalTo(-30)
})
pwInput.snp.makeConstraints({ make in
make.top.equalTo(idInput.snp.bottom).offset(10)
make.leading.equalTo(pwLabel).offset(80)
make.trailing.equalTo(-30)
})
checkBtn1.snp.makeConstraints({ make in
make.top.equalTo(pwInput.snp.bottom).offset(50)
make.centerX.equalToSuperview()
})
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .orange
addUI() //UI 추가
addAutoLayout() // AutoLayout 추가
bindInput() // ViewModel 데이터 바인딩하는 함수
bindOutput() // View to ViewModel
}
let disposeBag = DisposeBag()
private let viewModel = ViewModelController()
// 아이디, 비밀번호 textField의 입력을 감지하는 함수
private func bindOutput(){ // View to ViewModel
idInput.rx.text.map({$0 ?? ""}).bind(to: viewModel.id)
.disposed(by: disposeBag)
pwInput.rx.text.map({$0 ?? ""}).bind(to: viewModel.pw)
.disposed(by: disposeBag)
}
// ViewModel에서 가져온 데이터를 UI에 적용
private func bindInput(){ // ViewModel to View
viewModel.checkValidInput.bind(to: checkBtn1.rx.isEnabled).disposed(by: disposeBag)
viewModel.checkValidInput.subscribe(
onNext: { [weak self] isValid in
let text = isValid ? "로그인" : "정보 부족"
self?.checkBtn1.setTitle(text, for: .normal)
})
.disposed(by: disposeBag)
}
}
UI를 코드로 작성하여 구현하니까 iOS 앱 제작하는데 조금 더 깊게 알게 된 기분이다.
AutoLayout을 조금 더 쉽게 적용하기 위해 SnapKit을 사용하였다.
아무튼
이번 핵심인 MVVM패턴을 적용하기 위해서 많이 사용하는 RxSwift를 사용한다( 바인딩 기능을 쉽게 사용할 수 있기 때문! )
bindInput(), bindOutput() 함수 두 개가 MVVM을 구성하는데 전부이다 (허무..)
ViewModelController 클래스 내부에 있는 Subject에 데이터를 넣어 주고 Observable을 이용하여 데이터를 가져와 UI에 적용시키면 된다. ( 간단한 코드 설명은 한 줄이 끝!!)
MVVM 함수가 궁금하면 아래 함수를 누르세요!
// 아이디, 비밀번호 textField의 입력을 감지하는 함수
private func bindOutput(){ // View to ViewModel
idInput.rx.text.map({$0 ?? ""}).bind(to: viewModel.id)
.disposed(by: disposeBag)
pwInput.rx.text.map({$0 ?? ""}).bind(to: viewModel.pw)
.disposed(by: disposeBag)
}
bindOutput이 하는 기능은 주석에 있듯이 textField에 입력을 감지하여 viewModel의 id와 pw라는 subject에게 알려주는 역할이다.
idInput.rx.text.map({$0 ?? ""}) : textField에서 입력을 감지하여 가져온 데이터
. bind(to: viewModel.id) : . 이전에 데이터를 viewModel.id에 전달
간단하죠?
// ViewModel에서 가져온 데이터를 UI에 적용
private func bindInput(){ // ViewModel to View
viewModel.checkValidInput.bind(to: checkBtn1.rx.isEnabled).disposed(by: disposeBag)
viewModel.checkValidInput.subscribe(
onNext: { [weak self] isValid in
let text = isValid ? "로그인" : "정보 부족"
self?.checkBtn1.setTitle(text, for: .normal)
})
.disposed(by: disposeBag)
}
bindOutput() 함수에서 bind관련된 설명 참고하세요!
ViewModel에 있는 Observable에서 데이터를 가져오는 방법으로 subscribe가 있다.
subscribe를 사용하여 데이터를 가져온 뒤 stream 방식으로 데이터를 처리하면 된다.
※ weak self를 사용하는 이유
순환 참조를 방지하여 메모리 누수를 막기 위해 사용한다.
(이 부분이 이해가 안 된다면 ARC 메모리 관리 방법을 공부하세요!)
□ViewModel
import Foundation
import RxSwift
import RxCocoa
class ViewModelController {
var id: BehaviorSubject<String> = BehaviorSubject(value: "")
var pw: BehaviorSubject<String> = BehaviorSubject(value: "")
var checkValidId: Observable<Bool>{ id.map({ print("\($0)"); return $0.isEmpty ? false : true }) }
var checkValidpw: Observable<Bool>{ pw.map({ print("\($0)"); return $0.isEmpty ? false : true }) }
var checkValidInput: Observable<Bool> {
return Observable.combineLatest(checkValidId,checkValidpw).map({$0 && $1})
}
}
View에 비해 코드가 약간 초라.. 하네...?
BehaviorSubject(최근 데이터를 알려주는 subject)를 이용하여 View에서 데이터를 가져온다.
데이터를 가공한 후 checkValidInput이라는 Observable을 생성하여 Observable 타입의 두 개의 데이터를 비교하여 Observable에 저장한다.
이후 View에서 해당 데이터를 바인딩한다.
# 참고한 사이트