Skip to content

Commit

Permalink
Refactoring RegistrationViewController. Combine을 이용해 MVVM으로 분리.
Browse files Browse the repository at this point in the history
  • Loading branch information
SHcommit committed Dec 8, 2022
1 parent 9133be3 commit e2ff944
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 108 deletions.
Expand Up @@ -6,6 +6,7 @@
//

import UIKit
import Combine

/**
### TODO: 회원가입 기능 combined 적용하기
Expand All @@ -19,22 +20,26 @@ class RegistrationController: UIViewController, UINavigationControllerDelegate {
//MARK: - Properties
private lazy var photoButton: UIButton = initialPhotoButton()
private lazy var userInputStackView: UIStackView = initialUserInputStackView()
private var emailTextField: CustomTextField = initialEmailTextField()
private var passwordTextField: CustomTextField = initialPasswordTextField()
private var fullnameTextField: CustomTextField = initialFullnameTextField()
private var usernameTextField: CustomTextField = initialUsernameTextField()
private var emailTextField: UITextField = initialEmailTextField()
private var passwordTextField: UITextField = initialPasswordTextField()
private var fullnameTextField: UITextField = initialFullnameTextField()
private var usernameTextField: UITextField = initialUsernameTextField()
private lazy var signUpButton: LoginButton = initialSignUpButton()
private var readyLogInLineStackView: UIStackView = initialReadyLogInLineStackView()
private var indicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium)
private var viewModel = RegistrationViewModel()
private var subscriptions: Set<AnyCancellable> = Set<AnyCancellable>()
private var appear = PassthroughSubject<Void,Never>()
private var signUpTap = PassthroughSubject<UINavigationController?,Never>()

private var vm = RegistrationViewModel()

//MARK: - Lifecycle

override func viewDidLoad() {
super.viewDidLoad()

setupUI()
setupLabelsBinding()
setupBinding()
}

}
Expand Down Expand Up @@ -63,24 +68,39 @@ extension RegistrationController {
setupReadyLogInLineStackViewConstraints()
}

func setupLabelsBinding() {
emailTextField.bind { [weak self] text in
self?.vm.email.value = text
self?.changeValidTextFields()
}
passwordTextField.bind { [weak self] text in
self?.vm.password.value = text
self?.changeValidTextFields()
}
fullnameTextField.bind { [weak self] text in
self?.vm.fullname.value = text
self?.changeValidTextFields()
}
usernameTextField.bind { [weak self] text in
self?.vm.username.value = text
self?.changeValidTextFields()
}
func setupBinding() {

let input = RegistrationViewModelInput(appear: appear.eraseToAnyPublisher(),
signUpTap: signUpTap.eraseToAnyPublisher())

viewModel.bind(with: input)

CombineUtils.textfieldNotificationPublisher(withTF: emailTextField)
.receive(on: DispatchQueue.main)
.sink { [unowned self] text in
viewModel.email = text
}.store(in: &subscriptions)
CombineUtils.textfieldNotificationPublisher(withTF: passwordTextField)
.receive(on: RunLoop.main)
.sink { [unowned self] text in
viewModel.password = text
}.store(in: &subscriptions)
CombineUtils.textfieldNotificationPublisher(withTF: fullnameTextField)
.receive(on: RunLoop.main)
.sink { [unowned self] text in
viewModel.fullname = text
}.store(in: &subscriptions)
CombineUtils.textfieldNotificationPublisher(withTF: usernameTextField)
.receive(on: RunLoop.main)
.sink { [unowned self] text in
viewModel.username = text
}.store(in: &subscriptions)

viewModel.isValidUserForm()
.receive(on: RunLoop.main)
.sink { [unowned self] isValid in
viewModel.checkIsValidTextFields(isValid: isValid, button: signUpButton)
}.store(in: &subscriptions)
}

func updatePhotoButtonState(_ image: UIImage) {
Expand All @@ -91,22 +111,6 @@ extension RegistrationController {
photoButton.clipsToBounds = true
dismiss(animated: true)
}

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

}

Expand All @@ -118,14 +122,7 @@ extension RegistrationController {
}

@objc func didTapSignUpButton(_ sender: Any) {
startIndicator(indicator: indicator)
Task() {
do {
try await registerUserFromSignUp()
}catch {
registerUserFromSignUpErrorHandling(error: error)
}
}
signUpTap.send(navigationController)
}

@objc func didTapPhotoButton(_ sender: Any) {
Expand All @@ -143,38 +140,12 @@ extension RegistrationController: UIImagePickerControllerDelegate {

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let selectedImage = info[.editedImage] as? UIImage else { return }
vm.profileImage = selectedImage
viewModel.profileImage = selectedImage
updatePhotoButtonState(selectedImage)

}
}


//MARK: - API
extension RegistrationController {
func registerUserFromSignUp() async throws {
try await AuthService.registerUser(withUserInfo: vm)
DispatchQueue.main.async {
self.endIndicator(indicator: self.indicator)
self.navigationController?.popViewController(animated: true)
}
}
func registerUserFromSignUpErrorHandling(error: Error) {
switch error {
case AuthError.badImage:
print("DEBUG: Failure bind registerUser's info.profileImage")
case AuthError.invalidUserAccount:
print("DEBUG: Failure create user account")
case AuthError.invalidSetUserDataOnFireStore:
print("DEBUG: Failure add user Info in firestore")
default:
print("DEBUG: Unexcept error occured: \(error.localizedDescription)")
}
}
}



//MARK: - Initial subviews
extension RegistrationController {

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

import Foundation
import UIKit
import Combine

struct RegistrationViewModelInput {

/// Emit ViewWilAppear event to viewModel
let appear: AnyPublisher<Void,Never>

/// Emit signUp event to viewModel when user did tap SignUp button.
let signUpTap: AnyPublisher<UINavigationController?,Never>

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

import Foundation
import UIKit
import Combine

protocol RegistrationViewModelType {

/// 사용자의 특정 event를 input 타입으로 분리. RegistrationViewController로 부터 특정 event publish.
func bind(with input: RegistrationViewModelInput)

/// Return UserInfoModel
func getUserInfoModel(uid: String, url: String) -> UserInfoModel
}


protocol RegistrationViewModelUserFormType {

func isValidUserForm() -> AnyPublisher<Bool,Never>

/// Check all TextField is not empty with zip operation :)
func checkIsValidTextFields(isValid: Bool, button: UIButton)

/// Button's isEnable true. when isValidUserForm isValidUserForm publish true
func validUserForm(with button: UIButton)

/// Button's isEnable false. when isValidUserForm isValidUserForm publish false
func notValidUserForm(with button: UIButton)

}

protocol RegistrationViewModelNetworkServiceType {

//MARK: APIs
/// Wrapper func in AuthService.registerUser(withUserInfo:)
func registerUserFromSignUp() async throws

/// Register User form from async func registerUserFormSignUp()
func registerUser()

//MARK: - API error handling
/// Error handling from registerUserFormSIgnUp func
func registerUserFromSignUpErrorHandling(error: Error)

}
Expand Up @@ -62,7 +62,7 @@ extension LoginViewModel: LoginViewModelType {
}

//MARK: - LoginViewModelAPIType
extension LoginViewModel: LoginViewModelAPIType {
extension LoginViewModel: LoginViewModelNetworkServiceType {

func loginInputAccount(mainHomeTab vc: MainHomeTabController) {
Task() {
Expand Down
Expand Up @@ -6,53 +6,105 @@
//

import UIKit

import Combine

class RegistrationViewModel {

//MARK: - Properties
var email = Dynamic("")
var password = Dynamic("")
var fullname = Dynamic("")
var username = Dynamic("")
var profileImage: UIImage?

//MARK: - Helpers
var isValiedUserForm: Bool {
get {
return !(email.value.isEmpty) && !(password.value.isEmpty)
&& !(fullname.value.isEmpty) && !(username.value.isEmpty)
}
}
@Published var email: String = ""
@Published var password: String = ""
@Published var fullname: String = ""
@Published var username: String = ""
@Published var profileImage: UIImage? = UIImage()
var subscriptions: Set<AnyCancellable> = Set<AnyCancellable>()

}

//MARK: - RegistrationViewModelType
extension RegistrationViewModel: RegistrationViewModelType {


func getUserInfoModel(uid: String, url: String) -> UserInfoModel {
return UserInfoModel(email: email.value,
fullname: fullname.value,
profileURL: url,
uid: uid,
username: username.value)
return UserInfoModel(email: email, fullname: fullname,
profileURL: url, uid: uid,
username: username)
}

func bind(with input: RegistrationViewModelInput) {
input
.signUpTap
.receive(on: RunLoop.main)
.sink { [unowned self] navigationController in
navigationController?.startIndicator(indicator: indicator)
registerUser()
navigationController?.endIndicator(indicator: indicator)
navigationController?.popViewController(animated: true)
}.store(in: &subscriptions)

}

}

//MARK: - RegistrationViewModelUserFormType
extension RegistrationViewModel: RegistrationViewModelUserFormType {

func isValidUserForm() -> AnyPublisher<Bool, Never> {
$email
.zip($fullname, $username, $password)
.map { (emailText, fullnameText, usernameText, passwordText) in
return !emailText.isEmpty && !fullnameText.isEmpty && !usernameText.isEmpty && !passwordText.isEmpty
}
.eraseToAnyPublisher()
}

func checkIsValidTextFields(isValid: Bool, button: UIButton) {
isValid ? validUserForm(with: button) : notValidUserForm(with: button)
}

func validUserForm(with button: UIButton) {
button.isEnabled = true
button.backgroundColor = UIColor.systemPink.withAlphaComponent(0.6)
button.titleLabel?.textColor.withAlphaComponent(1)
}

func notValidUserForm(with button: UIButton) {
button.isEnabled = false
button.backgroundColor = UIColor.systemPink.withAlphaComponent(0.3)
button.titleLabel?.textColor.withAlphaComponent(0.2)
}

}

// T value가 변할 때마다 listener?(value) 클로저를 실행한다.
// 이때 이 값을 TextField UI에 갱신할 것이다.
class Dynamic<T> {
typealias Listener = (T) -> Void
var listener: Listener?
var value: T {
didSet {
listener?(value)
//MARK: - RegistrationViewModelNetworkServiceType
extension RegistrationViewModel: RegistrationViewModelNetworkServiceType {

func registerUser() {
Task() { [weak self] in
do {
try await self?.registerUserFromSignUp()
} catch {
self?.registerUserFromSignUpErrorHandling(error: error)
}
}
}

func bind(callback: @escaping Listener) {
listener = callback

func registerUserFromSignUp() async throws {
try await AuthService.registerUser(withUserInfo: self)
}

init(_ value: T) {
self.value = value
func registerUserFromSignUpErrorHandling(error: Error) {
switch error {
case AuthError.badImage:
print("DEBUG: Failure bind registerUser's info.profileImage")
case AuthError.invalidUserAccount:
print("DEBUG: Failure create user account")
case AuthError.invalidSetUserDataOnFireStore:
print("DEBUG: Failure add user Info in firestore")
default:
print("DEBUG: Unexcept error occured: \(error.localizedDescription)")
}

}

}

0 comments on commit e2ff944

Please sign in to comment.