diff --git a/Clone App/Instagram/Instagram/Controller/Authentication/RegistrationController.swift b/Clone App/Instagram/Instagram/Controller/Authentication/RegistrationController.swift index 6e54d5cf..49174fac 100644 --- a/Clone App/Instagram/Instagram/Controller/Authentication/RegistrationController.swift +++ b/Clone App/Instagram/Instagram/Controller/Authentication/RegistrationController.swift @@ -6,6 +6,7 @@ // import UIKit +import Combine /** ### TODO: 회원가입 기능 combined 적용하기 @@ -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 = Set() + private var appear = PassthroughSubject() + private var signUpTap = PassthroughSubject() - private var vm = RegistrationViewModel() + //MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() setupUI() - setupLabelsBinding() + setupBinding() } } @@ -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) { @@ -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) - } - } - } } @@ -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) { @@ -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 { diff --git a/Clone App/Instagram/Instagram/Model/RegistrationViewModelInput.swift b/Clone App/Instagram/Instagram/Model/RegistrationViewModelInput.swift index 12d4702d..c9f303ff 100644 --- a/Clone App/Instagram/Instagram/Model/RegistrationViewModelInput.swift +++ b/Clone App/Instagram/Instagram/Model/RegistrationViewModelInput.swift @@ -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 + + /// Emit signUp event to viewModel when user did tap SignUp button. + let signUpTap: AnyPublisher + +} diff --git a/Clone App/Instagram/Instagram/Protocols/RegistrationViewModelType.swift b/Clone App/Instagram/Instagram/Protocols/RegistrationViewModelType.swift index a501cf03..476cb830 100644 --- a/Clone App/Instagram/Instagram/Protocols/RegistrationViewModelType.swift +++ b/Clone App/Instagram/Instagram/Protocols/RegistrationViewModelType.swift @@ -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 + + /// 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) + +} diff --git a/Clone App/Instagram/Instagram/ViewModel/Authentification/LoginViewModel.swift b/Clone App/Instagram/Instagram/ViewModel/Authentification/LoginViewModel.swift index f61e4883..89df7840 100644 --- a/Clone App/Instagram/Instagram/ViewModel/Authentification/LoginViewModel.swift +++ b/Clone App/Instagram/Instagram/ViewModel/Authentification/LoginViewModel.swift @@ -62,7 +62,7 @@ extension LoginViewModel: LoginViewModelType { } //MARK: - LoginViewModelAPIType -extension LoginViewModel: LoginViewModelAPIType { +extension LoginViewModel: LoginViewModelNetworkServiceType { func loginInputAccount(mainHomeTab vc: MainHomeTabController) { Task() { diff --git a/Clone App/Instagram/Instagram/ViewModel/Authentification/RegistrationViewModel.swift b/Clone App/Instagram/Instagram/ViewModel/Authentification/RegistrationViewModel.swift index 80de722e..80959605 100644 --- a/Clone App/Instagram/Instagram/ViewModel/Authentification/RegistrationViewModel.swift +++ b/Clone App/Instagram/Instagram/ViewModel/Authentification/RegistrationViewModel.swift @@ -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 = Set() + +} + +//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 { + $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 { - 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)") + } + } }