Cache server data globally and refresh views - ios

I'm building an app that uses firebase for authentication and database functionality. Once a user signs up, a database record is stored for that user, containing some basic information like first name, last name etc.
Once a user logs in with his credentials I want to set a global variable (perhaps userDefaults?) which contains the user data for that specific user. Otherwise I have to fetch user data for every time I want to fill a label with for instance, a user's first name.
I managed to set userdefaults upon login and use this info in UIlables. But when I let users make changes to their data, of which some is important for the functioning of the app, I can update the server AND the userdefaults but the app itself doesn't update with the correct data. It keeps the old data in (for example) UIlables.
I would love to get some more insight on what the best work-flow is to manage situations like these.
When opening the app, i have a tabBarController set as rootviewcontroller. In the load of tabbarcontroller I have the following code retrieving the user data from firebase and saving it to userdefaults:
guard let uid = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("users").child(uid).observeSingleEvent(of: .value, with: { (snapshot) in
print(snapshot.value ?? "")
guard let dictionary = snapshot.value as? [String: Any] else { return }
let firstname = dictionary["First name"] as? String
let lastname = dictionary["Last name"] as? String
print("first name is: " + firstname!)
UserDefaults.standard.set(firstname, forKey: "userFirstName")
print(UserDefaults.standard.value(forKey: "userFirstName"))
self.setupViewControllers()
}
Then I continue on loading in all the viewcontrollers in the tabBarController:
self.setupViewControllers()
During that process the labels in those viewcontrollers get filled in with the userdefaults data.
This is an example of a label being filled in with userDefaults but not being updated upon changing of userdefaults:
let welcomeLabel: UILabel = {
let label = UILabel()
let attributedText = NSMutableAttributedString(string: "Welcome ")
attributedText.append(NSAttributedString(string: "\(UserDefaults.standard.string(forKey: "userFirstName")!)"))
label.attributedText = attributedText
label.font = UIFont.systemFont(ofSize: 30, weight: .bold)
return label
}()
this is a function i'm using to update the first name (via a textfield filled in by the user):
#objc func updateName() {
guard let uid = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("users").child(uid).updateChildValues(["First name" : updateNameField.text ?? ""])
UserDefaults.standard.set(updateNameField.text, forKey: "userFirstName")
print(UserDefaults.standard.value(forKey: "userFirstName"))
}

So you'll have to organize things first. In a new file define constants such as below. These constant will be accessible in global scope unless private
Constants.swift
private let storedusername = "usname"
private let storedName = "uname"
private let displaypic = "udp"
private let aboutme = "udesc"
var myusername : String {
get {
return (UserDefaults.standard.string(forKey: storedusername)!)
} set {
UserDefaults.standard.set(newValue, forKey: storedusername)
}
}
var myname : String {
get {
return (UserDefaults.standard.string(forKey: storedName)!)
} set {
UserDefaults.standard.set(newValue, forKey: storedName)
}
}
var myProfileImage : Data {
get {
return (UserDefaults.standard.data(forKey: displaypic)!)
} set {
UserDefaults.standard.set(newValue, forKey: displaypic)
}
}
var myAboutMe : String? {
get {
return (UserDefaults.standard.string(forKey: aboutme)!)
} set {
UserDefaults.standard.set(newValue, forKey: aboutme)
}
}
Now the next time you want to save anything in UserDefaults, you'll just do the following anywhere throughout your code base :
myusername = "#CVEIjk"
And to retrive it, just call it :
print(myusername)
IMPORTANT NOTE --
Always remember to initialize them. You can do this as the user signs up. As soon as they fill out their details and hit submit, just save them to these variables. That wouldn't cause unnecessary crash.
You'll have to save them at every location you perform updates regarding these nodes in the database.
Now, the refreshing views part. I am taking a scenario where your ProfileView.swift has the view and user goes to EditProfile.swift for updating the content.
You initialize all your observers the place where the update will have the immediate effect. Because the view immediately after the update matters. The rest will be called through the getter of the aboutme
ProfileView.swift
func openEditView() {
NotificationCenter.default.addObserver(self, selector: #selector(fetchUserDetails), name: Notification.Name("update"), object: nil)
//setting this right before the segue will create an observer specifically and exclusively for updates. Hence you don't have to worry about the extra observers.
perform(segue: With Identifier:)// Goes to the editProfile page
}
This function will be initially called in viewDidLoad(). At this time you need to make sure you have all the data, else it will produce no values. But if you are storing everything as the user signs up, you are safe.
#objc func fetchUserDetails() {
if uid != nil {
if myname.count > 0 { // This will check if the variable has anything in the memory or not. Dont confuse this with [Array].count
self.nameLabel = myname
}
}
}
This function also acts an ab observer method. So when the notifications are posted they can run again.
Now, EditProfile.swift
In the block where you are updating the server, save the values and then create a Notification.post and put this method right before you dismiss(toViewController:)
func updateUserCacheData(name: String, username: String, aboutme: String, ProfilePhoto: UIImage? = nil) {
DispatchQueue.global().async {
myname = name
myusername = username
myAboutMe = aboutme
if self.newImage != nil {
myProfileImage = self.newImage!.jpegData(compressionQuality: 1)!
}
DispatchQueue.main.async {
NotificationCenter.default.post(name: .refreshProfileViews, object: nil)
}
}
}
func updateToServerAndBackToProfileView() {
self.updateUserCacheData(name: iname!, username: iusername, aboutme: iaboutme!)
self.dismiss(animated: true, completion: nil)
}
}
As long as this goes back to ProfileView, your views will be instantly refreshed. You can keep an observer wherever you view will be first displayed after the dismiss. the rest will fetch updated content always. Also, don't forget to deinit your Observer in ProfileView
//This code is in ProfileView.swift
deinit {
NotificationCenter.default.removeObserver(self, name: Notification.Name("update"), object: nil)
}
Also, in cases where the content might be empty, simply initialize it with empty content. For example, if user doesn't choose to add aboutme while signing up, you can just put
`myaboutme = ""`
This will create a safe environment for you and you are well set.

Related

How to use static variable to keep track of the increment?

I am using static variable to keep track of the number of hotels added in firebase. Let's say, in start, the value of static variable is 1, then when data is added in firebase, the number is incremented to 2. But, when again the data is added and this view controller is loaded again, the value of static variable gets back to 1 and the new data posted replace the older data. How can I manage that thing? I know that pretty basic and silly question, but sometimes the brain just don't work. Below is the code.
class OwnerAddListing2ViewController: UIViewController {
static var numberOfHotels:Int = 1
let DataForCurrency : [String] = ["USD", "Rs"]
let DataForDays : [String] = ["PerNight", "PerWeek", "PerMonth"]
override func viewDidLoad() {
super.viewDidLoad()
currencyField.inputView = currencyPicker
daysField.inputView = daysPicker
}
#IBAction func nextButtonTapped(_ sender: UIButton) {
let currency = currencyField.text
let charges = chargesField.text
let days = daysField.text
let phone = phoneField.text
let email = emailField.text
//Get reference to firebase Database
let db = Firestore.firestore()
//Post data tw database
db.collection("Property").document("\(Auth.auth().currentUser!.uid)").collection("Hotel").document("\(OwnerAddListing2ViewController.numberOfHotels)").setData(["Currency": currency!, "Charges" : charges!, "Days" : days!, "Phone" : phone!, "EmailAddress" : email!], merge: true) {(error) in
if error != nil {
}
else {
print("Data Posted Succesfully")
OwnerAddListing2ViewController.numberOfHotels = OwnerAddListing2ViewController.numberOfHotels + 1
}
}
}
Static Variables only keep the data saved in One application Life cycle. As soon as you restart the application, the static variable will be initialized from the default value. If you want to persist the value of your variables throughout , may be you should consider using UserDefaults which can store small amount of information. But be careful not to store any sensitive data like passwords.

How to store Firebase data to access easily and fast in Swift

I am working on my user profile page on my app and I need to retrieve data from Firebase and display the data on my user profile page. I have managed to retrieve the data successfully from the database but the problem is that every time I go to the user profile page, it takes a little time to access the information in the database, so as soon as you go to the user profile page, the page will be empty. How can I avoid this?
Put another way, is there a way to access the data and store before going to the user profile page and then displaying the data that is stored? Here is my code:
// Setup the name label
func setupNameLabel() {
// Access the database and get the current user's name
Database.database().reference().child("Users").child(userID!).child("Name").observeSingleEvent(of: .value) { (snapshot) in
guard let name = snapshot.value as? String else { return }
self.nameLabel.text = name
}
view.addSubview(nameLabel)
}
// Setup the username label
func setupUsernameLabel() {
// Access the database and get the current user's username
Database.database().reference().child("Users").child(userID!).child("Username").observeSingleEvent(of: .value) { (snapshot) in
guard let username = snapshot.value as? String else { return }
self.usernameLabel.text = username
}
view.addSubview(usernameLabel)
}
// Setup the email label
func setupEmailLabel() {
// Access the database and get the current user's email
Database.database().reference().child("Users").child(userID!).child("Email").observeSingleEvent(of: .value) { (snapshot) in
guard let email = snapshot.value as? String else { return }
self.emailLabel.text = email
}
view.addSubview(emailLabel)
}
Try To make a Model and then using Single Method and Get Data with Completion
func getData(forUserID: String, completion: #escaping (Model) -> Swift.Void, error: #escaping (Bool) -> Void) {
}
Check models of Salada cocoapod, please.
https://github.com/1amageek/Salada
Especially the models like Relation, Disposer, Set, Array, File are very beautiful.

How to store user data as string, and load that string in viewDidLoad

I am trying to load a value that has been inputted by the user in the viewDidLoad via a String. I am using UserDefaults to save the users value that they input into a UITextField (userValue), I then save this to the String 'search'. I am able to print out the value of search in the GoButton function, and it works fine, but when I load my ViewController as new, the value of 'search' is equal to nil. The aim here is to have the users previous search saved, and loaded into the UITextField (that is used as a search box) upon loading the ViewController.
Code Below:
class ViewController: UIViewController {
#IBOutlet weak var userValue: UITextField!
var search: String!
}
viewDidLoad:
override func viewDidLoad() {
if (search != nil)
{
userValue.text! = String (search)
}
}
Button Function:
#IBAction func GoButton(_ sender: Any) {
let userSearch: String = userValue.text!
let perference = UserDefaults.standard
perference.set(userSearch, forKey: "hello")
perference.value(forKey: "hello")
let value = perference.value(forKey: "hello") as! String
search = value
print (search) // <<this works, it prints out the users search value
}
#VishalSharma has the right idea, but the code should probably look more like…
override func viewDidLoad() {
super.viewDidLoad()
if let search = UserDefaults.standard.string(forKey: "hello") {
userValue.text = search
}
}
or even more simply…
userValue.text = UserDefaults.standard.string(forKey: "hello")
When you load, search is effectively nil.
So either you read userDefaults in viewDidload or you come through a segue: then you can load search in the prepare.
I've always found it convenient and useful to store all UserDefault properties as an extension within the same file along with their getters and setters. It is far easier to maintain, use and read. by using the #function keyword for the key you are referencing the variable's name and not a string that can be accidentally changed somewhere else in code.
UserDefaults.swift
import Foundation
// An Extension to consolidate and manage user defaults.
extension UserDefaults {
/// A value Indicating if the user has finished account setup.
/// - Returns: Bool
var finishedAcountSetup: Bool {
get { return bool(forKey: #function) }
set { set(newValue, forKey: #function) }
}
/// The hello text at the start of the application.
/// - Returns: String?
var helloText: String? {
get { return string(forKey: #function) }
set {set(newValue, forKey: #function) }
}
//etc...
}
When you use these values reference the standard settings:
//Setting
UserDefaults.standard.helloText = "Updated Hello Text"
// Getting
// for non-optional value you can just get:
let didCompleteSetup = UserDefaults.standard.finishedAcountSetup
// Otherwise, safely unwrap the value with `if-let-else` so you can set a default value.
if let text = UserDefaults.standard.helloText {
// Ensure there is text to set, otherwise use the default
label.text = text
} else {
// helloText is nil, set the default
label.text = "Some Default Value"
}
obviously, it provides nil because when view controller load the search is nil try this.
let perference = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
if (perference.value(forKey: "hello") != nil) {
search = perference.value(forKey: "hello") as! String
userValue.text! = String (search)
}
}

How can I stop UICollectionView from showing duplicate items after Firebase update

I have two UICollection views on a page that displays data about a Room. It includes photos of the room in one UICollection View and another UICollection View which contains a list of items in that room. There's a link to edit the Room. When a user clicks on the link, they then segue to another view that let's them update it including adding additional photos.
After adding a photo, and hitting submit, in the background the photo is uploaded to Firebase storage and in the Firebase database, the record is updated to include the name of the file that was just uploaded. Meanwhile, the user is segued back to the Room view.
There's a watched on the record of the room in Firebase and when it updates, then the view is refreshed with new data. This is where the problem occurs. It appears, based on a lot of debugging that I've been doing, that the Observe method fires twice and what ends up happening, is the UICollection view that holds the images of the room will show duplicates of the last photo added.
For example, if I add one photo to the room, that photo will appear in the collection 2x. I've attempted to clear the array before the array is updated with the images, and from my analysis, it appears that the array only contains two items, despite showing three in the view. I'm not sure what is happening that would cause this?
Here's a link to the entire file, because I think it might help.
Here's the loadData() method in case this is all that's important:
func loadData() {
self.ref = Database.database().reference()
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
guard let userID = Auth.auth().currentUser?.uid else { return }
let buildingRef = self.ref.child("buildings").child(userID)
buildingRef.keepSynced(true)
buildingRef.child(self.selected_building as String).observe(DataEventType.value, with: { (snapshot) in
let value = snapshot.value as? NSDictionary
if ((value) != nil) {
let building_id = value?["id"] as! String
let saved_image = value?["imageName"] as! String
let user_id = userID as! String
let destination = "/images/buildings/\(userID)/\(building_id)/"
let slideShowDictionary = value?["images"] as? NSDictionary
if ((slideShowDictionary) != nil) {
self.slideShowImages = [UIImage]()
self.slideShowCollection.reloadData()
var last_value = ""
slideShowDictionary?.forEach({ (_,value) in
print("are they different? \(last_value != (value as! String))")
if (last_value != value as! String) {
print("count: \(self.slideShowImages.count)")
print("last_value \(last_value)")
print("value \(value)")
last_value = value as! String
CloudStorage.instance.downloadImage(reference: destination, image_key: value as! String, completion: { (image) in
self.slideShowImages.append(image)
self.slideShowCollection.reloadData()
})
}
})
CloudData.instance.getBuildingById(userId: user_id, buildingId: building_id, completion: { (building) in
self.title = building.buildingName as String
self.roomsCollection.reloadData()
})
}
}
})
// User is signed in.
self.getRooms()
}
I am not completely familiar with the Firebase API but if you are having issues with the observation I would suspect the following:
#IBAction func unwindToRoomsVC(segue:UIStoryboardSegue) {
loadData()
}
Triggering loadData a second time looks like it would add a second observation block. As best I can tell the .observe method probably persists the block it is given and triggers it on all changes.

NSUserDefaults Loosing Value in Swift

Update: I've tried changing setValue to setObject, and the same error occurred.Upon further investigation with breakpoints and the LLDB, they are nil before the controller is even presented. I'm not saving them right.
I'm trying to simply save a couple of strings of text, and display them on another view using Swift. I'm not sure why I'm having such a hard time. Here is how I'm trying to accomplish this:
VC1
#IBAction func registerTapped(sender : AnyObject)
// Save the login information
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setValue(username.text, forKey: "username")
defaults.setValue(password.text, forKey: "password")
if firstName.text.isEmpty == false {
defaults.setValue(firstName.text, forKey: "firstname")
}
if lastName.text.isEmpty == false {
defaults.setValue(lastName.text, forKey: "lastname")
}
let profileView = ProfileViewController()
self.presentViewController(profileView, animated: true, completion: nil)
}
}
Cool. That looks like the correct way to save strings in UITextFields based upon my research. So, I open up VC2 and try to load the saved text into the new UITextField's, like so:
override func viewDidLoad() {
super.viewDidLoad()
let defaults = NSUserDefaults.standardUserDefaults()
username.text = defaults.stringForKey("username")
password.text = defaults.stringForKey("password")
if let first = defaults.stringForKey("firstname")
{
firstName.text = first
}
if let last = defaults.stringForKey("lastname") {
lastName.text = last
}
}
I get the crash fatal error: unexpectedly found nil while unwrapping an Optional value. I've been digging through tutorials for hours and can't for the life of me figure out what I am doing wrong.
Is it because it an an optional? This is my LLDB output:
Your issue has nothing to do NSUserDefaults, whats nil are your labels username, password, etc. in your second controller.
You should add a segue to your button (the one with registerTapped) to show the second controller and remove the last two lines in registerTapped.
Break your code into steps and debug each one. Your code would crash if your outlet is nil or if the key/value pair doesn't exist. Check that both username and password (The text fields) are not nil, as well as that the defaults results aren't nil:
var text: String?
text = defaults.stringForKey("username")
if let text = text
{
if let username = username
{
username.text = text
}
else
{
println("username field is nil!")
}
}
else
{
println("defaults stringForKey("username") = nil!")
}
text = defaults.stringForKey("password")
if let text = text
{
if let password = password
{
password.text = text
}
else
{
println("password field is nil!")
}
}
else
{
println("defaults stringForKey("password") = nil!")
}

Resources