🟨 구현 화면
🟨 구현 기능
1️⃣ Signout 기능
- HomeViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
...
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "rectangle.portrait.and.arrow.right"), style: .plain, target: self, action: #selector(didTapSignOut))
}
// 로그아웃 기능
@objc private func didTapSignOut() {
try? Auth.auth().signOut()
handleAuthentication()
}
...
// 로그인 여부에 따른 OnboardingView 출현
private func handleAuthentication() {
if Auth.auth().currentUser == nil {
let onboardingVC = UINavigationController(rootViewController: OnboardingViewController())
// onboardingVC.modalPresentationStyle = .fullScreen
present(onboardingVC, animated: true)
}
}
- RegisterViewController.swift
- 계정 생성에 성공하면 RegisterView가 사라지도록 함
private func bindViews() {
...
viewModel.$user.sink { [weak self] user in
// 계정 생성, 로그인이 완료 되면 RegisterView 닫기
guard user != nil else { return }
guard let onBoardingVC = self?.navigationController?.viewControllers.first as? OnboardingViewController else { return }
onBoardingVC.dismiss(animated: true)
}
.store(in: &subscriptions)
...
}
2️⃣ Login 기능 구현
- LoginViewController.swift 파일 생성
- 기존에 생성했던 RegisterViewController와 동일한 UI 적용
import UIKit
import Combine
class LoginViewController: UIViewController {
private var viewModel = AuthenticationViewViewModel()
private var subscriptions: Set<AnyCancellable> = []
private let loginTitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Login to your account"
label.font = .systemFont(ofSize: 32, weight: .bold)
return label
}()
private let emailTextField: UITextField = {
let textField = UITextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.autocapitalizationType = .none
textField.keyboardType = .emailAddress
textField.attributedPlaceholder = NSAttributedString(
string: "Email",
attributes: [NSAttributedString.Key.foregroundColor : UIColor.gray]
)
return textField
}()
private let passwordTextField: UITextField = {
let textField = UITextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.attributedPlaceholder = NSAttributedString(
string: "Password",
attributes: [NSAttributedString.Key.foregroundColor : UIColor.gray]
)
textField.isSecureTextEntry = true
// iCloud Keychain is disabled 해결
textField.textContentType = .oneTimeCode
return textField
}()
private let loginButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Login", for: .normal)
button.tintColor = .label
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
button.backgroundColor = .systemCyan
button.layer.masksToBounds = true
button.layer.cornerRadius = 25
button.isEnabled = false
return button
}()
@objc private func didChangeEmailTextField() {
viewModel.email = emailTextField.text
viewModel.validateAuthenticationForm()
}
@objc private func didChangePasswordTextField() {
viewModel.password = passwordTextField.text
viewModel.validateAuthenticationForm()
}
private func bindViews() {
emailTextField.addTarget(self, action: #selector(didChangeEmailTextField), for: .editingChanged)
passwordTextField.addTarget(self, action: #selector(didChangePasswordTextField), for: .editingChanged)
viewModel.$isAuthenticationFormValid.sink { [weak self] validationState in
self?.loginButton.isEnabled = validationState
}
.store(in: &subscriptions)
viewModel.$user.sink { [weak self] user in
// 계정 생성, 로그인이 완료 되면 등록창 끄기
guard user != nil else { return }
guard let onBoardingVC = self?.navigationController?.viewControllers.first as? OnboardingViewController else { return }
onBoardingVC.dismiss(animated: true)
}
.store(in: &subscriptions)
viewModel.$error.sink { [weak self] errorString in
guard let error = errorString else { return }
self?.presentAlert(with: error)
}
.store(in: &subscriptions)
}
private func presentAlert(with error: String) {
let alert = UIAlertController(title: "Error", message: error, preferredStyle: .alert)
let okayButton = UIAlertAction(title: "OK", style: .default)
alert.addAction(okayButton)
present(alert, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
view.addSubview(loginTitleLabel)
view.addSubview(emailTextField)
view.addSubview(passwordTextField)
view.addSubview(loginButton)
configureConstraints()
loginButton.addTarget(self, action: #selector(didTapLogin), for: .touchUpInside)
bindViews()
}
@objc private func didTapLogin() {
viewModel.loginUser()
}
private func configureConstraints() {
let loginTitleLabelConstraints = [
loginTitleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loginTitleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20)
]
let emailTextFieldConstraints = [
emailTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
emailTextField.topAnchor.constraint(equalTo: loginTitleLabel.bottomAnchor, constant: 20),
emailTextField.widthAnchor.constraint(equalToConstant: view.frame.width - 40),
emailTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
emailTextField.heightAnchor.constraint(equalToConstant: 60)
]
let passwordTextFieldConstraints = [
passwordTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
passwordTextField.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 20),
passwordTextField.widthAnchor.constraint(equalToConstant: view.frame.width - 40),
passwordTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
passwordTextField.heightAnchor.constraint(equalToConstant: 60)
]
let loginButtonConstraints = [
loginButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
loginButton.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 20),
loginButton.widthAnchor.constraint(equalToConstant: 180),
loginButton.heightAnchor.constraint(equalToConstant: 50)
]
NSLayoutConstraint.activate(loginTitleLabelConstraints)
NSLayoutConstraint.activate(emailTextFieldConstraints)
NSLayoutConstraint.activate(passwordTextFieldConstraints)
NSLayoutConstraint.activate(loginButtonConstraints)
}
}
- RegisterViewViewModel →AuthenticationViewViewModel 이름 변경
- 계정을 등록하는 경우와 로그인 하는 경우의 로직이 동일하기 때문에 번거롭게 2개 만들어 관리할 필요가 없기 떄문
- AuthenticationViewViewModel.swift
- createUser()와 동일하게 loginUser() 메서드 생성
import Foundation
import Firebase
import FirebaseAuthCombineSwift
import Combine
final class AuthenticationViewViewModel: ObservableObject {
@Published var email: String?
@Published var password: String?
@Published var isAuthenticationFormValid: Bool = false
@Published var user: User?
@Published var error: String?
private var subscriptions: Set<AnyCancellable> = []
func validateAuthenticationForm() {
guard let email = email,
let password = password else {
isAuthenticationFormValid = false
return
}
isAuthenticationFormValid = isValidEmail(email) && password.count >= 8
}
func isValidEmail(_ email: String) -> Bool {
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
return emailPred.evaluate(with: email)
}
func createUser() {
guard let email = email,
let password = password else { return }
AuthManager.shared.registerUser(with: email, password: password)
.sink { [weak self] completion in
if case .failure(let error) = completion {
self?.error = error.localizedDescription
}
} receiveValue: { [weak self] user in
self?.user = user
}
.store(in: &subscriptions)
}
func loginUser() {
guard let email = email,
let password = password else { return }
AuthManager.shared.loginUser(with: email, password: password)
.sink { [weak self] completion in
if case .failure(let error) = completion {
self?.error = error.localizedDescription
}
} receiveValue: { [weak self] user in
self?.user = user
}
.store(in: &subscriptions)
}
}
- AuthManager.swift
- firebase 서버를 통해 email, password 로 로그인할 함수 설정 (loginUser)
import Foundation
import Firebase
import FirebaseAuthCombineSwift
import Combine
class AuthManager {
static let shared = AuthManager()
func registerUser(with email: String, password: String) -> AnyPublisher<User, Error> {
return Auth.auth().createUser(withEmail: email, password: password)
.map(\.user)
.eraseToAnyPublisher()
}
func loginUser(with email: String, password: String) -> AnyPublisher<User, Error> {
return Auth.auth().signIn(withEmail: email, password: password)
.map(\.user)
.eraseToAnyPublisher()
}
}
🟨 TIL
- 추후에 firebase를 통한 로그인, 로그아웃 기능, 비밀번호 찾기, 아이디 찾기 등 정리할 것
'Clone App > Twitter' 카테고리의 다른 글
[Twitter Clone] Add ProfileDataFormView (0) | 2024.06.11 |
---|---|
[Twitter Clone] Set user Info into firebase storage (1) | 2024.06.09 |
[Twitter Clone] Add ViewModel and bind view (0) | 2024.06.05 |
[Twitter Clone] Add Firebase to App (setup onboarding view controllers) (0) | 2024.06.04 |
[Twitter Clone] Add firebase - part1 (0) | 2024.05.28 |