diff --git a/Topical Dictionary/Controller/AccountViewController.swift b/Topical Dictionary/Controller/AccountViewController.swift index 3b5e5dd..f70e8b1 100644 --- a/Topical Dictionary/Controller/AccountViewController.swift +++ b/Topical Dictionary/Controller/AccountViewController.swift @@ -9,28 +9,31 @@ import UIKit import FirebaseAuth -class AccountViewController: UIViewController, AccountViewModelDelegate { +class AccountViewController: BaseViewController, AccountViewModelDelegate { @IBOutlet weak var accountTableView: UITableView! var viewModel: AccountViewModel? + private let authService = AuthService.shared override func viewDidLoad() { super.viewDidLoad() - viewModel = AccountViewModel(delegate: self) - accountTableView.dataSource = self accountTableView.delegate = self } - + + override func configureViewModel() { + viewModel = AccountViewModel(delegate: self) + baseViewModel = viewModel + } + func logout() { let alert = UIAlertController(title: "Loging out", message: "You are currently logging out. Do you want to continue?", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Logout", style: .cancel, handler: { _ in - let auth = Auth.auth() do { - try auth.signOut() + try authService.signOut() } catch let signoutError as NSError { print("Error while signing out: \(signoutError)") } @@ -53,12 +56,6 @@ class AccountViewController: UIViewController, AccountViewModelDelegate { // MARK: - View Model Delegate - func didErrorOccured(_ viewModel: AccountViewModel, error: Error) { - let alert = UIAlertController(title: "Something Went Wrong", message: error.localizedDescription, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - present(alert, animated: true, completion: nil) - } - func didUpdateName(_ viewModel: AccountViewModel, name: String) { DispatchQueue.main.async { self.accountTableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .fade) diff --git a/Topical Dictionary/Controller/BaseViewController.swift b/Topical Dictionary/Controller/BaseViewController.swift new file mode 100644 index 0000000..ebcd679 --- /dev/null +++ b/Topical Dictionary/Controller/BaseViewController.swift @@ -0,0 +1,40 @@ +// +// BaseViewController.swift +// Topical Dictionary +// +// Created by İbrahim Ethem Karalı on 2025-02-14. +// + +import UIKit + +class BaseViewController: UIViewController, BaseViewModelDelegate { + var baseViewModel: BaseViewModelProtocol? { + didSet { + baseViewModel?.delegate = self + } + } + + override func viewDidLoad() { + super.viewDidLoad() + configureViewModel() + bindViewModel() + } + + func configureViewModel() { + // Override in subclasses to create view model. + } + + func bindViewModel() { + // Override in subclasses to bind view model outputs. + } + + func didReceiveError(_ error: Error) { + presentError(error) + } + + func presentError(_ error: Error) { + let alert = UIAlertController(title: "Something Went Wrong", message: error.localizedDescription, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + present(alert, animated: true, completion: nil) + } +} diff --git a/Topical Dictionary/Controller/DictionaryViewController.swift b/Topical Dictionary/Controller/DictionaryViewController.swift index d4c7a78..7897674 100644 --- a/Topical Dictionary/Controller/DictionaryViewController.swift +++ b/Topical Dictionary/Controller/DictionaryViewController.swift @@ -10,7 +10,7 @@ import UIKit import FirebaseCore import FirebaseFirestore -class DictionaryViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, WordManagerDelegate, HeadCellDelegate, WordDetailViewDelegate, DictionaryServiceDelegate { +class DictionaryViewController: BaseViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, WordManagerDelegate, HeadCellDelegate, WordDetailViewDelegate, DictionaryServiceDelegate { enum sections: Int { case head = 0 @@ -21,6 +21,8 @@ class DictionaryViewController: UIViewController, UITableViewDelegate, UITableVi @IBOutlet var wordsTableView: UITableView! @IBOutlet weak var favoriteButton: UIBarButtonItem! + + var viewModel: DictionaryViewModel? lazy var selectedDictionary = DictionaryModel() var searchedWord: WordData? @@ -53,6 +55,11 @@ class DictionaryViewController: UIViewController, UITableViewDelegate, UITableVi setFavoriteImage(isFavorite, favoriteButton) } + + override func configureViewModel() { + viewModel = DictionaryViewModel() + baseViewModel = viewModel + } private func fireBaseSettings() { let settings = FirestoreSettings() diff --git a/Topical Dictionary/Controller/HomeViewController.swift b/Topical Dictionary/Controller/HomeViewController.swift index 244789d..97351ce 100644 --- a/Topical Dictionary/Controller/HomeViewController.swift +++ b/Topical Dictionary/Controller/HomeViewController.swift @@ -19,13 +19,15 @@ enum SortingType { case zToA } -class HomeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, DictionariesManagerDelegate { +class HomeViewController: BaseViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, DictionariesManagerDelegate { // MARK: Implementations var db: Firestore! @IBOutlet var mainTableView: UITableView! @IBOutlet var favButton: UIBarButtonItem! + + var viewModel: HomeViewModel? private var isFav = false private var stockRightBarItems: [UIBarButtonItem]? @@ -68,6 +70,11 @@ class HomeViewController: UIViewController, UITableViewDelegate, UITableViewData dictionaryManager.setListener() } + + override func configureViewModel() { + viewModel = HomeViewModel() + baseViewModel = viewModel + } // MARK: - Bar Button Actions diff --git a/Topical Dictionary/Controller/LoginRegister/EmailLoginViewController.swift b/Topical Dictionary/Controller/LoginRegister/EmailLoginViewController.swift index c5c143e..dbea002 100644 --- a/Topical Dictionary/Controller/LoginRegister/EmailLoginViewController.swift +++ b/Topical Dictionary/Controller/LoginRegister/EmailLoginViewController.swift @@ -7,9 +7,8 @@ // import UIKit -import FirebaseAuth -class EmailLoginViewController: UIViewController, UITextFieldDelegate { +class EmailLoginViewController: BaseViewController, UITextFieldDelegate { @IBOutlet weak var loginOrRegisterSegment: UISegmentedControl! @@ -23,6 +22,8 @@ class EmailLoginViewController: UIViewController, UITextFieldDelegate { @IBOutlet weak var fullnameTextField: UITextField! @IBOutlet weak var privacyPolicy: UILabel! // add real terms and conditions + + var viewModel: EmailLoginViewModel? override func viewDidLoad() { super.viewDidLoad() @@ -46,6 +47,11 @@ class EmailLoginViewController: UIViewController, UITextFieldDelegate { privacyPolicy.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapPrivacyPolicy))) } + + override func configureViewModel() { + viewModel = EmailLoginViewModel() + baseViewModel = viewModel + } @objc func didTapPrivacyPolicy() { let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "PrivacyViewController") @@ -66,6 +72,12 @@ class EmailLoginViewController: UIViewController, UITextFieldDelegate { self.myActivityIndicator?.endActivity() } } + + override func didReceiveError(_ error: Error) { + myActivityIndicator?.endActivity() + view.isUserInteractionEnabled = true + super.didReceiveError(error) + } @IBAction func loginOrRegister(_ sender: UISegmentedControl) { view.endEditing(true) @@ -96,19 +108,12 @@ class EmailLoginViewController: UIViewController, UITextFieldDelegate { if loginOrRegisterSegment.selectedSegmentIndex == 0 { // Login view.isUserInteractionEnabled = false - Auth.auth().signIn(withEmail: emailTextField.text!, password: passwordTextField.text!) { (authDataResult, error) in - if error != nil { - self.displayAlert(detail: error!.localizedDescription, title: "Something Went Wrong") - self.view.isUserInteractionEnabled = true - - return - } - + viewModel?.login(email: emailTextField.text ?? "", password: passwordTextField.text ?? "") { authDataResult in if let authDataRes = authDataResult { print("user signed in: UID: \(authDataRes.user.uid)") print("is email verified: \(authDataRes.user.isEmailVerified)") - self.myActivityIndicator?.endActivity() } + self.myActivityIndicator?.endActivity() self.view.isUserInteractionEnabled = true } @@ -116,21 +121,11 @@ class EmailLoginViewController: UIViewController, UITextFieldDelegate { if let password = passwordTextField.text, password == confirmPasswordTextField.text, let email = emailTextField.text, let name = fullnameTextField.text { - Auth.auth().createUser(withEmail: email, password: password) { authDataResult, error in - if let err = error { - self.displayAlert(detail: err.localizedDescription, title: "Something Went Wrong") - return + viewModel?.register(email: email, password: password, displayName: name) { authDataResult in + if let authDataRes = authDataResult { + print("user signed up: UID: \(authDataRes.user.uid)") } self.myActivityIndicator?.endActivity() - let changeRequest = authDataResult?.user.createProfileChangeRequest() - changeRequest?.displayName = name - changeRequest?.commitChanges(completion: { error in - if error != nil { - print("Error while setting display name: \(error!.localizedDescription)") - } else { - print("success") - } - }) } } else { print("Passwords doesn't match") diff --git a/Topical Dictionary/Controller/LoginRegister/LoginViewController.swift b/Topical Dictionary/Controller/LoginRegister/LoginViewController.swift index 329062b..1d2554a 100644 --- a/Topical Dictionary/Controller/LoginRegister/LoginViewController.swift +++ b/Topical Dictionary/Controller/LoginRegister/LoginViewController.swift @@ -15,7 +15,7 @@ import CryptoKit import AuthenticationServices import NVActivityIndicatorView -class LoginViewController: UIViewController, UIScrollViewDelegate, GIDSignInDelegate { +class LoginViewController: BaseViewController, UIScrollViewDelegate, GIDSignInDelegate { fileprivate var currentNonce: String? fileprivate var myActivityIndicator: MyActivityIndicator? @@ -29,7 +29,9 @@ class LoginViewController: UIViewController, UIScrollViewDelegate, GIDSignInDele @IBOutlet weak var facebookButton: UIButton! @IBOutlet weak var appleButton: UIButton! @IBOutlet weak var googleButton: UIButton! - + + var viewModel: LoginViewModel? + let fbLoginManager = LoginManager() @IBOutlet weak var scrollView: UIScrollView! @@ -56,6 +58,11 @@ class LoginViewController: UIViewController, UIScrollViewDelegate, GIDSignInDele GIDSignIn.sharedInstance()?.delegate = self } + + override func configureViewModel() { + viewModel = LoginViewModel() + baseViewModel = viewModel + } // MARK: Facebook login diff --git a/Topical Dictionary/Service/AuthService.swift b/Topical Dictionary/Service/AuthService.swift new file mode 100644 index 0000000..785f9d0 --- /dev/null +++ b/Topical Dictionary/Service/AuthService.swift @@ -0,0 +1,76 @@ +// +// AuthService.swift +// Topical Dictionary +// +// Created by İbrahim Ethem Karalı on 2025-02-14. +// + +import Foundation +import FirebaseAuth + +enum AuthServiceError: Error { + case missingUser +} + +final class AuthService { + static let shared = AuthService() + + private let auth: Auth + + init(auth: Auth = .auth()) { + self.auth = auth + } + + var currentUser: User? { + auth.currentUser + } + + func loginMethod(for user: User?) -> AuthProvider? { + let providerID = user?.providerData.first?.providerID + switch providerID { + case AuthProvider.email.rawValue: + return AuthProvider.email + case AuthProvider.facebook.rawValue: + return AuthProvider.facebook + case AuthProvider.google.rawValue: + return AuthProvider.google + case AuthProvider.apple.rawValue: + return AuthProvider.apple + default: + return nil + } + } + + func signOut() throws { + try auth.signOut() + } + + func signIn(email: String, password: String, completion: @escaping (AuthDataResult?, Error?) -> Void) { + auth.signIn(withEmail: email, password: password, completion: completion) + } + + func register(email: String, password: String, displayName: String, completion: @escaping (AuthDataResult?, Error?) -> Void) { + auth.createUser(withEmail: email, password: password) { authDataResult, error in + if let error = error { + completion(authDataResult, error) + return + } + guard let _ = authDataResult?.user else { + completion(authDataResult, AuthServiceError.missingUser) + return + } + self.updateDisplayName(displayName) { updateError in + completion(authDataResult, updateError) + } + } + } + + func updateDisplayName(_ name: String, completion: @escaping (Error?) -> Void) { + guard let change = auth.currentUser?.createProfileChangeRequest() else { + completion(AuthServiceError.missingUser) + return + } + change.displayName = name + change.commitChanges(completion: completion) + } +} diff --git a/Topical Dictionary/ViewModel/AccountViewModel.swift b/Topical Dictionary/ViewModel/AccountViewModel.swift index 598e377..370ba57 100644 --- a/Topical Dictionary/ViewModel/AccountViewModel.swift +++ b/Topical Dictionary/ViewModel/AccountViewModel.swift @@ -10,52 +10,36 @@ import Foundation import Firebase import FirebaseAuth -class AccountViewModel: NSObject { +class AccountViewModel: BaseViewModel { var userModel: UserModel - var delegate: AccountViewModelDelegate + var accountDelegate: AccountViewModelDelegate + private let authService: AuthService - init(delegate: AccountViewModelDelegate) { - self.delegate = delegate - let currentUser = Auth.auth().currentUser - let loginMethod = { () -> AuthProvider? in - let providerID = currentUser?.providerData.first?.providerID - switch providerID { - case AuthProvider.email.rawValue: - return AuthProvider.email - case AuthProvider.facebook.rawValue: - return AuthProvider.facebook - case AuthProvider.google.rawValue: - return AuthProvider.google - case AuthProvider.apple.rawValue: - return AuthProvider.apple - default: - return nil - } - } + init(delegate: AccountViewModelDelegate, authService: AuthService = .shared) { + self.accountDelegate = delegate + self.authService = authService + let currentUser = authService.currentUser userModel = UserModel(userID: currentUser?.uid ?? "", fullName: currentUser?.displayName, email: currentUser?.email, - loginMethod: loginMethod()) + loginMethod: authService.loginMethod(for: currentUser)) super.init() } func updateDisplayName(with text: String) { - let change = Auth.auth().currentUser?.createProfileChangeRequest() - change?.displayName = text - change?.commitChanges(completion: { error in + authService.updateDisplayName(text) { error in if let err = error { - self.delegate.didErrorOccured(self, error: err) + self.delegate?.didReceiveError(err) } self.userModel.fullName = text - self.delegate.didUpdateName(self, name: text) - }) + self.accountDelegate.didUpdateName(self, name: text) + } } } protocol AccountViewModelDelegate { - func didErrorOccured(_ viewModel: AccountViewModel, error: Error) func didUpdateName(_ viewModel: AccountViewModel, name: String) } diff --git a/Topical Dictionary/ViewModel/BaseViewModel.swift b/Topical Dictionary/ViewModel/BaseViewModel.swift new file mode 100644 index 0000000..529ba54 --- /dev/null +++ b/Topical Dictionary/ViewModel/BaseViewModel.swift @@ -0,0 +1,20 @@ +// +// BaseViewModel.swift +// Topical Dictionary +// +// Created by İbrahim Ethem Karalı on 2025-02-14. +// + +import Foundation + +protocol BaseViewModelDelegate: AnyObject { + func didReceiveError(_ error: Error) +} + +protocol BaseViewModelProtocol: AnyObject { + var delegate: BaseViewModelDelegate? { get set } +} + +class BaseViewModel: NSObject, BaseViewModelProtocol { + weak var delegate: BaseViewModelDelegate? +} diff --git a/Topical Dictionary/ViewModel/DictionaryViewModel.swift b/Topical Dictionary/ViewModel/DictionaryViewModel.swift new file mode 100644 index 0000000..e860a45 --- /dev/null +++ b/Topical Dictionary/ViewModel/DictionaryViewModel.swift @@ -0,0 +1,10 @@ +// +// DictionaryViewModel.swift +// Topical Dictionary +// +// Created by İbrahim Ethem Karalı on 2025-02-14. +// + +import Foundation + +final class DictionaryViewModel: BaseViewModel {} diff --git a/Topical Dictionary/ViewModel/EmailLoginViewModel.swift b/Topical Dictionary/ViewModel/EmailLoginViewModel.swift new file mode 100644 index 0000000..fcf6797 --- /dev/null +++ b/Topical Dictionary/ViewModel/EmailLoginViewModel.swift @@ -0,0 +1,38 @@ +// +// EmailLoginViewModel.swift +// Topical Dictionary +// +// Created by İbrahim Ethem Karalı on 2025-02-14. +// + +import Foundation +import FirebaseAuth + +final class EmailLoginViewModel: BaseViewModel { + private let authService: AuthService + + init(authService: AuthService = .shared) { + self.authService = authService + super.init() + } + + func login(email: String, password: String, completion: @escaping (AuthDataResult?) -> Void) { + authService.signIn(email: email, password: password) { result, error in + if let error = error { + self.delegate?.didReceiveError(error) + return + } + completion(result) + } + } + + func register(email: String, password: String, displayName: String, completion: @escaping (AuthDataResult?) -> Void) { + authService.register(email: email, password: password, displayName: displayName) { result, error in + if let error = error { + self.delegate?.didReceiveError(error) + return + } + completion(result) + } + } +} diff --git a/Topical Dictionary/ViewModel/HomeViewModel.swift b/Topical Dictionary/ViewModel/HomeViewModel.swift new file mode 100644 index 0000000..96dc6af --- /dev/null +++ b/Topical Dictionary/ViewModel/HomeViewModel.swift @@ -0,0 +1,10 @@ +// +// HomeViewModel.swift +// Topical Dictionary +// +// Created by İbrahim Ethem Karalı on 2025-02-14. +// + +import Foundation + +final class HomeViewModel: BaseViewModel {} diff --git a/Topical Dictionary/ViewModel/LoginViewModel.swift b/Topical Dictionary/ViewModel/LoginViewModel.swift new file mode 100644 index 0000000..be43526 --- /dev/null +++ b/Topical Dictionary/ViewModel/LoginViewModel.swift @@ -0,0 +1,10 @@ +// +// LoginViewModel.swift +// Topical Dictionary +// +// Created by İbrahim Ethem Karalı on 2025-02-14. +// + +import Foundation + +final class LoginViewModel: BaseViewModel {}