Cache implementation method - swift

I want to save as cache as UserDefaults.
But,
This is signupButtonTapped:
#IBAction private func signUpButtonTapped(_ sender: Any) {
KRProgressHUD.show(withMessage: "loading")
Authentication.signUp(
name: nameTextField.text!, // "new"
email: emailTextField.text!, // "new"
password: passwordTextField.text!, // "new"
image: selectedImage,
onSuccess: {
KRProgressHUD.dismiss()
API.User.getCurrentUser { (user) in
CurrentUser.shared.synchronize(user: user)
}
self.presentViewController()
}) { (error) in
KRProgressHUD.showError(withMessage: "\(error!)")
}
}
this is CurrentUser class:
import Foundation
final class CurrentUser: User {
// Get
static let shared: CurrentUser = {
let user = CurrentUser()
let userDefaults = UserDefaults.standard
user.userID = userDefaults.string(forKey: userID_field)
user.name = userDefaults.string(forKey: name_field)
user.email = userDefaults.string(forKey: email_field)
return user
}()
// Save
func synchronize(user: user) {
print(user.name, user.userID, user.email) // "new", "new", "new"
// <<<< BREAKPOINT
let userDefaults = UserDefaults.standard
userDefaults.set(user.userID, forKey: userID_field)
userDefaults.set(user.name, forKey: name_field)
userDefaults.set(user.email, forKey: email_field)
print(userDefaults.string(forKey: name_field), userDefaults.string(forKey: userID_field), userDefaults.string(forKey: email_field)) // "new", "new", "new"
print(CurrentUser.shared.name, CurrentUser.shared.userID, CurrentUser.shared.email) // "old", "old", "old"
// <<<< BREAKPOINT
}
I want the last print(CurrentUser.shared.ooo) function to be "new", "new", "new".
However, the old value "old" is displayed instead.
What is wrong with my cache implementation?

Since you are using static instance shared that property will only hold the first value. You need to change your strategy to something else, because once your app is launched the computed property won't compute again and thus won't fetch data from userdefaults.
You see when you save your new data and then quit and launch your app again it should display the expected result, but since you are trying to get the data when your app is launched it will obviously display the previously stored one. A simple solution would be to not user shared instance for this, you could do something like following:
//Put the currentUser in User class
class User {
var userID = ""
var name = ""
var email = ""
class var current: User {
let user = User()
let defaults = UserDefaults.standard
user.email = defaults.string(forKey: email_field) ?? "" //this can be nil if no data
user.userID = defaults.string(forKey: userID_field) ?? ""
user.name = defaults.string(forKey: name_field) ?? ""
return user
}
func synchronize() {
let userDefaults = UserDefaults.standard
userDefaults.set(self.userID, forKey: userID_field)
userDefaults.set(self.name, forKey: name_field)
userDefaults.set(self.email, forKey: email_field)
print(userDefaults.string(forKey: name_field), userDefaults.string(forKey: userID_field), userDefaults.string(forKey: email_field))
print(User.current.name, User.current.userID, User.current.email)
}
}
Now you can perform your operations. Like so
let newUser = User()
newUser.userID = "123"
....
newUser.synchronize() //save
//When you need current user
User.current

Related

Swiftui - Fetch data after login

I'm starting to learn SwiftUI and i'm looking for better solution to fetch user data after successful login.
For example, i need to see if the user has the account blocked or not after login
For this i have create struct in Users.swift
struct Users: Identifiable, Codable{
#DocumentID var id: String?
var Societe : String
var Nom : String
var Prenom : String
var HasBlocked : Bool
var SiteID : [String]
var Poste : String
var Email : String
}
and firebase class in Firebase.swift
class Firebase: ObservableObject {
#EnvironmentObject var viewRouter: ViewRouter
#Published var user : Users? = nil
private var db = Firestore.firestore()
func fetchData() {
db.collection("Users").document(Auth.auth().currentUser!.uid).addSnapshotListener{ (snapshot, error) in
guard let documents = snapshot?.data() else {
print("No documents")
return
}
let name = documents["Nom"] as? String ?? ""
let surname = documents["Prenom"] as? String ?? ""
let email = documents["Email"] as? String ?? ""
let societe = documents["Societe"] as? String ?? ""
let poste = documents["Poste"] as? String ?? ""
let siteId = documents["SiteID"] as? [String] ?? []
let hasBlocked = documents["HasBlocked"] as? Bool ?? true
if let user = self.user?.SiteID{
print(user)
}else{
print("no document2")
}
self.user = Users(Societe: societe, Nom: name, Prenom: surname, HasBlocked: hasBlocked, SiteID: siteId, Poste: poste, Email: email)
}
}
}
and in the SignInView after press login button, i call func signInUser
#ObservedObject private var firebase = Firebase()
func signInUser(userEmail: String, userPassword: String) {
signInProcessing = true
Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
guard error == nil else {
signInProcessing = false
signInErrorMessage = error!.localizedDescription
return
}
switch authResult {
case .none:
print("Could not sign in user.")
signInProcessing = false
case .some(_):
print("User signed in")
// get data and check ??
signInProcessing = false
withAnimation {
viewRouter.currentPage = .homePage
}
}
}
}
But i don't know how to fetch data after login for check if the account has blocked and then pass the data to Homeview without re-fetch data to optimize call database.
thank for your help
Let me address this at a high level as your code seems fine but its's just wrapping your brain around the asynchronous calls.
I am pretty sure you have everything you need - here's the flow using pseudo code
authenticate { auth closure
read user document using auth.uid { snapshot closure
if snapshot has user blocked {
do not proceed to next view
} else { //user is not blocked
proceed to next view
}
}
}
Remember that Firebase data is only valid within the closure following the Firebase call and if you follow the above pseudo code, it does just that...
auth closure - has valid firebase auth information so you can get the uid
snapshot closure - has valid snapshot info so you can see if the user is blocked
the If statement will then determine the next thing to do - either don't show
the view if blocked or show it if not blocked
So borrowing from your code
func signInUser(userEmail: String, userPassword: String) {
Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
db.collection("Users").document(Auth.auth().currentUser!.uid).getDocument {
either go to next view or not
Note that you should be using getDocument in this case to read the document once instead of adding a listener.

Best way to use UserDefaults with ObservableObject in Swiftui

I'm trying to save user basic's data in UserDefaults.
My goal is to be able to consume data from UserDefaults and to update them each time the user do some changes.
I'm using an ObservableObject class to set and get these data
class SessionData : ObservableObject {
#Published var loggedInUser: User = User(first_name: "", last_name: "", email: "")
static let shared = SessionData()
func setLoggedInUser (user: User) {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(user) {
UserDefaults.standard.set(encoded, forKey: "User")
self.loggedInUser = currentUser
}
}
and also
struct ProfileView: View {
#ObservedObject var sessionData: SessionData = SessionData.shared
var body: some View {
VStack{
Text(self.sessionData.loggedInUser.first_name)
}
}
}
This way the changes are updated. But if I leave the app I will lose the data.
Solution 2:
I also tried to rely on reading the data from UserDefault like this
class SessionData : ObservableObject {
func getLoggedInUser() -> User? {
if let currentUser = UserDefaults.standard.object(forKey: "User") as? Data {
let decoder = JSONDecoder()
if let loadedUser = try? decoder.decode(User.self, from: currentUser) {
return loadedUser
}
}
return nil
}
}
Problem: I don't get the updates once a user change something :/
I don't find a nice solution to use both UserDefaults and ObservableObject
in "getLoggedInUser()" you are not updating the published var "loggedInUser".
Try this to do the update whenever you use the function:
func getLoggedInUser() -> User? {
if let currentUser = UserDefaults.standard.object(forKey: "User") as? Data {
let decoder = JSONDecoder()
if let loadedUser = try? decoder.decode(User.self, from: currentUser) {
loggedInUser = loadedUser // <--- here
return loadedUser
}
}
return nil
}
or just simply this:
func getLoggedInUser2() {
if let currentUser = UserDefaults.standard.object(forKey: "User") as? Data {
let decoder = JSONDecoder()
if let loadedUser = try? decoder.decode(User.self, from: currentUser) {
loggedInUser = loadedUser // <--- here
}
}
}
You could also do this to automatically save your User when it changes (instead of using setLoggedInUser):
#Published var loggedInUser: User = User(first_name: "", last_name: "", email: "") {
didSet {
if let encoded = try? JSONEncoder().encode(loggedInUser) {
UserDefaults.standard.set(encoded, forKey: "User")
}
}
}
and use this as init(), so you get back what you saved when you leave the app:
init() {
getLoggedInUser2()
}

Realm objects returns nil after adding

I created an Person object when the User logs in:
let creds = SyncCredentials.jwt(accessToken)
SyncUser.logIn(with: creds, server: Constants.syncAuthURL, onCompletion: { [weak self](user, err) in
if let user = user {
self?.setDefaultRealmConfiguration(with: user)
let realm = try! Realm()
let identity = (user.identity)!
let person = Person()
person.id = identity
try! realm.write {
realm.add(person, update: true)
}
self?.performSegue(withIdentifier: "showProfile", sender: self)
}
})
The Person successfully created on the cloud server.
In the next viewcontroller i like to fetch the object based on the id:
let realm = try! Realm()
guard let uuid = SyncUser.current?.identity! else { return }
let person = realm.objects(Person.self).filter("id = %#", uuid).first
try! realm.write {
person?.name = "test"
}
The person is always nil I also tried the query the object with the primary key, but with no success.
The Person class looks like:
class Person : Object {
#objc dynamic var id = ""
#objc dynamic var created: Date = Date()
#objc dynamic var name = ""
#objc dynamic var email = ""
#objc dynamic var avatar : Data?
override static func primaryKey() -> String? {
return "id"
}
}
UPDATE
I created a new app with just one Viewcontroller and the Person class:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let creds = SyncCredentials.usernamePassword(username: "admin", password: "test")
SyncUser.logIn(with: creds, server: Constants.AUTH_URL, onCompletion: { (user, err) in
if let user = user {
var config = user.configuration(realmURL: Constants.REALM_URL)
config.objectTypes = [Person.self]
Realm.asyncOpen(configuration: config, callback: { (realm, error) in
guard let realm = realm else {return}
let objects = realm.objects(Person.self)
print(objects) // always empty why???
try! realm.write {
let p = Person()
p.id = "test"
realm.add(p)
}
print(objects) // one object
})
}
})
}
}
as with my other problem the person is successfully added to the cloud. but when I restart the app the objects are empty on the first query. Maybe I miss understanding something with the synched realms?
let results = realm.objects(Person.self)
let subscription = results.subscribe()
print(results)
resolve my problem

Create an object from firebase database

I'm using firebase to store user records. When a user logs in, I am trying to pull the record and create a user object to pass around amongst the view controllers as opposed to hitting the database multiple times.
class User: NSObject {
var name: String?
var email: String?
}
I have a variable, myUser: User? in my controller and would like to assign the record retrieved from firebase to that variable.
func retrieveUserWith(uid: String) {
Database.database().reference().child("users").child(uid).observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
let user = User()
user.name = dictionary["name"] as? String
user.email = dictionary["email"] as? String
self.myUser = user
}
})
}
Now I understand that the firebase call is asynchronous and I can't directly assign the created user to myUser variable as shown above.
Is there another way to assign user to myUser to avoid hitting the database every time I switch view controllers?
This is not really the correct way to get the info you want. Firebase already offers a sharedInstance of the User.
if let user = Auth.auth().currentUser {
let name = user.displayName // should check that this exists
let email = user.email // should check that this exists
}
Nonetheless, to achieve this the way you are looking to do so:
class User: NSObject {
var name: String
var email: String?
static var sharedInstance: User!
init(name: String, email: String) {
self.name = name
self.email = email
}
}
Calls to firebase are asynchronous, so you should have a completionHandler that will get called when the call is finished:
func retrieveUserWith(uid: String, completionHandler: #escaping () -> ()) {
Database.database().reference().child("users").child(uid).observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
let name = dictionary["name"] as? String ?? ""
let email = dictionary["email"] as? String ?? ""
let user: User = User(name: name, email: email)
User.sharedInstance = user
completionHandler()
}
})
}
Then to use the sharedInstance of the user elsewhere in your app you can do the following:
if let user = User.sharedInstance {
// do stuff here
}

How to use FIRApp.createUserWithEmail in specific database reference?

Iv been converting my post Firebase 2 codebase to Firebase 3 and having some troubles.
So basically Im trying to figure out how I create a new user at a specific location on my Firebase DB.
Goal - I want to save all new users # var _USER_REF = FIRDatabaseReference().child("\(BASE_URL)/users")
Here is the code so far.
class DataService {
static let dataService = DataService()
let BASE_URL = "https://project-1321.firebaseio.com"
var _BASE_REF = FIRDatabaseReference().child(BASE_URL)
var _USER_REF = FIRDatabaseReference().child("\(BASE_URL)/users")
var _NEWS_REF = FIRDatabaseReference().child("\(BASE_URL)/news")
var _MARKET_STATS = FIRDatabaseReference().child("\(BASE_URL)/market")
var CURRENT_USER_REF: FIRDatabaseReference {
let userID = NSUserDefaults.standardUserDefaults().valueForKey("uid") as! String
let currentUser = FIRDatabaseReference().child("\(_BASE_REF)").child("users").child(userID)
//let currentUser = Firebase(url: "\(BASE_REF)").childByAppendingPath("users").childByAppendingPath(userID)
return currentUser
}
func createNewAccount(uid: String, user: Dictionary<String, String>) {
_USER_REF.child(uid).setValue(user)
}
}
View Controller
#IBAction func registerAccount(sender: AnyObject) {
guard let email = self.emailRegField.text where !self.emailRegField.text!.isEmpty else {
return
}
guard let username = self.usernameRegField.text where !self.usernameRegField.text!.isEmpty else {
return
}
guard let password = self.passwordRegField.text where !self.passwordRegField.text!.isEmpty else {
return
}
FIRAuth.auth()?.createUserWithEmail(email, password: password) {
(user, error) in
if error != nil {
print(error)
self.signUpErrorAlert("Alert", message: "There was a problem signing up!")
} else {
let user = ["provider": user?.providerID, "email": email, "username": username]
DataService.createNewAccount(user) // Doesnt Work
}
//Store UID in NSDefaults so if user reopen app they automatically log in if UID exsisits.
NSUserDefaults.standardUserDefaults().setValue(result ["uid"], forKey: "uid")
// Segue New User
self.performSegueWithIdentifier("newUserSegue", sender: nil)
}
// Loggin in a User who already has UID Saved to NSDefaults
When a user log's in or Registers I plan to save their "UID" to NSDefaults.
Then check like so :
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if NSUserDefaults.standardUserDefaults().valueForKey("uid") != nil {
self.performSegueWithIdentifier("newUserSegue", sender: nil)
} else {
print("User is not registered or has their UID saved to NSDefaults")
}
}
Is this a safe method?
#brkr In response to your comment above, you can still use the UIDs in order to add unique users to your Firebase database.
For example, here is your users ref (in the DataService Class):
let REF_USERS = FIRDatabase.database().referenceFromURL("\(URL_BASE)/users")
Now, create a user in Firebase 3.0:
FIRAuth.auth()!.createUserWithEmail(email, password: pwd, completion: { authData, error in
if error == nil {
// Log user in
FIRAuth.auth()?.signInWithEmail(email, password: pwd) { authData, error in
// Save user information to Firebase data
let user = // your user Dictionary
DataService.createNewAccount(authData!.uid, user: user)
}
} else {
// Handle login error here
}
})
The code for the create account method:
func createNewAccount(uid: String, user: Dictionary<String, String>) {
REF_USERS.child(uid).setValue(user)
}
I dont think it is necessary with new Firebase, look in the Dashboard, all your users should be under "Auth" tab,
Also this line doesnt make any sense in new Firebase, the URL you are querying is in the .plist you downloaded.
let BASE_URL = "https://project-1321.firebaseio.com" //remove this line
and use something like this
let firebaseRef = FIRDatabase.database().reference()
let newsRef = firebaseRef.child("news")
you can find many useful informations here https://firebase.google.com/docs/auth/ios/password-auth#sign_in_a_user_with_an_email_address_and_password