firebase / FirebaseUI-iOS

iOS UI bindings for Firebase.
Apache License 2.0
1.49k stars 467 forks source link

Add SwiftUI example #811

Open jtressle opened 4 years ago

jtressle commented 4 years ago

It'll be helpful to add a swiftUI example that uses the standard FirebaseUI. Here's skeleton code that works for me:

import SwiftUI
import FirebaseUI
import Firebase

public var screenWidth: CGFloat {
    return UIScreen.main.bounds.width
}

public var screenHeight: CGFloat {
    return UIScreen.main.bounds.height
}

struct LoginView : View {

    @State private var viewState = CGSize(width: 0, height: screenHeight)
    @State private var MainviewState = CGSize.zero

    var body : some View {
        ZStack {
            CustomLoginViewController { (error) in
                if error == nil {
                    self.status()
                }
            }.offset(y: self.MainviewState.height).animation(.spring())
            MainView().environmentObject(DataStore()).offset(y: self.viewState.height).animation(.spring())
        }
    }

    func status() {
        self.viewState = CGSize(width: 0, height: 0)
        self.MainviewState = CGSize(width: 0, height: screenHeight)
    }
}

struct LoginView_Previews : PreviewProvider {
    static var previews : some View {
        LoginView()
    }
}

struct CustomLoginViewController : UIViewControllerRepresentable {

    var dismiss : (_ error : Error? ) -> Void

    func makeCoordinator() -> CustomLoginViewController.Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIViewController
    {
        let authUI = FUIAuth.defaultAuthUI()

        let providers : [FUIAuthProvider] = [
            FUIEmailAuth(),
            FUIGoogleAuth(),
            FUIOAuth.appleAuthProvider()
        ]

        authUI?.providers = providers
        authUI?.delegate = context.coordinator

        let authViewController = authUI?.authViewController()

        return authViewController!
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<CustomLoginViewController>)
    {

    }

    //coordinator
    class Coordinator : NSObject, FUIAuthDelegate {
        var parent : CustomLoginViewController

        init(_ customLoginViewController : CustomLoginViewController) {
            self.parent = customLoginViewController
        }

        // MARK: FUIAuthDelegate
        func authUI(_ authUI: FUIAuth, didSignInWith authDataResult: AuthDataResult?, error: Error?)
        {
            if let error = error {
                parent.dismiss(error)
            }
            else {
                parent.dismiss(nil)
            }
        }

        func authUI(_ authUI: FUIAuth, didFinish operation: FUIAccountSettingsOperationType, error: Error?)
        {
        }
    }
}

In my SceneDelegate.swift, I'm calling the following in scene:willConnectTo:

// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
    let window = UIWindow(windowScene: windowScene)
    if Auth.auth().currentUser == nil {
        window.rootViewController = UIHostingController(rootView: LoginView())
    }
    else {
        window.rootViewController = UIHostingController(rootView:MainView().environmentObject(DataStore()))
    }

    self.window = window
    window.makeKeyAndVisible()
}

Hope this helps.

chrisyue1998 commented 4 years ago

What is DataStore() referring to in the line

MainView().environmentObject(DataStore()).offset(y: self.viewState.height).animation(.spring())
jtressle commented 4 years ago

@chrisyue1998 it's a swift class that handles my data store.

swells808 commented 4 years ago

@jtressle would you mind posting the datastore file I would like to understand better how that works with your auth login function?

My hope is that you are storing user info to pass on to firebase storage with it but, I am curious on the whole function.

jtressle commented 4 years ago

@swells808 my user info is all handled by the Firebase Auth library. The only check I do is check if the user has logged in. After that, I count on the read/write permissions on Firebase to handle object restrictions. As shown above, if the user is not logged in, I present the login view.

if Auth.auth().currentUser == nil {
        window.rootViewController = UIHostingController(rootView: LoginView())
    }
    else {
        window.rootViewController = UIHostingController(rootView:MainView().environmentObject(DataStore()))
    }

The datastore file does not contain any user info since we rely on Firebase read/write access restrictions.

Hope this helps.

swells808 commented 4 years ago

@jtressle Thank you that explains a lot! Follow up question what types of info do you use the DataStore for if not user info?

mattbrandman commented 4 years ago

Hi @jtressle I am trying to implement this in a sheet in swiftui so that it's a popup instead of a root view. However I am finding that at the end of the sign in flow the sheet closes but the delegate is not called.

When I use it as a root view everything works fine, any idea what may be causing the altered behavior in a swiftui sheet?

Interestingly it also works when using the popover modifier just not the sheet modifier

arthurgarzajr commented 3 years ago

@mattbrandman I just encountered that myself. The problem is when authentication is complete, the sheet is dismissed, so the delegate methods don't get called because the Authentication view is garbage collected. You need to use a different object to handle the delegate method callbacks, don't keep them in the Coordinator like the above example because the Coordinator will no longer exist when the sheet is dismissed.

pedrovelmo commented 3 years ago

@arthurgarzajr I am encountering the same issue right now. Any chance you can post some code with the different object handling the delegate callbacks?

smaldd14 commented 3 years ago

@arthurgarzajr I too am seeing what @mattbrandman is experiencing using the Auth view in the sheet. I've created an a class AuthuiDelegate that is basically the same as what the coordinator would be. I instantiate the object in the makeViewController method by doing let authuiDelegate = AuthuiDelegate(self) and set authui?.delegate = authuiDelegate. What am I missing?

arthurgarzajr commented 3 years ago

@pedrovelmo, @smaldd14,

I created a view model that implemented the FUIAuthDelegate protocol. The view model was then passed in as a delegate to the CustomLoginViewController (based off of @jtressle's example).

Here it is in it's entirety, going off of the OP's sample code. You'll find differences in the coordinator and how CustomLoginViewController is used. Notice the passed in delegate, which is my AuthViewModel.

import SwiftUI
import FirebaseUI
import Firebase

public var screenWidth: CGFloat {
    return UIScreen.main.bounds.width
}

public var screenHeight: CGFloat {
    return UIScreen.main.bounds.height
}

struct LoginView : View {

    @State private var viewState = CGSize(width: 0, height: screenHeight)
    @State private var MainviewState = CGSize.zero
    @ObservedObject var authViewModel = AuthViewModel()

    var body : some View {
        ZStack {
            CustomLoginViewController(delegate: authViewModel) { (error) in
                if error == nil {
                    self.status()
                }
            }.offset(y: self.MainviewState.height).animation(.spring())
            MainView().environmentObject(DataStore()).offset(y: self.viewState.height).animation(.spring())
        }
    }

    func status() {
        self.viewState = CGSize(width: 0, height: 0)
        self.MainviewState = CGSize(width: 0, height: screenHeight)
    }
}

struct LoginView_Previews : PreviewProvider {
    static var previews : some View {
        LoginView()
    }
}

struct CustomLoginViewController : UIViewControllerRepresentable {
    var delegate: FUIAuthDelegate?

    var dismiss : (_ error : Error? ) -> Void

    func makeCoordinator() -> CustomLoginViewController.Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIViewController
    {
        let authUI = FUIAuth.defaultAuthUI()

        let providers : [FUIAuthProvider] = [
            FUIEmailAuth(),
            FUIGoogleAuth(),
            FUIOAuth.appleAuthProvider()
        ]

        authUI?.providers = providers
        authUI?.delegate = self.delegate

        let authViewController = authUI?.authViewController()

        return authViewController!
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<CustomLoginViewController>)
    {

    }

    //coordinator
    class Coordinator: NSObject {
        var parent: CustomLoginViewController

        init(_ customLoginViewController : CustomLoginViewController) {
            self.parent = customLoginViewController
        }

    }
}

import FirebaseUI
class AuthViewModel: NSObject, ObservableObject, FUIAuthDelegate {

    func authUI(_ authUI: FUIAuth, didSignInWith authDataResult: AuthDataResult?, url: URL?, error: Error?) {
        print("Signed in as \(Auth.auth().currentUser?.displayName)")
    }

    func authUI(_ authUI: FUIAuth, didSignInWith authDataResult: AuthDataResult?, error: Error?) {
        print("Signed in")
    }

    func authUI(_ authUI: FUIAuth, didFinish operation: FUIAccountSettingsOperationType, error: Error?) {
        print("Signed in")
    }
}
sneakysquidd commented 3 years ago

I am trying to implement this and I was wondering if anyone had a sample project I could look at to help me implement this. I am having trouble with what the DataStore class does and how this all links together. I saw the explanation for datastore above but I'm still unsure how to implement it. Thanks

ianphaas commented 3 years ago

Hello,

I have implemented this and it works well. My only problem is that I can not figure out how to change the current view when the user logs in or out. Does anyone have an example showing how to do this?

nyxee commented 1 year ago

Some help with DataStore please.

Is it just an empty class or struct??

jtressle commented 1 year ago

@nyxee it was just a ObservableObject that handled some snapshot listeners. I believe I've since removed the environment object in favor of using the git Resolver (https://github.com/hmlongco/Resolver).

Basically, just handle loading data after the user has logged in.

nyxee commented 1 year ago

Why does it not work in preview? I lost three days on that.