Swift/RxSwift

ReactorKit 으로 로그인 페이지 만들기

iosswift 2022. 10. 14. 01:58

Login Controller

//
//  ViewController.swift
//  ReactorPractice
//
//  Created by Mac mini on 2022/10/13.
//

import Foundation
import UIKit
import RxSwift
import RxCocoa
import Then
import ReactorKit
import RxViewController
import RxGesture
import SnapKit

class ViewController: UIViewController, View {
    
    internal var disposeBag = DisposeBag()
    
    private let behaviorRelay = BehaviorRelay<Void>(value: ())
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        setupLayout()
    }
    
    init(reactor: LoginReactor) {
        super.init(nibName: nil, bundle: nil)
        self.reactor = reactor
    }
    
    func bind(reactor: LoginReactor) {
        
        // MARK: -  View -> Reactor
        
        // initialize state when viewWillAppear called
        self.rx.viewWillAppear
            .map { _ in Reactor.Action.initialize }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

        emailTF.rx.text.orEmpty
            .map { Reactor.Action.typeEmail($0)}
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        passwordTF.rx.text.orEmpty
            .map { Reactor.Action.typePassword($0)}
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        loginBtn.rx.tap
            .map { Reactor.Action.login }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        
        // MARK: - Reactor -> View
        reactor.state.map { $0.email }
            .distinctUntilChanged()
            .bind(to: emailTF.rx.text )
            .disposed(by: disposeBag)

        reactor.state.map { $0.password }
            .distinctUntilChanged()
            .bind(to: passwordTF.rx.text )
            .disposed(by: disposeBag)
        
        
        // spinner
        reactor.state.map { $0.isSpinnerRunning }
            .distinctUntilChanged()
            .subscribe(onNext: { [weak self] shouldRun in
                guard let self = self else { return }
                shouldRun ? self.spinner.startAnimating() : self.spinner.stopAnimating()
            })
            .disposed(by: disposeBag)
        
        // login
        reactor.state.map { $0.shouldLogin }
            .distinctUntilChanged()
            .subscribe(onNext: { [weak self] shouldLogin in
                guard let self = self else { return }
                if shouldLogin {
                    self.moveToNextPage()
                }
            })
            .disposed(by: disposeBag)
        
        // hide keyboard when empty view tapped
        self.view.rx.tapGesture()
            .subscribe(onNext: { [weak self] _ in
                self?.hideKeyboard()
            })
            .disposed(by: disposeBag)
    }
    
    private func hideKeyboard() {
        self.view.endEditing(true)
    }
    
    private func setupLayout() {
        [
         titleLabel,
         emailTF, passwordTF,
         loginBtn, spinner
        ].forEach { self.view.addSubview($0)}
        
        titleLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(200)
            make.centerX.equalToSuperview()
            make.width.equalToSuperview()
            make.height.equalTo(50)
        }
        
        emailTF.snp.makeConstraints { make in
            make.top.equalTo(titleLabel.snp.bottom).offset(36)
            make.leading.equalToSuperview().inset(48)
            make.trailing.equalToSuperview().inset(48)
            make.height.equalTo(56)
        }
        
        passwordTF.snp.makeConstraints { make in
            make.top.equalTo(emailTF.snp.bottom).offset(24)
            make.leading.equalToSuperview().inset(48)
            make.trailing.equalToSuperview().inset(48)
            make.height.equalTo(56)
        }
        
        loginBtn.snp.makeConstraints { make in
            make.bottom.equalToSuperview().inset(70)
            make.height.equalTo(56)
            make.leading.equalToSuperview().inset(48)
            make.trailing.equalToSuperview().inset(48)
        }
        
        spinner.transform = CGAffineTransform(scaleX: 2, y: 2)
        
        spinner.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.width.height.equalTo(100)
        }
    }
    
    
    private func moveToNextPage() {
        let secondController = SecondViewController()
        navigationController?.pushViewController(secondController, animated: true)
    }
    
    
    private let titleLabel = UILabel().then {
        $0.textColor = .black
        $0.text = "Login Page"
        $0.textAlignment = .center
        $0.font = UIFont.systemFont(ofSize: 20)
        $0.numberOfLines = 2
    }
    
    private let emailTF = UITextField().then {
        $0.placeholder = "E-mail"
        $0.backgroundColor = .gray
        $0.keyboardType = .emailAddress
        $0.autocapitalizationType = .none
        $0.autocorrectionType = .no
    }
    
    private let passwordTF = UITextField().then {
        $0.placeholder = "Password"
        $0.backgroundColor = .gray
        $0.isSecureTextEntry = true
    }
    
    private let loginBtn = UIButton().then {
        $0.setTitle("Login", for: .normal)
        $0.setTitleColor(.white, for: .normal)
        $0.backgroundColor = .red
        $0.layer.cornerRadius = 8
    }
    
    var spinner = UIActivityIndicatorView().then {
        $0.hidesWhenStopped = true
        $0.color = .magenta
    }

    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

 

 

LoginReactor

//
//  LoginReactor.swift
//  ReactorPractice
//
//  Created by Mac mini on 2022/10/13.
//

import Foundation
import RxCocoa
import RxSwift
import ReactorKit


// TODO: make UserService.

class LoginReactor: Reactor {

    private let tempId = "id@gmail.com"
    private let tempPassword = "password!"
    
    public enum Action {
        case login
        case typeEmail(String)
        case typePassword(String)
        case initialize
    }

    public enum Mutation {
        case login
        case updateEmail(String)
        case updatePassword(String)
        case shouldShowSpinner(Bool)
        case shouldInitialize
    }

    public struct State {
        
        var email: String
        var password: String
        var isSpinnerRunning: Bool
        var shouldLogin: Bool
        var errorMsg: String
        
        init() {
            self.email = ""
            self.password = ""
            self.isSpinnerRunning = false
            self.shouldLogin = false
            self.errorMsg = ""
        }
    }
    
    public var initialState: State = State()

    func mutate(action: Action) -> Observable<Mutation> {
        switch action {

        case .login:
            return Observable.concat([
                Observable.just(Mutation.shouldShowSpinner(true)),
                // delay 2 sec to show spinner clearly
                Observable.just(Mutation.login).delay(RxTimeInterval.seconds(2), scheduler: MainScheduler.instance),
                
                Observable.just(Mutation.shouldShowSpinner(false))
            ])
            
        case .typeEmail(let email):
            return .just(.updateEmail(email))
            
        case .typePassword(let password):
            return .just(.updatePassword(password))
            
        case .initialize:
            return .just(.shouldInitialize)
        }
    }

    func reduce(state: State, mutation: Mutation) -> State {
        var state = state

        switch mutation {
            
        case .login:
            if self.tempId == state.email && self.tempPassword == state.password {
                state.shouldLogin = true
            }
            
        case .updateEmail(let email):
            state.email = email
            
        case .updatePassword(let password):
            state.password = password

        case .shouldShowSpinner(let shouldShow):
            state.isSpinnerRunning = shouldShow

            if shouldShow == false {
                state.shouldLogin = false
            }

        case .shouldInitialize:
            state = State()
        }

        return state
    }
    
    
    
    deinit {
        print("loginReactor deinit")
    }
    
    public init() {
        print("loginReactor initialized.")
    }
}