How to keep data sources synchronized with firebase realtime database with Swift? - ios

I am using Firebase realtime database. Data structure is an array of posts, which user can also comment and like.
I retrieve data like this and put them into a local posts array:
ref.observe(.childAdded, with: { (snapshot) -> Void in
self.posts.append(snapshot)
self.tableView.reloadData()
})
They are displayed correctly and no problems so far. Now let's say user likes a post. I add his id to likers array of post in local posts array. However firebase database don't know this yet.
My question is what is the correct way to keep local data and firebase data synchronized?

The trick with Firebase is usually to only update the database when the user performs an action, such as liking a post. From that database update you then get a new event, for example a .childChanged for updating the likes. You then update your UI based on the event from the database.
This is sometimes known as a reactive model, or more formally as Command Query Responsibility Segregation: you separate the flow of the commands (from user to database) from the flow of the queries (from database to views).

You should use DatabaseHandler. You should listen your database and then remove handler when you leave your viewcontroller.
fileprivate lazy var ref = Database.database().reference().child("...")
private var yourHandler: DatabaseHandle?
override func viewDidLoad() {
super.viewDidLoad()
yourHandler = ref.observe(.childAdded, with: { (snapshot) -> in
self.posts.append(snapshot)
DispatchQueue.main.async {
self.tableView.reloadData()
}
})
}
deinit {
if let handler = yourHandler {
ref.removeObserver(withHandle: handler)
}
}
Now, when you add new item to database, your handler get this item and display it in your viewcontroller.
Note: Always call the reloadData () method on the main thread

Related

How to store and load data properly with CoreData?

I am a beginner and never worked close with CoreData. I have a JSON response, which results need to be shown in a Table View. I want implement a CoreData to my project.
JSON parsing (in Separate Swift file)
func parseJSON(with currencyData: Data){
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(CurrencyData.self, from: currencyData)
for valute in decodedData.Valute.values {
if valute.CharCode != "XDR" {
let currency = Currency(context: self.context)
currency.shortName = valute.CharCode
currency.currentValue = valute.Value
}
}
do {
try context.save()
} catch {
print("Error saving context, \(error)")
}
} catch {
self.delegate?.didFailWithError(self, error: error)
return
}
}
And in my VC I want to load it in my tableView which takes data from currencyArray:
func loadCurrency() {
let request: NSFetchRequest<Currency> = Currency.fetchRequest()
do {
currencyArray = try context.fetch(request)
} catch {
print(error)
}
tableView.reloadData()
}
I start the parsing and load currency data in my VC:
override func viewDidLoad() {
super.viewDidLoad()
currencyNetworking.performRequest()
loadCurrency()
}
But as a result when app first launch my tableView is empty. Second launch - I have doubled data. Maybe this is because loadCurrency() starts before performRequest() able to receive data from JSON and save it to context.
I tried also to not save context in parseJSON() but first through delegate method send it to VC and perform save to context and load from context from here. Then tableView loads from the first launch. But then every time app starts in my CoreData database I see an increase of the same data (33, 66, 99 lines).
My goal is to save parsed data to CoreData once (either where I parseJSON or in VC) and change only currentValue attribute when user want to update it.
How to make it correct?
You need to first choose which is your source of truth where you are showing data from. In your case it seems to be your local database which is filled from some external source.
If you wish to use your local database with data provided from remote server then you need to have some information to keep track of "same" entries. This is most usually achieved by using an id, an identifier which is unique between entries and persistent over changes in entry. Any other property may be used that corresponds to those rules. For instance CharCode may be sufficient in your case if there will always be only one of them.
Next to that you may need a deleted flag which means that you also get deleted items from server and when you find entry with deleted flag set to true you need to delete this object.
Now your pseudo code when getting items from server you should do:
func processEntries(_ remoteEntries: [CurrencyEntry]) {
remoteEntries.forEach { remoteEntry in
if remoteEntry.isDeleted {
if let existingLocalEntry = database.currencyWithID(remoteEntry.id) {
existingLocalEntry.deleteFromDatabase()
}
} else {
if let existingLocalEntry = database.currencyWithID(remoteEntry.id) {
database.updateCurrency(existingLocalEntry, withRemoteEntry: remoteEntry)
} else {
database.createCurrency(fromRemoteEntry: remoteEntry)
}
}
}
}
And this is just for synchronization approach.
Now going to your view controller. When it appears you call to reload data from server and at the same time display whatever is in your database. This is all fine but you are missing a re-display once new data is available.
What you optimally need is another hook where your view controller will be notified when database has changes. So best thing to do is add event when your database changes which is whenever you save your database. You probably looking at something like this:
extension Database {
func save() {
guard context.hasChanges else { return }
try? context.save()
self.notifyListenersOfChangesInDatabase()
}
and this method would be called within your parseJSON and everywhere else that you save your database. Then you would add your view controller as a listener on your database as
override func viewDidLoad() {
super.viewDidLoad()
database.addListener(self)
currencyNetworking.performRequest()
loadCurrency()
}
How exactly will listener and database be connected is up to you. I would suggest using delegate approach but it would mean a bit more work. So another simpler approach is using NotificationCenter which should have a lot of examples online, including on StackOverflow. The result will be
Instead of database.addListener(self) you have NotificationCenter.default.addObserver...
Instead of notifyListenersOfChangesInDatabase you use NotificationCenter.default.post...
In any of the cases you get a method in your view controller which is triggered whenever your database is changed. And in that method you call loadCurrency. This is pretty amazing because now you don't care who updated the data in your database, why and when. Whenever the change occurs you reload your data and user sees changes. For instance you could have a timer that pools for new data every few minutes and everything would just work without you needing to change anything.
The other approach you can do is simply add a closure to your performRequest method which triggers as soon as your request is done. This way your code should be like
override func viewDidLoad() {
super.viewDidLoad()
loadCurrency()
currencyNetworking.performRequest {
self.loadCurrency()
}
}
note that loadCurrency is called twice. It means that we first want to show whatever is stored in local database so that user sees his data more or less instantly. At the same time we send out a request to remote server which may take a while. And once server response finished processing a reload is done again and user can view updated data.
Your method performRequest could then look like something as the following:
func performRequest(_ completion: #escaping () -> Void) {
getDataFromRemoteServer { rawData in
parseJSON(with: rawData)
completion()
}
}

Firebase query observing reshowing data

I have a firebase query that observes data from a posts child.
func fetchPosts () {
let query = ref.queryOrdered(byChild: "timestamp").queryLimited(toFirst: 10)
query.observe(.value) { (snapshot) in
for child in snapshot.children.allObjects as! [DataSnapshot] {
if let value = child.value as? NSDictionary {
let post = Post()
let poster = value["poster"] as? String ?? "Name not found"
let post_content = value["post"] as? String ?? "Content not found"
let post_reveals = value["Reveals"] as? String ?? "Reveals not found"
post.post_words = post_content
post.poster = poster
post.Reveals = post_reveals
self.postList.append(post)
DispatchQueue.main.async { self.tableView.reloadData() }
//make this for when child is added but so that it also shows psots already there something like query.observre event type of
}
}
However, when a user posts something, it creates a more than one cell with the data. For instance, if I post "hello", a two new cards show up with the hello on it. However, when I exit the view and recall the fetch posts function, it shows the correct amount of cells. Also, when I delete a post from the database, it adds a new cell as well and creates two copies of it until I reload the view, then it shows the correct data from the database.
I suspect this has something to do with the observe(.value), as it might be getting the posts from the database and each time the database changes it creates a new array. Thus, when I add a new post, it is adding an array for the fact that the post was added and that it now exists in the database, and when I refresh the view it just collects the data directly from the database.
Also, sometimes the correct amount of cells show and other times there's multiple instances of random posts, regardless of whether I have just added them or not.
How can I change my query so that it initially loads all the posts from the database, and when some post is added it only creates one new cell instead of two?
Edit: The logic seeming to occur is that when the function loads, it gets all the posts as it calls the fetchPosts(). Then, when something is added to the database, it calls the fetchPosts() again and adds the new data to the array while getting all the old data. yet again.
One thing I always do when appending snapshots into an array with Firebase is check if it exists first. In your case I would add
if !self.postList.contains(post) {
self.postList.append...
however, to make this work, you have to make an equatable protocol for what I'm guessing is a Post class like so:
extension Post: Equatable { }
func ==(lhs: Post, rhs: Post) -> Bool {
return lhs.uid == rhs.uid
}
You are right in thinking that the .value event type will return the entire array each time there is a change. What you really need is the query.observe(.childAdded) listener. That will fetch individual posts objects rather than the entire array. Call this in your viewDidAppear method.
You may also want to implement the query.observe(.childRemoved) listener as well to detect when posts are removed.
Another way would be to call observeSingleEvent(.value) on the initial load then add a listener query.queryLimited(toLast: 1).observe(.childAdded) to listen for the latest post.

Cannot successfully reload UITableView elements when parsing data from Firebase

I'm making an app where users can buy and sell tickets. Users are able to create a new ticket and it successfully uploads to firebase however a reference to the ticket ID is stored in the user data which references the ticket id in the ticket data. The structure of the database is below:
DATABASE
USERS
TICKETS
TICKETS
TICKET INFO
USER
USER INFO AND TICKET ID OF TICKETS THEY ARE SELLING
My problem is that the first time I load the tickets from the selling tickets it's fine. However when the user adds a new ticket that they are selling, the table view loads everything twice.
override func viewWillAppear(_ animated: Bool) {
self.tickets = []
DataService.ds.REF_USER_CURRENT.child("selling").observe(.value, with: { (snapshot) in //HERE WE REFERNCE OUR SINGELTON CLASS AND OBSERVE CHAMGE TO THE POSTS OBJECT
self.tickets = [] //WE CLEAR THE POSTS ARRAY BEFORE WE START MANIPULATION TO MAKE SURE THAT WE DONT REPEAT CELLS
if let snapshot = snapshot.children.allObjects as? [DataSnapshot]{
print("ADAM: \(snapshot)")//CHECKING THAT THE OBJECTS EXIST AS AN ARRAY OF DATA SNAPSHOTS
for snap in snapshot {
DataService.ds.REF_TICKETS.child(snap.key).observe(.value, with: { (snapshot) in
if let ticketDict = snapshot.value as? Dictionary<String, AnyObject>{
let ticket = Ticket(ticketID: snap.key, ticketData: ticketDict)
self.self.tickets.append(ticket)
}
self.sell_ticketsTableView.reloadData()
})
}
}
//self.sell_ticketsTableView.reloadData()
self.tickets = []//RELAOD THE DATA
})
}
I'm not quite sure where I have gone wrong.
Please change your code to this. I have added the part where you clear your array
override func viewWillAppear(_ animated: Bool) {
self.removeAll() // This is how you clear your array
DataService.ds.REF_USER_CURRENT.child("selling").observe(.value, with: { (snapshot) in //HERE WE REFERNCE OUR SINGELTON CLASS AND OBSERVE CHAMGE TO THE POSTS OBJECT
self.tickets = [] //WE CLEAR THE POSTS ARRAY BEFORE WE START MANIPULATION TO MAKE SURE THAT WE DONT REPEAT CELLS
if let snapshot = snapshot.children.allObjects as? [DataSnapshot]{
print("ADAM: \(snapshot)")//CHECKING THAT THE OBJECTS EXIST AS AN ARRAY OF DATA SNAPSHOTS
for snap in snapshot {
DataService.ds.REF_TICKETS.child(snap.key).observe(.value, with: { (snapshot) in
if let ticketDict = snapshot.value as? Dictionary<String, AnyObject>{
let ticket = Ticket(ticketID: snap.key, ticketData: ticketDict)
self.tickets.append(ticket)
}
self.sell_ticketsTableView.reloadData()
})
}
}
//self.sell_ticketsTableView.reloadData()
})
}
You are observing the value of what a user is selling, which means every time they add something to this list, your listener will trigger and give you the new value of users/$uid/selling in its entirety.
This is why you are seeing double when the user adds another ticket; the listener is triggered and you append each ticket to the array again. You can get around this by checking if the ticket is already in the array before you append it however, your current implementation can be improved.
Instead of using observe(.value, you should use .childAdded. The listener will trigger every time a new child is added and only give you that specific child snapshot.
The listener will initially trigger for each child at that reference and you can append them to the array individually. However, once all the children have been loaded, the next child to be added will trigger this listener, which you can append to the array.

Asynchronous functions and Firebase with Swift 3

I read some questions about that. But I still have issues with asynchronous functions.
For example: I have a viewController1 where a button perform a segue to a viewController2. In the viewController2 class, I initialize some values in another class file named exampleClass. These values are retrieved from Firebase database or location values. These values need a little moment to be retrieved. I return thes values from the exampleClass into my viewController2. I print these values in the viewController2 viewDidLoad().
My issue: The device doesn't wait that the values are retrieved and execute following functions. Result: When I touch the button, printed values are nil values. It can also make the app crash if I don't secure the code.
What I've found so far: I learned that I only have to call a func at the end of a Firebase snapshot (for example) like this:
userRef.observeSingleEvent(of: .value, with: { (snapshot) -> Void in
self.name = snapshot.value as! String!
print(self.name)
self.forceEnd()
}) { (error) in
print(error.localizedDescription)
}
I named this function forceEnd to be clear. This is not working for me. I also tried to create handlers but no positive results.
My question: How can I force the device to wait for the values to be retrieved before performing the following question?
How can I force the device to wait for the values to be retrieved before performing the following question?
You don't want to force the device to wait, only need to perform some operations once these values are retrieved from Firebase database.
Performing an operation asynchronously can be done in multiple ways like blocks, protocols, notifications, etc.
Generally, blocks are the more elegant approach.
Some sample code can be like:
func myFirebaseNetworkDataRequest(finished: () -> Void) { // the function thats going to take a little moment
...
print("Doing something!") // firebase network request
finished()
}
// usage of above function can be as-
override func viewDidLoad() {
myFirebaseNetworkDataRequest {
// perform further operations here after data is fetched
print("Finally! It took a lot of moments to end but now I can do something else.")
}
}

Data from Firebase not loading into array

I don't have a storyboard. I'm doing everything programmatically.
The loadData() method takes Firebase data, put it into a Company object, and loads the object into the companies array. In the didFinishLaunchingWithOptions method in the App Delegate, I instantiated the class and called loadData()
When I run breakpoint at the line indicated by the comment and type "po companies" in the console, I get 0 companies. The print statements inside .observe are printed to the console and I can see that the company's properties are non-null, but anything outside .observe, including the for loop and the print statement called after the load data method in the App Delegate are not printed.
class informationStateController {
func loadData() {
//Set firebase database reference
ref = FIRDatabase.database().reference()
//Retrieve posts and listen for changes
databaseHandle = ref?.child("companies").observe(.childAdded, with: { (snapshot) in
//Code that executes when child is added
let company = Company()
company.name = snapshot.childSnapshot(forPath: "name").value as! String
print(company.name)
company.location = snapshot.childSnapshot(forPath: "location").value as! String
print(company.location)
self.companies.append(company)
print("databaseHandle was called")
})
for company in companies {
print(company)
}
//breakpoint inserted here
}
}
Why is my array empty and why are print statements outside .observe NOT printing to the console? The output for the console is set to "All Output". I called import FirebaseDatabase in the class and import Firebase in the App Delegate.
Data is loaded from the Firebase Database asynchronously. This means that by the time you print the companies, they won't have loaded yet.
You can easily see this by also printing the companies as they're loaded:
//Set firebase database reference
ref = FIRDatabase.database().reference()
//Retrieve posts and listen for changes
databaseHandle = ref?.child("companies").observe(.childAdded, with: { (snapshot) in
//Code that executes when child is added
let company = Company()
company.name = snapshot.childSnapshot(forPath: "name").value as! String
print(company.name)
company.location = snapshot.childSnapshot(forPath: "location").value as! String
print(company.location)
self.companies.append(company)
print("databaseHandle was called")
for company in companies {
print(company)
}
})
Now you'll first see one company printed (when childAdded fires for the first time), then two companies (when childAdded fires again), then three companies, etc.
Per the docs (emphasis mine)
Important: The FIRDataEventTypeValue event is fired every time data is changed at the specified database reference, including changes to children. To limit the size of your snapshots, attach only at the highest level needed for watching changes. For example, attaching a listener to the root of your database is not recommended.
In your case, you're observing changes to the database, but no changes are happening, so you won't bet getting new data. I think the docs make this unnecessarily confusing, if you want to pull records that already exist, you have to query for it:
https://firebase.google.com/docs/database/ios/lists-of-data#sort_data
// Last 100 posts, these are automatically the 100 most recent
// due to sorting by push() keys
let recentPostsQuery = (ref?.child("companies").queryLimited(toFirst: 100))!
Once you have that queried data, you can then deal with the observer and append data as required when new data is pushed.
All of this aside, Frank's answer is the reason you'll never see the print when a company is added even if you set the listener up right — you need to write that inside the completion block of the observer or query.

Resources