diff options
author | Paul Beusterien <paulbeusterien@google.com> | 2017-09-19 11:18:04 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-09-19 11:18:04 -0700 |
commit | 9447e72cab40c9ea59e49a726d2890bcf356d38a (patch) | |
tree | dc25e027769cc1fbe6bd6e0ae0d877858e0780e7 /Example/Auth/SwiftSample/ViewController.swift | |
parent | a34d091971d05ef8e2625074157eb9ff6dda3cbd (diff) |
Consolidate AuthSamples into main Firebase Xcode project (#288)
Diffstat (limited to 'Example/Auth/SwiftSample/ViewController.swift')
-rw-r--r-- | Example/Auth/SwiftSample/ViewController.swift | 700 |
1 files changed, 700 insertions, 0 deletions
diff --git a/Example/Auth/SwiftSample/ViewController.swift b/Example/Auth/SwiftSample/ViewController.swift new file mode 100644 index 0000000..e90b727 --- /dev/null +++ b/Example/Auth/SwiftSample/ViewController.swift @@ -0,0 +1,700 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import UIKit + +import FirebaseCommunity.FirebaseAuth +import GoogleSignIn + +final class ViewController: UIViewController, UITextFieldDelegate, FIRAuthUIDelegate { + /// The profile image for the currently signed-in user. + @IBOutlet weak var profileImage: UIImageView! + + /// The display name for the currently signed-in user. + @IBOutlet weak var displayNameLabel: UILabel! + + /// The email for the currently signed-in user. + @IBOutlet weak var emailLabel: UILabel! + + /// The ID for the currently signed-in user. + @IBOutlet weak var userIDLabel: UILabel! + + /// The list of providers for the currently signed-in user. + @IBOutlet weak var providerListLabel: UILabel! + + /// The picker for the list of action types. + @IBOutlet weak var actionTypePicker: UIPickerView! + + /// The picker for the list of actions. + @IBOutlet weak var actionPicker: UIPickerView! + + /// The picker for the list of credential types. + @IBOutlet weak var credentialTypePicker: UIPickerView! + + /// The label for the "email" text field. + @IBOutlet weak var emailInputLabel: UILabel! + + /// The "email" text field. + @IBOutlet weak var emailField: UITextField! + + /// The label for the "password" text field. + @IBOutlet weak var passwordInputLabel: UILabel! + + /// The "password" text field. + @IBOutlet weak var passwordField: UITextField! + + /// The "phone" text field. + @IBOutlet weak var phoneField: UITextField! + + /// The scroll view holding all content. + @IBOutlet weak var scrollView: UIScrollView! + + // The active keyboard input field. + var activeField: UITextField? + + /// The currently selected action type. + fileprivate var actionType = ActionType(rawValue: 0)! { + didSet { + if actionType != oldValue { + actionPicker.reloadAllComponents() + actionPicker.selectRow(actionType == .auth ? authAction.rawValue : userAction.rawValue, + inComponent: 0, animated: false) + } + } + } + + /// The currently selected auth action. + fileprivate var authAction = AuthAction(rawValue: 0)! + + /// The currently selected user action. + fileprivate var userAction = UserAction(rawValue: 0)! + + /// The currently selected credential. + fileprivate var credentialType = CredentialType(rawValue: 0)! + + /// The current Firebase user. + fileprivate var user: User? = nil { + didSet { + if user?.uid != oldValue?.uid { + actionTypePicker.reloadAllComponents() + actionType = ActionType(rawValue: actionTypePicker.selectedRow(inComponent: 0))! + } + } + } + + func registerForKeyboardNotifications() { + NotificationCenter.default.addObserver(self, + selector: + #selector(keyboardWillBeShown(notification:)), + name: NSNotification.Name.UIKeyboardWillShow, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(keyboardWillBeHidden(notification:)), + name: NSNotification.Name.UIKeyboardWillHide, + object: nil) + } + + func deregisterFromKeyboardNotifications() { + NotificationCenter.default.removeObserver(self, + name: NSNotification.Name.UIKeyboardWillShow, + object: nil) + NotificationCenter.default.removeObserver(self, + name: NSNotification.Name.UIKeyboardWillHide, + object: nil) + } + + func keyboardWillBeShown(notification: NSNotification) { + scrollView.isScrollEnabled = true + let info = notification.userInfo! + let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size + let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize!.height, 0.0) + + scrollView.contentInset = contentInsets + scrollView.scrollIndicatorInsets = contentInsets + + var aRect = self.view.frame + aRect.size.height -= keyboardSize!.height + if let activeField = activeField { + if (!aRect.contains(activeField.frame.origin)) { + scrollView.scrollRectToVisible(activeField.frame, animated: true) + } + } + } + + func keyboardWillBeHidden(notification: NSNotification){ + let info = notification.userInfo! + let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size + let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, -keyboardSize!.height, 0.0) + scrollView.contentInset = contentInsets + scrollView.scrollIndicatorInsets = contentInsets + self.view.endEditing(true) + scrollView.isScrollEnabled = false + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + activeField = textField + } + + func textFieldDidEndEditing(_ textField: UITextField) { + activeField = nil + } + + func dismissKeyboard() { + view.endEditing(true) + } + + func verify(phoneNumber: String, completion: @escaping (PhoneAuthCredential?, Error?) -> Void) { + if #available(iOS 8.0, *) { + PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate:self) { + verificationID, error in + guard error == nil else { + completion(nil, error) + return + } + let codeAlertController = + UIAlertController(title: "Enter Code", message: nil, preferredStyle: .alert) + codeAlertController.addTextField { textfield in + textfield.placeholder = "SMS Code" + textfield.keyboardType = UIKeyboardType.numberPad + } + codeAlertController.addAction(UIAlertAction(title: "OK", + style: .default, + handler: { (UIAlertAction) in + let code = codeAlertController.textFields!.first!.text! + let phoneCredential = + PhoneAuthProvider.provider().credential(withVerificationID: verificationID ?? "", + verificationCode: code) + completion(phoneCredential, nil) + })) + self.present(codeAlertController, animated: true, completion: nil) + } + } + } + /// The user's photo URL used by the last network request for its contents. + fileprivate var lastPhotoURL: URL? = nil + + override func viewDidLoad() { + GIDSignIn.sharedInstance().uiDelegate = self + updateUserInfo(Auth.auth()) + NotificationCenter.default.addObserver(forName: .AuthStateDidChange, + object: Auth.auth(), queue: nil) { notification in + self.updateUserInfo(notification.object as? Auth) + } + phoneField.delegate = self + registerForKeyboardNotifications() + + let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + scrollView.addGestureRecognizer(tap) + } + + override func viewWillDisappear(_ animated: Bool) { + deregisterFromKeyboardNotifications() + } + + /// Executes the action designated by the operator on the UI. + @IBAction func execute(_ sender: UIButton) { + switch actionType { + case .auth: + switch authAction { + case .fetchProviderForEmail: + Auth.auth().fetchProviders(forEmail: emailField.text!) { providers, error in + self.ifNoError(error) { + self.showAlert(title: "Providers", message: providers?.joined(separator: ", ")) + } + } + case .signInAnonymously: + Auth.auth().signInAnonymously() { user, error in + self.ifNoError(error) { + self.showAlert(title: "Signed In Anonymously") + } + } + case .signInWithCredential: + getCredential() { credential in + Auth.auth().signIn(with: credential) { user, error in + self.ifNoError(error) { + self.showAlert(title: "Signed In With Credential", message: user?.textDescription) + } + } + } + case .createUser: + Auth.auth().createUser(withEmail: emailField.text!, password: passwordField.text!) { + user, error in + self.ifNoError(error) { + self.showAlert(title: "Signed In With Credential", message: user?.textDescription) + } + } + case .signOut: + try! Auth.auth().signOut() + GIDSignIn.sharedInstance().signOut() + } + case .user: + switch userAction { + case .updateEmail: + user!.updateEmail(to: emailField.text!) { error in + self.ifNoError(error) { + self.showAlert(title: "Updated Email", message: self.user?.email) + } + } + case .updatePhone: + let phoneNumber = phoneField.text + self.verify(phoneNumber: phoneNumber!, completion: { (phoneAuthCredential, error) in + guard error == nil else { + self.showAlert(title: "Error", message: error!.localizedDescription) + return + } + self.user!.updatePhoneNumber(phoneAuthCredential!, completion: { error in + self.ifNoError(error) { + self.showAlert(title: "Updated Phone Number") + self.updateUserInfo(Auth.auth()) + } + }) + }) + case .updatePassword: + user!.updatePassword(to: passwordField.text!) { error in + self.ifNoError(error) { + self.showAlert(title: "Updated Password") + } + } + case .reload: + user!.reload() { error in + self.ifNoError(error) { + self.showAlert(title: "Reloaded", message: self.user?.textDescription) + } + } + case .reauthenticate: + getCredential() { credential in + self.user!.reauthenticate(with: credential) { error in + self.ifNoError(error) { + self.showAlert(title: "Reauthenticated", message: self.user?.textDescription) + } + } + } + case .getToken: + user!.getIDToken() { token, error in + self.ifNoError(error) { + self.showAlert(title: "Got ID Token", message: token) + } + } + case .linkWithCredential: + getCredential() { credential in + self.user!.link(with: credential) { user, error in + self.ifNoError(error) { + self.showAlert(title: "Linked With Credential", message: user?.textDescription) + } + } + } + case .deleteAccount: + user!.delete() { error in + self.ifNoError(error) { + self.showAlert(title: "Deleted Account") + } + } + } + } + } + + /// Gets an AuthCredential potentially asynchronously. + private func getCredential(completion: @escaping (AuthCredential) -> Void) { + switch credentialType { + case .google: + GIDSignIn.sharedInstance().delegate = GoogleSignInDelegate(completion: { user, error in + self.ifNoError(error) { + completion(GoogleAuthProvider.credential( + withIDToken: user!.authentication.idToken, + accessToken: user!.authentication.accessToken)) + } + }) + GIDSignIn.sharedInstance().signIn() + case .password: + completion(EmailAuthProvider.credential(withEmail: emailField.text!, + password: passwordField.text!)) + case .phone: + let phoneNumber = phoneField.text + self.verify(phoneNumber: phoneNumber!, completion: { (phoneAuthCredential, error) in + guard error == nil else { + self.showAlert(title: "Error", message: error!.localizedDescription) + return + } + completion(phoneAuthCredential!) + }) + } + } + + /// Updates user's profile image and info text. + private func updateUserInfo(_ auth: Auth?) { + user = auth?.currentUser + displayNameLabel.text = user?.displayName + emailLabel.text = user?.email + userIDLabel.text = user?.uid + let providers = user?.providerData.map { userInfo in userInfo.providerID } + providerListLabel.text = providers?.joined(separator: ", ") + if let photoURL = user?.photoURL { + lastPhotoURL = photoURL + let queue: DispatchQueue + if #available(iOS 8.0, *) { + queue = DispatchQueue.global(qos: .background) + } else { + queue = DispatchQueue.global(priority: DispatchQueue.GlobalQueuePriority.background) + } + queue.async { + if let imageData = try? Data(contentsOf: photoURL) { + let image = UIImage(data: imageData) + DispatchQueue.main.async { + if self.lastPhotoURL == photoURL { + self.profileImage.image = image + } + } + } + } + } else { + lastPhotoURL = nil + self.profileImage.image = nil + } + updateControls() + } + + // Updates the states of the UI controls. + fileprivate func updateControls() { + let action: Action + switch actionType { + case .auth: + action = authAction + case .user: + action = userAction + } + let isCredentialEnabled = action.requiresCredential + credentialTypePicker.isUserInteractionEnabled = isCredentialEnabled + credentialTypePicker.alpha = isCredentialEnabled ? 1.0 : 0.6 + let isEmailEnabled = isCredentialEnabled && credentialType.requiresEmail || action.requiresEmail + emailInputLabel.alpha = isEmailEnabled ? 1.0 : 0.6 + emailField.isEnabled = isEmailEnabled + let isPasswordEnabled = isCredentialEnabled && credentialType.requiresPassword || + action.requiresPassword + passwordInputLabel.alpha = isPasswordEnabled ? 1.0 : 0.6 + passwordField.isEnabled = isPasswordEnabled + phoneField.isEnabled = credentialType.requiresPhone || action.requiresPhoneNumber + } + + fileprivate func showAlert(title: String, message: String? = "") { + if #available(iOS 8.0, *) { + let alertController = + UIAlertController(title: title, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "OK", + style: .default, + handler: { (UIAlertAction) in + alertController.dismiss(animated: true, completion: nil) + })) + self.present(alertController, animated: true, completion: nil) + } else { + UIAlertView(title: title, + message: message ?? "(NULL)", + delegate: nil, + cancelButtonTitle: nil, + otherButtonTitles: "OK").show() + } + } + + private func ifNoError(_ error: Error?, execute: () -> Void) { + guard error == nil else { + showAlert(title: "Error", message: error!.localizedDescription) + return + } + execute() + } +} + +extension ViewController : GIDSignInUIDelegate { + func sign(_ signIn: GIDSignIn!, present viewController: UIViewController!) { + present(viewController, animated: true, completion: nil) + } + + func sign(_ signIn: GIDSignIn!, dismiss viewController: UIViewController!) { + dismiss(animated: true, completion: nil) + } +} + +extension ViewController : UIPickerViewDataSource { + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + switch pickerView { + case actionTypePicker: + if Auth.auth().currentUser != nil { + return ActionType.countWithUser + } else { + return ActionType.countWithoutUser + } + case actionPicker: + switch actionType { + case .auth: + return AuthAction.count + case .user: + return UserAction.count + } + case credentialTypePicker: + return CredentialType.count + default: + return 0 + } + } +} + +extension ViewController : UIPickerViewDelegate { + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) + -> String? { + switch pickerView { + case actionTypePicker: + return ActionType(rawValue: row)!.text + case actionPicker: + switch actionType { + case .auth: + return AuthAction(rawValue: row)!.text + case .user: + return UserAction(rawValue: row)!.text + } + case credentialTypePicker: + return CredentialType(rawValue: row)!.text + default: + return nil + } + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + switch pickerView { + case actionTypePicker: + actionType = ActionType(rawValue: row)! + case actionPicker: + switch actionType { + case .auth: + authAction = AuthAction(rawValue: row)! + case .user: + userAction = UserAction(rawValue: row)! + } + case credentialTypePicker: + credentialType = CredentialType(rawValue: row)! + default: + break + } + updateControls() + } +} + +/// An adapter class to pass GoogleSignIn delegate method to a block. +fileprivate final class GoogleSignInDelegate: NSObject, GIDSignInDelegate { + + private let completion: (GIDGoogleUser?, Error?) -> Void + private var retainedSelf: GoogleSignInDelegate? + + init(completion: @escaping (GIDGoogleUser?, Error?) -> Void) { + self.completion = completion + super.init() + retainedSelf = self + } + + func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser?, withError error: Error?) { + completion(user, error) + retainedSelf = nil + } +} + +/// The list of all possible action types. +fileprivate enum ActionType: Int { + + case auth, user + + // Count of action types when no user is signed in. + static var countWithoutUser: Int { + return ActionType.auth.rawValue + 1 + } + + // Count of action types when a user is signed in. + static var countWithUser: Int { + return ActionType.user.rawValue + 1 + } + + /// The text description for a particular enum value. + var text : String { + switch self { + case .auth: + return "Auth" + case .user: + return "User" + } + } +} + +fileprivate protocol Action { + /// The text description for the particular action. + var text: String { get } + + /// Whether or not the action requires a credential. + var requiresCredential : Bool { get } + + /// Whether or not the action requires an email. + var requiresEmail: Bool { get } + + /// Whether or not the credential requires a password. + var requiresPassword: Bool { get } + + /// Whether or not the credential requires a phone number. + var requiresPhoneNumber: Bool { get } +} + +/// The list of all possible actions the operator can take on the Auth object. +fileprivate enum AuthAction: Int, Action { + + case fetchProviderForEmail, signInAnonymously, signInWithCredential, createUser, signOut + + /// Total number of auth actions. + static var count: Int { + return AuthAction.signOut.rawValue + 1 + } + + var text : String { + switch self { + case .fetchProviderForEmail: + return "Fetch Provider ⬇️" + case .signInAnonymously: + return "Sign In Anonymously" + case .signInWithCredential: + return "Sign In w/ Credential ↙️" + case .createUser: + return "Create User ⬇️" + case .signOut: + return "Sign Out" + } + } + + var requiresCredential : Bool { + return self == .signInWithCredential + } + + var requiresEmail : Bool { + return self == .fetchProviderForEmail || self == .createUser + } + + var requiresPassword : Bool { + return self == .createUser + } + + var requiresPhoneNumber: Bool { + return false + } +} + +/// The list of all possible actions the operator can take on the User object. +fileprivate enum UserAction: Int, Action { + + case updateEmail, updatePhone, updatePassword, reload, reauthenticate, getToken, + linkWithCredential, deleteAccount + + /// Total number of user actions. + static var count: Int { + return UserAction.deleteAccount.rawValue + 1 + } + + var text : String { + switch self { + case .updateEmail: + return "Update Email ⬇️" + case .updatePhone: + if #available(iOS 8.0, *) { + return "Update Phone ⬇️" + } else { + return "-" + } + case .updatePassword: + return "Update Password ⬇️" + case .reload: + return "Reload" + case .reauthenticate: + return "Reauthenticate ↙️" + case .getToken: + return "Get Token" + case .linkWithCredential: + return "Link With Credential ↙️" + case .deleteAccount: + return "Delete Account" + } + } + + var requiresCredential : Bool { + return self == .reauthenticate || self == .linkWithCredential + } + + var requiresEmail : Bool { + return self == .updateEmail + } + + var requiresPassword : Bool { + return self == .updatePassword + } + + var requiresPhoneNumber : Bool { + return self == .updatePhone + } + +} + +/// The list of all possible credential types the operator can use to sign in or link. +fileprivate enum CredentialType: Int { + + case google, password, phone + + /// Total number of enum values. + static var count: Int { + return CredentialType.phone.rawValue + 1 + } + + /// The text description for a particular enum value. + var text : String { + switch self { + case .google: + return "Google" + case .password: + return "Password ➡️️" + case .phone: + if #available(iOS 8.0, *) { + return "Phone ➡️️" + } else { + return "-" + } + } + } + + /// Whether or not the credential requires an email. + var requiresEmail : Bool { + return self == .password + } + + /// Whether or not the credential requires a password. + var requiresPassword : Bool { + return self == .password + } + + /// Whether or not the credential requires a phone number. + var requiresPhone : Bool { + return self == .phone + } +} + +fileprivate extension User { + var textDescription: String { + return self.displayName ?? self.email ?? self.uid + } +} |