Files
firebase-ios-sdk/Example/Auth/SwiftSample/ViewController.swift
T

700 lines
22 KiB
Swift
Raw Normal View History

2017-05-15 12:27:07 -07:00
/*
* 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
2017-05-15 12:27:07 -07:00
final class ViewController: UIViewController, UITextFieldDelegate, FIRAuthUIDelegate {
2017-05-15 12:27:07 -07:00
/// 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?
2017-05-15 12:27:07 -07:00
/// 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)
}
}
}
2017-05-15 12:27:07 -07:00
/// 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()
2017-05-15 12:27:07 -07:00
}
/// 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())
}
})
})
2017-05-15 12:27:07 -07:00
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!)
})
2017-05-15 12:27:07 -07:00
}
}
/// 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 {
2017-05-15 12:27:07 -07:00
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
2017-05-15 12:27:07 -07:00
}
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()
}
2017-05-15 12:27:07 -07:00
}
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.
2017-05-15 12:27:07 -07:00
var requiresCredential : Bool { get }
/// Whether or not the action requires an email.
2017-05-15 12:27:07 -07:00
var requiresEmail: Bool { get }
/// Whether or not the credential requires a password.
2017-05-15 12:27:07 -07:00
var requiresPassword: Bool { get }
/// Whether or not the credential requires a phone number.
var requiresPhoneNumber: Bool { get }
2017-05-15 12:27:07 -07:00
}
/// 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
}
2017-05-15 12:27:07 -07:00
}
/// 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
2017-05-15 12:27:07 -07:00
/// 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 "-"
}
2017-05-15 12:27:07 -07:00
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
}
2017-05-15 12:27:07 -07:00
}
/// The list of all possible credential types the operator can use to sign in or link.
fileprivate enum CredentialType: Int {
case google, password, phone
2017-05-15 12:27:07 -07:00
/// Total number of enum values.
static var count: Int {
return CredentialType.phone.rawValue + 1
2017-05-15 12:27:07 -07:00
}
/// 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 "-"
}
2017-05-15 12:27:07 -07:00
}
}
/// Whether or not the credential requires an email.
2017-05-15 12:27:07 -07:00
var requiresEmail : Bool {
return self == .password
}
/// Whether or not the credential requires a password.
2017-05-15 12:27:07 -07:00
var requiresPassword : Bool {
return self == .password
}
/// Whether or not the credential requires a phone number.
var requiresPhone : Bool {
return self == .phone
}
2017-05-15 12:27:07 -07:00
}
fileprivate extension User {
var textDescription: String {
return self.displayName ?? self.email ?? self.uid
}
}