Skip to content

Commit

Permalink
LoginController에서 컴바인을 통해 ViewModel로 분리. 가독성, 클린코드를 고려한 리펙터링.
Browse files Browse the repository at this point in the history
  • Loading branch information
SHcommit committed Dec 7, 2022
1 parent b1d963f commit 93ce1b9
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 74 deletions.
Expand Up @@ -7,40 +7,30 @@

import UIKit
import Firebase

/**
이번에 할거는 LoginController를 MVVM으로 리펙터링 할 것이다. 물론 로그인 컨트롤과 연관된 서비스도 리펙터링
*/

protocol AuthentificationDelegate: class {
func authenticationCompletion(uid: String) async
}
import Combine

class LoginController: UIViewController {

//MARK: - Properties
weak var authDelegate: AuthentificationDelegate?
private let instagramIcon: UIImageView = initialInstagramIcon()
private lazy var emailTextField: CustomTextField = initialEmailTextField()
private lazy var passwdTextField: CustomTextField = initialPasswdTextField()
private lazy var emailTextField: UITextField = initialEmailTextField()
private lazy var passwdTextField: UITextField = initialPasswdTextField()
private lazy var loginButton: LoginButton = initialLoginButton()
private lazy var forgotHelpLineStackView: UIStackView = initialForgotStackView()
private lazy var signUpLineStackView: UIStackView = initialSignUpLineStackView()
private var indicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium)

private var vm = LoginViewModel()
var viewModel: LoginViewModel = LoginViewModel()
private var subscriptions: Set<AnyCancellable> = Set<AnyCancellable>()
private var tapLogin: PassthroughSubject<UIViewController,Never> = PassthroughSubject<UIViewController,Never>()

//MARK: - Life cycle

override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupTextFieldBindingByViewModel()
setupBindings()
}
}


//MARK: - View helpers
extension LoginController {

Expand All @@ -67,23 +57,8 @@ extension LoginController {

@objc func didTapLoginButton(_ sender: Any) {
startIndicator(indicator: indicator)
let email = vm.email.value
let pw = vm.password.value
guard let vc = self.presentingViewController as? MainHomeTabController else {
return
}
Task() {
do {
guard let authDataResult = try await AuthService.handleIsLoginAccount(email: email, pw: pw) else { throw FetchUserError.invalidUserInfo }
endIndicator(indicator: indicator)
vc.view.isHidden = false
await authDelegate?.authenticationCompletion(uid: authDataResult.user.uid)
}catch FetchUserError.invalidUserInfo {
print("DEBUG: Fail to bind userInfo")
}catch {
print("DEBUG: Failure an occured error: \(error.localizedDescription) ")
}
}
guard let presentingViewController = presentingViewController else { return }
tapLogin.send(presentingViewController)
}

@objc func didTapHelpButton(_ sender: Any) {
Expand All @@ -97,40 +72,46 @@ extension LoginController {

}


//MARK: - Helpers
extension LoginController {

//vm의 값이 바뀌면 현재 연결된 UITextField의 쳐져있는 값도 변경.
func setupTextFieldBindingByViewModel() {
vm.email.bind{ [weak self] text in
self?.emailTextField.text = text
}
vm.password.bind{ [weak self] text in
self?.passwdTextField.text = text
}
}

func changeValidTextFields() {
if vm.isValiedUserForm {
DispatchQueue.main.async {
self.loginButton.isEnabled = true
self.loginButton.backgroundColor = UIColor.systemPink.withAlphaComponent(0.6)
self.loginButton.titleLabel?.textColor.withAlphaComponent(1)
}
} else {
DispatchQueue.main.async {
self.loginButton.isEnabled = false
self.loginButton.backgroundColor = UIColor.systemPink.withAlphaComponent(0.3)
self.loginButton.titleLabel?.textColor.withAlphaComponent(0.2)
}
}
func setupBindings() {

subscriptions.forEach { $0.cancel() }
subscriptions.removeAll()

let input = LoginViewModelInput(tapLogin: tapLogin.eraseToAnyPublisher())
viewModel.login(with: input)

textfieldNotificationPublisher(withTF: emailTextField)
.receive(on: RunLoop.main)
.sink { [unowned self] text in
viewModel.email = text
viewModel.checkIsValidTextFields(withLogin: loginButton)
}.store(in: &subscriptions)

textfieldNotificationPublisher(withTF: passwdTextField)
.receive(on: RunLoop.main)
.sink { [unowned self] text in
viewModel.passwd = text
viewModel.checkIsValidTextFields(withLogin: loginButton)
}.store(in: &subscriptions)

}

func textfieldNotificationPublisher(withTF textField: UITextField) -> AnyPublisher<String,NotificationCenter.Publisher.Failure> {
return NotificationCenter.default
.publisher(for: UITextField.textDidChangeNotification,object: textField)
.map {
guard let text = ($0.object as? UITextField)?.text else {
return ""
}
return text
}.eraseToAnyPublisher()
}

}



//MARK: - Setup Navigation
extension LoginController {
func setupNavigationAppearance() {
Expand All @@ -157,30 +138,20 @@ extension LoginController {
let iv = UIImageView()
iv.translatesAutoresizingMaskIntoConstraints = false
iv.image = .imageLiteral(name: "Instagram_logo_white")

return iv
}

func initialEmailTextField() -> CustomTextField {
let tf = CustomTextField(placeHolder: "Email")
tf.keyboardType = .emailAddress
tf.setHeight(50)
tf.bind{ [weak self] text in
self?.vm.email.value = text
self?.changeValidTextFields()
}
return tf
}

func initialPasswdTextField() -> CustomTextField {
let tf = CustomTextField(placeHolder: "Password")
tf.isSecureTextEntry = true
tf.setHeight(50)
tf.bind { [weak self] text in
self?.vm.password.value = text
self?.changeValidTextFields()
}

return tf
}

Expand Down
Expand Up @@ -5,4 +5,11 @@
// Created by 양승현 on 2022/12/08.
//

import Foundation
import UIKit
import Combine


///define UI events
struct LoginViewModelInput {
var tapLogin: AnyPublisher<UIViewController,Never>
}
Expand Up @@ -5,4 +5,37 @@
// Created by 양승현 on 2022/12/08.
//

import Foundation
import UIKit
import Combine

protocol AuthentificationDelegate: class {

/// 회원이라면 메인화면으로 돌아간다.
/// 이때 Firebase 에서 지원하는 Auth.auth().currentUser는 잘 동작하지 않는다.
/// 초기에는 동작하지만 중간에 사용자가 로그아웃 후 다른 계정으로 로그인 할 경우 currentuser의 캐시가 변경되지 않는다고 한다.
/// 그래서 영구 저장소에 저장하고 갱신하기 위해 로그인된 유저의 uid를 따로 반환한다.
func authenticationCompletion(uid: String) async

}


protocol LoginViewModelType {

/// 사용자가 로그인, 비밀번호 칸을 두개 다 입력 했는지 체크 여부 반환한다.
/// 추후 비밀번호 최소 입력 개수를 제한하고 알림창을 띄우는 기능을 추가할 것이다.
func isValidUserForm() -> AnyPublisher<Bool,Never>

/// 입력한 email, pw 두 userForm을 통해 회원인지 아닌지 확인한다.
func checkIsValidTextFields(withLogin button: UIButton)

/// LoginController에서 ViewModel로 login 입력이 들어온다.
func login(with input: LoginViewModelInput)

}

protocol LoginViewModelAPIType {

/// Async, Await을 통해 회원 여부 판단을 request하는 server api관련 wrapper func.
func loginInputAccount(mainHomeTab vc: MainHomeTabController)

}
Expand Up @@ -5,4 +5,77 @@
// Created by 양승현 on 2022/12/07.
//

import Foundation
import UIKit
import Combine

final class LoginViewModel {

//MARK: - Properties
weak var authDelegate: AuthentificationDelegate?
@Published var email: String = ""
@Published var passwd: String = ""
var subscriptions: Set<AnyCancellable> = Set<AnyCancellable>()

}

//MARK: - LoginViewModelType
extension LoginViewModel: LoginViewModelType {

func isValidUserForm() -> AnyPublisher<Bool, Never> {
$email
.zip($passwd)
.map { (emailText, passwdText) in
return !emailText.isEmpty && !passwdText.isEmpty
}.eraseToAnyPublisher()
}

func checkIsValidTextFields(withLogin button: UIButton) {
isValidUserForm()
.receive(on: RunLoop.main)
.sink{ isValied in
if isValied {
button.isEnabled = true
button.backgroundColor = UIColor.systemPink.withAlphaComponent(0.6)
button.titleLabel?.textColor.withAlphaComponent(1)
} else {
button.isEnabled = false
button.backgroundColor = UIColor.systemPink.withAlphaComponent(0.3)
button.titleLabel?.textColor.withAlphaComponent(0.2)
}
}.store(in: &subscriptions)
}

func login(with input: LoginViewModelInput) {
subscriptions.forEach{ $0.cancel() }
subscriptions.removeAll()

input
.tapLogin
.receive(on: RunLoop.main)
.sink { [unowned self] presentingVC in
guard let vc = presentingVC as? MainHomeTabController else { return }
vc.view.isHidden = false
loginInputAccount(mainHomeTab: vc)
}.store(in: &subscriptions)
}

}

//MARK: - LoginViewModelAPIType
extension LoginViewModel: LoginViewModelAPIType {

func loginInputAccount(mainHomeTab vc: MainHomeTabController) {
Task() {
do {
guard let authDataResult = try await AuthService.handleIsLoginAccount(email: email, pw: passwd) else { throw FetchUserError.invalidUserInfo }
await authDelegate?.authenticationCompletion(uid: authDataResult.user.uid)
await vc.endIndicator(indicator: indicator)
}catch FetchUserError.invalidUserInfo {
print("DEBUG: Fail to bind userInfo")
}catch {
print("DEBUG: Failure an occured error: \(error.localizedDescription) ")
}
}
}

}

0 comments on commit 93ce1b9

Please sign in to comment.