Referencing a single instance of a class for entire application (Firebase) - ios

I am using Firebase as my backend, when a user signs up I create a new document in my Firestore DB and then start a listener on that document. When a user signs out I want to be able to stop listening for security reasons.
I currently have a standard Session listener class that handles Firebase authentication and detects when a user has logged in / logged out:
SessionListener
class SessionListener : ObservableObject {
#Published var session: User? { didSet { self.didChange.send(self) }}
var carRepo: CarRepository = CarRepository()
var didChange = PassthroughSubject<SessionListener, Never>()
var handle: AuthStateDidChangeListenerHandle?
func listen () {
// monitor authentication changes using firebase
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
// if we have a user, create a new user model
self.session = User(uid: user.uid,name,email: user.email)
self.carRepo.initialiseUser()
} else {
// if we don't have a user, set our session to nil
self.session = nil
}
}
}
...
//Sign in / Sign up methods below
I also have a repository class to communicate with my Firestore DB (set up listeners etc.)
CarRepository
class CarRepository: ObservableObject {
let db = Firestore.firestore()
#Published var cars = [Car]()
#Published var listener: ListenerRegistration?
init(){
loadData()
}
func detatchListener(){
if(self.listener != nil) {
self.listener!.remove()
}
}
func loadData() {
if(Auth.auth().currentUser?.uid != nil){
self.listener = db.collection("users").document(Auth.auth().currentUser!.uid).collection("cars").addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
querySnapshot.documents.compactMap { document in
try? self.cars.append(document.data(as: Car.self)!)
}
}
}
}
}
...
//Add user to DB below
My problem is that I need to access this CarRepository from both the SessionListener (detect when a user logs out so that I can call detachListener(), and I also need to access CarRepository in a viewModel to parse the cars array.
The problem is that this is creating two separate instances of CarRepository (multiple listeners are being set up for the same document, and only the instance was created by SessionListener will have detachListener() called. Instead, I want there to be a single object that all classes/structs have access to.

Have a singleton manager class for the database interaction. In that class, you can have a property of type [String: CarRepository]. The String key is the path to your document. This way you can guarantee only 1 object per path.
Whenever you think of something that needs to be unique and live with your application, think of singleton.

Related

How do I rerender my view in swiftUI after a user logged in with Google on Firebase?

I'm fairly new to iOS Programming and swiftUI in particular.
I have the issue that I try to integrate firebase auth in my App in order to manage users.
Now the login, and log out basically works, the issue is, that after logging in, my view (which conditionally renders eighter the Google sign-in button or a list of content does not rerender so I still see the sign-in button even though I'm signed in).
I have set an observable Object to hold my auth status but unfortunately, it does not reload the current user automatically. So I set up a function to reload it manually which I would like to trigger when logging in. This works for the logout button but the logging in finishes in the AppDelegate, where for some reason I can't access the reloadUser() function.
I'm sure there is a better way to do this and would appreciate any help!
The Environment:
final class UserData: ObservableObject {
#Published var showFavoritesOnly = false
#Published var qrTags = qrTagData
#Published var user: User? = Auth.auth().currentUser
func reloadUser() -> Void {
self.user = Auth.auth().currentUser
}
}
The View I'd like to render:
struct MyQuaggsList: View {
#EnvironmentObject private var userData: UserData
var body: some View {
Group {
if getLogInState() != nil {
VStack {
NavigationView {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.qrTags) { qrTag in
if !self.userData.showFavoritesOnly || qrTag.isFavorite {
NavigationLink(
destination: QuagDetail(qrTag: qrTag)
.environmentObject(self.userData)
) {
QuaggRow(qrTag: qrTag)
}
}
}
}
.navigationBarTitle(Text("My Quaggs"))
}
SignOutButton()
}
} else {
SignInView()
}
}.onAppear(perform: {self.userData.reloadUser()})
}
func getLogInState() -> User? {
return Auth.auth().currentUser
}
}
Also, note there is the .onAppear() function which unfortunately only triggers on the initial appear not on the reappearance of the view after the user logged in.
Thanks so much in advance! It has been really frustrating.
The firebase and swiftUI combination is kinda tricky at first, but you will figure out that the same pattern is used in every single project, no worries.
Just follow my steps and customise on your project, here is our strategy.
- This might be a long answer, but i want to leave it as a refrence to all Firebase-SwiftUI user Managing in Stack OverFlow. -
Creating a SessionStore class which provides the BindableObject, and listen to your users Authentification and Handle the Auth and CRUD methods.
Creating a Model to our project ( you already did it)
Adding Auth methods in SessionStore Class.
Listening for changes and putting things together.
Let s start by SessionStore Class:
import SwiftUI
import Firebase
import Combine
class SessionStore : BindableObject {
var didChange = PassthroughSubject<SessionStore, Never>()
var session: User? { didSet { self.didChange.send(self) }}
var handle: AuthStateDidChangeListenerHandle?
func listen () {
// monitor authentication changes using firebase
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
// if we have a user, create a new user model
print("Got user: \(user)")
self.session = User(
uid: user.uid,
displayName: user.displayName
)
} else {
// if we don't have a user, set our session to nil
self.session = nil
}
}
}
// additional methods (sign up, sign in) will go here
}
Notice that we’ve declared that our session property is an optional User type, which we haven’t yet defined. Let’s quickly make one:
class User {
var uid: String
var email: String?
var displayName: String?
init(uid: String, displayName: String?, email: String?) {
self.uid = uid
self.email = email
self.displayName = displayName
}
}
Now, adding signUp, signIn and signOut methods
class SessionStore : BindableObject {
// prev code...
func signUp(
email: String,
password: String,
handler: #escaping AuthDataResultCallback
) {
Auth.auth().createUser(withEmail: email, password: password, completion: handler)
}
func signIn(
email: String,
password: String,
handler: #escaping AuthDataResultCallback
) {
Auth.auth().signIn(withEmail: email, password: password, completion: handler)
}
func signOut () -> Bool {
do {
try Auth.auth().signOut()
self.session = nil
return true
} catch {
return false
}
}
}
Finally, we need a way to stop listening to our authentication change handler.
class SessionStore : BindableObject {
// prev code...
func unbind () {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
}
Finally, Making our content view:
import SwiftUI
struct ContentView : View {
#EnvironmentObject var session: SessionStore
var body: some View {
Group {
if (session.session != nil) {
Text("Hello user!")
} else {
Text("Our authentication screen goes here...")
}
}
}
}
#Ghazi Tozri thanks again for your answer, while it wasn't what I wanted to do exactly it pushed me in the right direction.
I just want to put here what I finally did so if anyone wants to not use Email / Password sign-in but Google sign-in they can benefit from it too.
I used the Combine Framework + the #Publisher Syntax to make it a bit more readable and I also don't need the Signing in and out Methods because Google Provides them.
The SwiftUI Button for Google sign-in would look something like this:
struct GoogleSignIn : UIViewRepresentable {
#EnvironmentObject private var userData: SessionStore
func makeUIView(context: UIViewRepresentableContext<GoogleSignIn>) -> GIDSignInButton {
let button = GIDSignInButton()
button.colorScheme = .dark
GIDSignIn.sharedInstance()?.presentingViewController = UIApplication.shared.windows.last?.rootViewController
//If you want to restore a session
//GIDSignIn.sharedInstance()?.restorePreviousSignIn()
return button
}
func updateUIView(_ uiView: GIDSignInButton, context: UIViewRepresentableContext<GoogleSignIn>) {
}
}
And the Used SessionStore like this:
import SwiftUI
import Combine
import Firebase
import GoogleSignIn
final class SessionStore: ObservableObject {
#Published var showFavoritesOnly = false
#Published var qrTags = qrTagData
#Published var session: User?
var handle: AuthStateDidChangeListenerHandle?
func listen() {
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
self.session = user
} else {
self.session = nil
}
}
}
}
In a view that checks for the authentication state I use the .onAppear() function like this:
struct UserProfile: View {
#EnvironmentObject private var session: SessionStore
var body: some View {
VStack {
if session.session != nil {
SignOutButton()
} else {
SignInView()
}
}.onAppear(perform: {self.session.listen()})
}
}
To sign out this function will do (from the Firebase Docs):
func signOut() -> Void {
let firebaseAuth = Auth.auth()
do {
try firebaseAuth.signOut()
}
catch let signOutError as NSError {
print ("Error signing out: %#", signOutError)
}
}
Hope this can help somebody else too.

Checking user authentication using Google Sign In and SwiftUI

I've successfully set up authentication within my app using Google Sign-In to where I am able to return a Firebase User. I am attempting to set up a Sign-In screen that is only shown when there is no authenticated Firebase User, however with my current code the Sign-In screen is always visible even though I am consistently returning an authenticated user.
I've implemented the didSignInFor function in AppDelegate
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error?) {
// ...
if let error = error {
print(error.localizedDescription)
return
}
guard let authentication = user.authentication else { return }
let credential = GoogleAuthProvider.credential(withIDToken: authentication.idToken,
accessToken: authentication.accessToken)
// ...
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print(error.localizedDescription)
return
}
let session = FirebaseSession.shared
if let user = Auth.auth().currentUser {
session.user = User(uid: user.uid, displayName: user.displayName, email: user.email)
print("User sign in successful: \(user.email!)")
}
}
}
as well as a few lines in didFinishLaunchingWithOptions that sets the isLoggedIn property of my ObservableObject FirebaseSession
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
let auth = Auth.auth()
if auth.currentUser != nil {
FirebaseSession.shared.isLoggedIn = true
print(auth.currentUser?.email!)
} else {
FirebaseSession.shared.isLoggedIn = false
}
//Cache
let settings = FirestoreSettings()
settings.isPersistenceEnabled = false
GIDSignIn.sharedInstance().clientID = FirebaseApp.app()?.options.clientID
GIDSignIn.sharedInstance().delegate = self
return true
}
My ObservableObject
class FirebaseSession: ObservableObject {
static let shared = FirebaseSession()
init () {}
//MARK: Properties
#Published var user: User?
#Published var isLoggedIn: Bool?
#Published var items: [Thought] = []
var ref: DatabaseReference = Database.database().reference(withPath: "\(String(describing: Auth.auth().currentUser?.uid ?? "Error"))")
//MARK: Functions
func listen() {
_ = Auth.auth().addStateDidChangeListener { (auth, user) in
if auth.currentUser != nil {
self.isLoggedIn = true
}
if let user = user {
self.user = User(uid: user.uid, displayName: user.displayName, email: user.email)
} else {
self.user = nil
}
}
}
}
Finally, I perform my authentication check in the main view of my app here accessing FirebaseSession via my ObservedObject
struct AppView: View {
#ObservedObject var session = FirebaseSession.shared
#State var modalSelection = 1
#State var isPresentingAddThoughtModal = false
var body: some View {
NavigationView {
Group {
if session.isLoggedIn == true {
ThoughtsView()
} else {
SignInView()
}
}
}
}
}
As mentioned above my check doesn't seem to work. Even though my user is authenticated, SignInView is always visible.
How can I successfully check my user authentication each time my app loads?
UPDATE
I am now able to check authentication when the app loads, but after implementing Sohil's solution I am not observing realtime changes to my ObservableObject FirebaseSession. I want to observe changes to FirebaseSession so that after a new user signs in, the body of AppView will be redrawn and present ThoughtsView instead of SignInView. Currently I have to reload the app in order for the check to occur after authentication.
How do I observe changes to FirestoreSession from AppView?
You need to do something like this. I didn't try running this so I'm not sure if there are any typos...
class SessionStore : ObservableObject {
#Published var session: FIRUser?
var isLoggedIn: Bool { session != nil}
var handle: AuthStateDidChangeListenerHandle?
init () {
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
self.session = user
} else {
self.session = nil
}
}
}
deinit {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
}
in your component:
struct AppView: View {
#ObservedObject var session = SessionStore()
var body: some View {
Group {
if session.isLoggedIn {
...
} else {
...
}
}
}
}
Note the important thing here is that the object that is changing is #Published. That's how you will receive updates in your view.
Your problem is in accessing the objects and it's value. Means, in AppDelegate.swift file you are creating an object of FirebaseSession and assigning the values, but then in your AppView you are again creating a new object of FirebaseSession which creates a new instance of the class and all the values are replaced to default.
So, you need to use the same object throughout our application lifecycle, which can be done by defining the let session = FirebaseSession() globally or by creating a Singleton Class like below.
class FirebaseSession: ObservableObject {
static let shared = FirebaseSession()
private init () {}
//Properties...
//Functions...
}
Then you can access the shared object like this:
FirebaseSession.shared.properties
This way your assigned values will be preserved during the app lifecycle.
I don't know if it should be useful for you, but I read now your question because I was finding a solution for a similar issue for me, so I found it and I'm going to share with you.
I thought about using a delegate. So I created a protocol with the name ApplicationLoginDelegate in my AppDelegate class.
I define the protocol in this way:
protocol ApplicationLoginDelegate: AnyObject {
func loginDone(userDisplayName: String)
}
And in the AppDelegate class I define the loginDelegate:
weak var loginDelegate: ApplicationLoginDelegate?
You can call the delegate func in your didSignIn func
Auth.auth().signIn(with: credential) { (res, error) in
if let err = error {
print(err.localizedDescription)
return
} else {
self.loginDelegate?.loginDone(userDisplayName: (res?.user.displayName)!)
}
}
So in the SceneDelegate you use your delegate as I show you:
class SceneDelegate: UIResponder, UIWindowSceneDelegate, ApplicationLoginDelegate {
var window: UIWindow?
var eventsVM: EventsVM?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Set the app login delegate
(UIApplication.shared.delegate as! AppDelegate).loginDelegate = self
// Environment Objects
let eventsVM = EventsVM()
self.eventsVM = eventsVM
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
.environmentObject(eventsVM)
// Use a UIHostingController as window root view controller.
[...]
}
func loginDone(userDisplayName: String) {
guard let eventsVM = self.eventsVM else { return }
eventsVM.mainUser = User(displayName: userDisplayName)
}
So when you have updated your Environment Object, that has itself a #Publisced object (for example an User object), you receive your updates everywhere you define and call the Environment Object!
That's it!

singleton class shared between app and extension

When i add value like reference for UIViewController or string Id to singleton class, then try to access them from share extension i could't get this value again
the share extension create new singleton with null value
How i can make this class and the data inside it shared between main app and extension?
class Gateway:NSObject {
private var id:String? = nil
private weak var delegate:GatewayDelegate!
func set(id:String){
self.id = id
}
func set(gatewayDelegate:GatewayDelegate){
self.delegate = gatewayDelegate
}
func dismiss() {
self.id = nil
self.delegate = nil
}
func append(str:String,_id:String) {
if let id = self.id ,id == _id ,self.delegate != nil {
self.delegate!.gatewayAppend(str: str)
}
}
static let shared = Gateway()
private override init() {}
}
An extension is a completely separate process. It runs in its own sandbox and its own memory. Both your main app and your extension can create an instance of the singleton object, but they will be separate instances. They won't share any data.
To exchange data between your main app and your extension you will need to use an app group with user defaults, the keychain or a file.

Detach from Firestore listener

I'm using Firestore together with Swift.
I have a singleton data class UserManager. I call this from my different ViewControllers to get data to populate my tableviews. I want the tableviews to automatically update when the collections are updated so I need to use a SnapshotListener. Everything works fine but I'm not sure how to detach from the listener when the Viewcontroller is closed.
In the singleton class I have methods like this below. The method gives a list of users and will be called from several different places around my app.
I also want to give back a reference to the listener so that I can detach from it when the Viewcontroller is closed. But I can't get it working. The below solution gives compiler error.
I've been trying to look at the reference, for example here
https://firebase.google.com/docs/firestore/query-data/listen but I need to get it working when the data is loaded in a singleton class instead of directly in the Viewcontroller. What is the way to go here?
In UserManager:
func allUsers(completion:#escaping ([User], ListenerRegistration?)->Void) {
let listener = db.collection("users").addSnapshotListener { querySnapshot, error in
if let documents = querySnapshot?.documents {
var users = [User]()
for document in documents {
let user = User(snapshot: document)
users.append(user)
}
completion(users, listener)
}
}
}
In ViewController:
override func viewDidLoad() {
super.viewDidLoad()
UserManager.shared.allUsers(completion: { (users, listener) in
self.users = users
self.listener = listener
self.tableView.reloadData()
})
}
deinit {
self.listener.remove()
}
I guess the compiler error that you see is referring to the fact that you are using listener into it's own defining context.
Try this for a change:
In UserManager:
func allUsers(completion:#escaping ([User])->Void) -> ListenerRegistration? {
return db.collection("users").addSnapshotListener { querySnapshot, error in
if let documents = querySnapshot?.documents {
var users = [User]()
for document in documents {
let user = User(snapshot: document)
users.append(user)
}
completion(users)
}
}
}
In ViewController:
override func viewDidLoad() {
super.viewDidLoad()
self.listener = UserManager.shared.allUsers(completion: { (users) in
self.users = users
self.tableView.reloadData()
})
}
deinit {
self.listener.remove()
}
I think that getDocument instead of addSnapshotListener is what you are looking for.
Using this method the listener is automatically detached at the end of the request...
It will be something similar to
func allUsers(completion:#escaping ([User])->Void) {
db.collection("users").getDocument { querySnapshot, error in
if let documents = querySnapshot?.documents {
var users = [User]()
for document in documents {
let user = User(snapshot: document)
users.append(user)
}
completion(users)
}
} }

User object location in iOS (swift)

I have a user object, which I would like to store the currently logged in user for, as well as have functions that will allow the user to do things, such as logout, login, etc.
Following various tutorials and plenty of posts on here, I've come up with some code that works well so far when logging in via the login page. Here is that code:
import Foundation
import UIKit
// define the protocol for using this class
protocol LoginProtocol {
func didRecieveLoginResponse(status: Bool, message: String?)
}
class User : APIControllerProtocol {
var delegate: LoginProtocol
var username: String?
var company: String?
var auth_token: String?
// more properties here
// initiate the class, must have a delegate as per the protocol
init(delegate: LoginProtocol, username: String?, school: String?) {
self.delegate = delegate
if(username != nil) {
self.username = username
self.company = company
}
}
// login using the API
func login(password: String) {
print("Logging in as " + username! + " - " + company!)
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
let api = APIController(delegate: self)
var postFields = [String: String]()
postFields["username"] = username
postFields["password"] = password
postFields["company"] = company
api.request("login",postData: postFields)
}
// API results were received, log in the user if appropriate
func didRecieveAPIResults(originalRequest: String,apiResponse: APIResponse) {
dispatch_async(dispatch_get_main_queue(), {
if(apiResponse.status == true) {
self.auth_token = details["auth_token"]
// more properties here
self.save_to_user_defaults()
self.delegate.didRecieveLoginResponse(true, message:nil )
} else {
self.delegate.didRecieveLoginResponse(false, message:apiResponse.message )
}
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
})
}
// save the properties to user defaults
func save_to_user_defaults() {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(self.username, forKey: "username")
defaults.setObject(self.company, forKey: "company")
defaults.setObject(self.auth_token, forKey: "auth_token")
// more properties here
}
// load the properties from user defaults
func load_from_user_defaults() {
let defaults = NSUserDefaults.standardUserDefaults()
self.username = defaults.objectForKey("username") as? String
self.company = defaults.objectForKey("company") as? String
self.auth_token = defaults.objectForKey("auth_token") as? String
// more properties here
}
}
The next stage for me is logging in the user via NSUserDefaults - my plan to do this is via something like this:
let user = User()
user.load_from_user_defaults()
However I'm not quite sure:
Whether I'm on the right track (this is my first complete swift app)
If I am, where to put the above 2 lines of code (perhaps the app delegate?), such that when the app is opened, the user in NSUserDefaults (if there is one) is logged back in
How to allow the rest of the app access to the user (I'd like to be able to be able to reference user data in my view controllers, e.g. let pointsLabel.text = users.points)
Thank you so much in advance!
Hope, my answer will help in improving your app:
NSUserDefaults is not secure, it can be easily opened and read, both on device & to a Mac. So user defaults is a good place for preferences and config info, however it's not a good for sensitive information, like passwords. Check the link for more details
Yes, it should be in app delegate. Load the user data and present the view accordingly.
Either you can pass user data to view controller or just read from persistent storage. Depends upon your requirement.

Resources