Build Instagram App: Part 4 (Swift 5) - 2020 - Xcode 11 - iOS Development
func registerNewUser(userName: String, email: String, password: String) -> AnyPublisher<Bool, Never> {
return Future { [weak self] promise in
var registerSubscription: AnyCancellable?
registerSubscription = DatabaseManager.shared
.canCreateNewUser(with: email, userName: userName)
.sink(receiveValue: { isSucceeded in
if isSucceeded {
self?.auth.createUser(withEmail: email, password: password, completion: { result, error in
if error == nil, result != nil {
var insertSubscription: AnyCancellable?
insertSubscription = DatabaseManager.shared.insertNewUser(with: email, userName: userName)
.sink { isSucceeded in
if isSucceeded {
promise(.success(true))
} else {
promise(.success(false))
}
insertSubscription?.cancel()
}
} else {
promise(.success(false))
}
})
} else {
promise(.success(false))
}
registerSubscription?.cancel()
})
}
.eraseToAnyPublisher()
}
func canCreateNewUser(with email: String, userName: String) -> AnyPublisher<Bool, Never> {
let collection = database.collection(Constants.userNameCollectionPath)
let query = collection.whereField("userName", isEqualTo: userName)
return Future { promise in
query.getDocuments { snapshots, error in
if
error == nil,
let snapshots = snapshots,
snapshots.documents.isEmpty {
promise(.success(true))
} else {
promise(.success(false))
}
}
}
.eraseToAnyPublisher()
}
func insertNewUser(with email: String, userName: String) -> AnyPublisher<Bool, Never> {
return Future { [weak self] promise in
if let collection = self?.database.collection(Constants.userNameCollectionPath) {
collection.document().setData(["userName": userName, "email": email]) { error in
if let error = error {
print(error.localizedDescription)
promise(.success(false))
} else {
promise(.success(true))
}
}
} else {
promise(.success(false))
}
}
.eraseToAnyPublisher()
}
func loginUser(userName: String?, email: String?, password: String) -> AnyPublisher<Bool, Error> {
return Future { [weak self] promise in
if let email = email {
self?.auth.signIn(withEmail: email, password: password, completion: { result, error in
if
result != nil,
error == nil {
promise(.success(true))
print("Sing In Succeed")
} else {
print("Sign In Error")
promise(.failure(AuthError.loginDidFail))
}
})
} else if let userName = userName {
promise(.failure(AuthError.loginDidFail))
} else {
promise(.failure(AuthError.loginDidFail))
}
}
.eraseToAnyPublisher()
}
import Foundation
import FirebaseAuth
import Combine
class AuthManager {
enum AuthError: LocalizedError {
case loginDidFail
case createUserDidFail
}
static let shared = AuthManager()
private let auth = FirebaseAuth.Auth.auth()
private init() {}
func isSignedIn() -> Bool {
return auth.currentUser != nil
}
func logout() -> AnyPublisher<Bool, Error> {
return Future { [weak self] promise in
do {
try self?.auth.signOut()
promise(.success(true))
} catch {
print(error.localizedDescription)
promise(.success(false))
}
}
.eraseToAnyPublisher()
}
func loginUser(userName: String?, email: String?, password: String) -> AnyPublisher<Bool, Error> {
return Future { [weak self] promise in
if let email = email {
self?.auth.signIn(withEmail: email, password: password, completion: { result, error in
if
result != nil,
error == nil {
promise(.success(true))
print("Sing In Succeed")
} else {
print("Sign In Error")
promise(.failure(AuthError.loginDidFail))
}
})
} else if let userName = userName {
promise(.failure(AuthError.loginDidFail))
} else {
promise(.failure(AuthError.loginDidFail))
}
}
.eraseToAnyPublisher()
}
func registerNewUser(userName: String, email: String, password: String) -> AnyPublisher<Bool, Never> {
return Future { [weak self] promise in
var registerSubscription: AnyCancellable?
registerSubscription = DatabaseManager.shared
.canCreateNewUser(with: email, userName: userName)
.sink(receiveValue: { isSucceeded in
if isSucceeded {
self?.auth.createUser(withEmail: email, password: password, completion: { result, error in
if error == nil, result != nil {
var insertSubscription: AnyCancellable?
insertSubscription = DatabaseManager.shared.insertNewUser(with: email, userName: userName)
.sink { isSucceeded in
if isSucceeded {
promise(.success(true))
} else {
promise(.success(false))
}
insertSubscription?.cancel()
}
} else {
promise(.success(false))
}
})
} else {
promise(.success(false))
}
registerSubscription?.cancel()
})
}
.eraseToAnyPublisher()
}
}
import Foundation
import Combine
import Firebase
class DatabaseManager {
struct Constants {
static let userNameCollectionPath = "userNameCollectionPath"
}
static let shared = DatabaseManager()
private let database = Firestore.firestore()
private init() {}
func canCreateNewUser(with email: String, userName: String) -> AnyPublisher<Bool, Never> {
let collection = database.collection(Constants.userNameCollectionPath)
let query = collection.whereField("userName", isEqualTo: userName)
return Future { promise in
query.getDocuments { snapshots, error in
if
error == nil,
let snapshots = snapshots,
snapshots.documents.isEmpty {
promise(.success(true))
} else {
promise(.success(false))
}
}
}
.eraseToAnyPublisher()
}
func insertNewUser(with email: String, userName: String) -> AnyPublisher<Bool, Never> {
return Future { [weak self] promise in
if let collection = self?.database.collection(Constants.userNameCollectionPath) {
collection.document().setData(["userName": userName, "email": email]) { error in
if let error = error {
print(error.localizedDescription)
promise(.success(false))
} else {
promise(.success(true))
}
}
} else {
promise(.success(false))
}
}
.eraseToAnyPublisher()
}
}
import UIKit
import Combine
class RegistrationViewController: UIViewController {
struct Constants {
static let cornerRadius: CGFloat = 8.0
}
private let nameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "UserName"
textField.returnKeyType = .next
textField.leftViewMode = .always
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.layer.cornerRadius = Constants.cornerRadius
textField.layer.masksToBounds = true
textField.backgroundColor = .secondarySystemBackground
textField.layer.borderWidth = 1.0
textField.layer.borderColor = UIColor.secondaryLabel.cgColor
textField.clearButtonMode = .always
textField.tag = 1
return textField
}()
private let emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Email Address"
textField.returnKeyType = .next
textField.leftViewMode = .always
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.layer.cornerRadius = Constants.cornerRadius
textField.layer.masksToBounds = true
textField.backgroundColor = .secondarySystemBackground
textField.layer.borderWidth = 1.0
textField.layer.borderColor = UIColor.secondaryLabel.cgColor
textField.clearButtonMode = .always
textField.tag = 1
return textField
}()
private let passwordTextField: UITextField = {
let textField = UITextField()
textField.textContentType = .newPassword
textField.placeholder = "Password"
textField.returnKeyType = .continue
textField.leftViewMode = .always
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.layer.cornerRadius = Constants.cornerRadius
textField.layer.masksToBounds = true
textField.backgroundColor = .secondarySystemBackground
textField.layer.borderWidth = 1.0
textField.layer.borderColor = UIColor.secondaryLabel.cgColor
textField.clearButtonMode = .always
textField.tag = 2
return textField
}()
private let registerButton: UIButton = {
let button = UIButton()
button.setTitle("Sign Up", for: .normal)
button.layer.masksToBounds = true
button.layer.cornerRadius = Constants.cornerRadius
button.backgroundColor = .systemGreen
button.setTitleColor(.white, for: .normal)
return button
}()
private var cancellables = Set<AnyCancellable>()
private let viewModel = RegistrationViewModel()
private let input: PassthroughSubject<RegistrationViewModel.Input, Never> = .init()
override func viewDidLoad() {
super.viewDidLoad()
setUI()
bind()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
nameTextField.frame = CGRect(x: 20, y: view.safeAreaInsets.top + 10, width: view.width - 40, height: 52)
emailTextField.frame = CGRect(x: 20, y: nameTextField.bottom + 10, width: view.width - 40, height: 52)
passwordTextField.frame = CGRect(x: 20, y: emailTextField.bottom + 10, width: view.width - 40, height: 52)
registerButton.frame = CGRect(x: 20, y: passwordTextField.bottom + 10, width: view.width - 40, height: 52)
}
private func setUI() {
view.backgroundColor = .systemBackground
view.addSubview(nameTextField)
view.addSubview(emailTextField)
view.addSubview(passwordTextField)
view.addSubview(registerButton)
}
private func handleRegisterResult(result: Bool) {
if result {
dismiss(animated: true, completion: nil)
} else {
let alert = UIAlertController(title: "Sign Up Error", message: "Unable to Sign Up", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alert, animated: true)
}
}
private func bind() {
let output = viewModel.transform(input: input.eraseToAnyPublisher())
output
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
switch result {
case .isRegisterDidSucceed(result: let result): self?.handleRegisterResult(result: result)
}
}
.store(in: &cancellables)
nameTextField
.textPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { [weak self] text in
self?.input.send(.nameInput(text: text))
}
.store(in: &cancellables)
nameTextField
.controlPublisher(for: .editingDidEndOnExit)
.sink { [weak self] _ in
self?.emailTextField.becomeFirstResponder()
}
.store(in: &cancellables)
emailTextField
.textPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { [weak self] text in
self?.input.send(.emailInput(text: text))
}
.store(in: &cancellables)
emailTextField
.controlPublisher(for: .editingDidEndOnExit)
.sink { [weak self] _ in
self?.passwordTextField.becomeFirstResponder()
}
.store(in: &cancellables)
passwordTextField
.textPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { [weak self] text in
self?.input.send(.passwordInput(text: text))
}
.store(in: &cancellables)
passwordTextField
.controlPublisher(for: .editingChanged)
.sink { [weak self] _ in
self?.passwordTextField.isSecureTextEntry = true
}
.store(in: &cancellables)
passwordTextField
.controlPublisher(for: .editingDidEndOnExit)
.sink { [weak self] _ in
self?.passwordTextField.resignFirstResponder()
self?.registerButton.sendActions(for: .touchUpInside)
}
.store(in: &cancellables)
registerButton
.tapPublisher
.sink { [weak self] _ in
self?.nameTextField.resignFirstResponder()
self?.emailTextField.resignFirstResponder()
self?.passwordTextField.resignFirstResponder()
self?.input.send(.registerButtonDidTap)
}
.store(in: &cancellables)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(backgroundViewDidTap))
view.addGestureRecognizer(tapGesture)
}
@objc private func backgroundViewDidTap() {
emailTextField.resignFirstResponder()
nameTextField.resignFirstResponder()
passwordTextField.resignFirstResponder()
}
}
import Foundation
import Combine
class RegistrationViewModel {
enum Input {
case nameInput(text: String)
case emailInput(text: String)
case passwordInput(text: String)
case registerButtonDidTap
}
enum Output {
case isRegisterDidSucceed(result: Bool)
}
private let nameText: CurrentValueSubject<String?, Never> = .init(nil)
private let emailText: CurrentValueSubject<String?, Never> = .init(nil)
private let passwordText: CurrentValueSubject<String?, Never> = .init(nil)
private var cancellables = Set<AnyCancellable>()
private let authManager = AuthManager.shared
private let output: PassthroughSubject<Output, Never> = .init()
func transform(input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never> {
input
.receive(on: DispatchQueue.global(qos: .default))
.sink { [weak self] result in
switch result {
case .emailInput(text: let text):
self?.emailText.send(text)
case .nameInput(text: let text):
self?.nameText.send(text)
case .passwordInput(text: let text):
self?.passwordText.send(text)
case .registerButtonDidTap:
self?.handleRegister()
}
}
.store(in: &cancellables)
return output.eraseToAnyPublisher()
}
private func handleRegister() {
guard
let email = emailText.value, !email.isEmpty,
let password = passwordText.value, !password.isEmpty, password.count >= 6,
let userName = nameText.value, !userName.isEmpty else { return }
var registerSubscription: AnyCancellable?
registerSubscription = authManager
.registerNewUser(userName: userName, email: email, password: password)
.sink(receiveValue: { [weak self] isSucceeded in
self?.output.send(.isRegisterDidSucceed(result: isSucceeded))
registerSubscription?.cancel()
})
}
}