I have this Firebase reference to retrieve all the pending (unread) messages by passing the priority of last read message. I do this onload of the chat room.
NSNumber* lastPriority = [self getLastPriority];
__block FirebaseHandle handle = [[msgListRef queryStartingAtPriority:lastPriority] observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
// here I will get all the pending messages and add them to my local message list and reload the UITableView
[self addMessageToList: snapshot]
[msgListRef removeObserverWithHandle:handle];
// Then I call the below function to keep listening for new messages when the chat is in progress
[self addMessageListener];
}];
- (void) addMessageListener{
msgListRef = [[Firebase alloc] initWithUrl:messageListUrl];
NSNumber* lastPriority = [self getLastPriority];
[[msgListRef queryStartingAtPriority:lastPriority] observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
[self addMessageObject:snapshot.value];
// Here I will reload the table view. This one is supposed to fire for each
//message since I used FEventTypeChildAdded.
//*** BUT THIS CALLBACK IS NEVER FIRED ***
}];
}
Any idea why the second callback with FEventTypeChildAdded observerType never gets fired when I already called FEventTypeValue before that? It works if I don't read all with FEventTypeValue. But in that case my UITableView reload will get called for each pending messages when the user enters the chat room.
Sorry guys.The issue was with my code. The firebase reference (msgListRef) was re-initilized at another function that I didnt notice.
Related
I’ve created simple UITableView with couple of rows that is loading from Firebase.
On viewWillAppear I connect observer and populate the array with data.
The problem happens when view is already loaded but asynchronous Firebase callback is still wasn’t called - Table appears as blank.
The table refreshes after callback and looks like expected but the delay is still noticeable.
I’ve configured offline mode and expecting that observer’s handler should be called immidiately but it doesn’t.
What is the proper way to handle that?
Waiting bar looks like not the best option because it’s just 100ms or so and data is already on device and I have just 8 rows.
Is there any solution?
Thank you!
Observe code:
-(void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
refHandle = [areasRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
[self.arrayOfAreas removeAllObjects];
for (FIRDataSnapshot* areaSnapshot in snapshot.children){
Area *area = [[Area alloc] init];
[area retrieveFromSnapshot:areaSnapshot];
[self.arrayOfAreas addObject:area];
} [self.tableView reloadData];
}]; }
viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
self.ref = [[FIRDatabase database] reference];
FIRUser *currentUser = [FIRAuth auth].currentUser;
areasRef = [[[_ref child:#"users"] child:currentUser.uid] child:#"areas"];
self.tableView.dataSource = self;
self.tableView.delegate = self;
self.tableView.tableFooterView = [[UIView alloc]init];
self.arrayOfAreas = [NSMutableArray new];
self.tableView.allowsMultipleSelectionDuringEditing = NO; }
After fresh install of your app,
Obviously there will be a delay to fetch the datas from firebase.
once you fetch all the data, you have to save it in your local DB.
From second time onwards, show the datas from your local DB and concurrently fetch the firebase DB also. (if you doesn't do update/delete in inner key values of the fetching path, firebase DB fetching query can be optimised based on the local data).
If you are saving the local DB datas to arrayOfAreas. you can change the observe block as follows
NSMutableArray *arrayTemp = [[NSMutableArray alloc]init];
for (FIRDataSnapshot* areaSnapshot in snapshot.children){
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Area" inManagedObjectContext:[CoreData sharedData].context];
Area *area = [[Area alloc] initWithEntity:entity insertIntoManagedObjectContext:nil];
[area retrieveFromSnapshot:areaSnapshot];
[arrayTemp addObject:area];
}
/**
if you are fetching all datas again
*/
self.arrayOfAreas = arrayTemp;
[self.tableView reloadData];
/**
if you are fetching only the datas that are newly added (datas that are not existing in local DB)
*/
[self.arrayOfAreas addObjectsFromArray: arrayTemp];
[self.tableView reloadData];
if you are using firebase offline feature, try below way. (it may not be a perfect solution)
invoke [FIRDatabase database].goOffline in viewDidLoad or app launch.
once the observer block executed, [FIRDatabase database].goOnline.
Your UITableview is getting loaded on viewDidLoad, as there would be no data in you array at initially your tableView is blank.
If you don't wish to load your tableView until the you retrieve data then you may assign your datasource and delegate after you finish fetching your records. (In your completion block of Firebase Method)
You can show loader till your data is retrieved.
I'm looking at the Firechat demo project, and they use a technique whereas they're using FEventTypeChildAdded as well as FEventTypeValue as the latter will fire just after FEventTypeChildAdded. This is done in order to prevent a "spam" of FEventTypeChildAdded callbacks to avoid a UI lockup. However, I implemented the same technique in my app, but FEventTypeValue is actually called before FEventTypeChildAdded, resulting in the BOOL changing to NO and locking the UI.
From the demo project:
// This allows us to check if these were messages already stored on the server
// when we booted up (YES) or if they are new messages since we've started the app.
// This is so that we can batch together the initial messages' reloadData for a perf gain.
__block BOOL initialAdds = YES;
[self.firebase observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
// Add the chat message to the array.
if (newMessagesOnTop) {
[self.chat insertObject:snapshot.value atIndex:0];
} else {
[self.chat addObject:snapshot.value];
}
// Reload the table view so the new message will show up.
if (!initialAdds) {
[self.tableView reloadData];
}
}];
// Value event fires right after we get the events already stored in the Firebase repo.
// We've gotten the initial messages stored on the server, and we want to run reloadData on the batch.
// Also set initialAdds=NO so that we'll reload after each additional childAdded event.
[self.firebase observeSingleEventOfType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
// Reload the table view so that the intial messages show up
[self.tableView reloadData];
initialAdds = NO;
}];
Here is how things are looking:
My root controller is a UITabBarController. Number 13 in the image is my default view that I've set to show a list of people when the app is first opened up.
Number 4 shows a page I'm taken to by clicking a settings link on page 3. On page 4 I can manage the people on the main view by confirming pending people or taking confirmed people offline. Basically I can set whether they show up or not or delete if I wish.
Changes are always shown in 5 and 6 because I remove rows after after deletions, confirmations or status changes. When making edits in the detail views 7 the changes are updated easily because I update the Person object that has been passed over during segue. So when I go back to the table view in 5 I just "self.tableView reloadData" in my viewWillLoad method.
In table 6 and action sheet pops up when I tap on a row and gives me the option to take any currently confirmed person back offline. All I do is remove the row and that's it. ViewDidLoad is run when ever I go back to controller view 4 then click "pending" or "confirmed".
Now the issue
Unlike my controller in number 4 my main table in number 13 is shown when my first UITableViewController tab is tapped or when the app is first loaded up. My code below is loaded in the viewDidLoad method and therefore only loaded once.
This is how my data is shown on main view:
- (void)populatePeopleArrayWithCloudData {
// Grab data and store in Person instances and then store each person in array
people = [[NSMutableArray alloc] init];
PFQuery *query = [PFQuery queryWithClassName:#"People"];
[query whereKey:#"active" equalTo:#1];
[query orderByDescending:#"createdAt"];
[query setLimit:10];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
for (PFObject *object in objects) {
Person *person = [[Person alloc] init];
[person setName:[object objectForKey:#"name"]];
[person setNotes:[object objectForKey:#"notes"]];
[person setAge:[[object objectForKey:#"age"] intValue]];
[person setSince:[object objectForKey:#"since"]];
[person setFrom:[object objectForKey:#"from"]];
[person setReferenceNumber:[object objectForKey:#"referenceNumber"]];
PFFile *userImageFile = object[#"image"];
[userImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *image = [UIImage imageWithData:imageData];
[person setImage:image];
}
}];
[person setActive:[[object objectForKey:#"active"] intValue]];
[person setObjectId:[object objectId]];
[people addObject:person];
}
} else {
// Log details of the failure
NSLog(#"Error: %# %#", error, [error userInfo]);
}
NSLog(#"Calling reloadData on %# in viewDidLoad", self.tableView);
[self.tableView reloadData];
}];
}
The code grabs my data from parse, runs a for loop storing each person in a Person instance and adding it to an NSMutableArray instance "people". This people instance is accessed by my tableview datasource methods.
When I tap another tab and then tap my main tab again, viewDidLoad is not loaded again. Ok, that's understandable. Now what if I want my main table(13) to reflect changes made in tables 5 and 6? I thought I could just go to viewWillAppear and run "self.tableView reloadData". I did this and nothing happened. Well that's what I thought. After some messing around I realised that my "people" array wasn't updated with the latest data from my database.
I then decided to try and call my populatePeopleArrayWithCloudData method from viewWillAppear but this just causes duplicate entries. My people array is used to populate table 13 twice. I said ok let me take the populatePeopleArrayWithCloudData method out of viewDidLoad. This works and changes are shown but then I start having issues with images being loaded, showing up late , last row issues etc. Calling populatePeopleArrayWithCloudData method in viewDidLoad solves all those issues.
I like having the code in viewDidLoad because it means less calls to the database. Like when a user clicks on a row and it takes them to the detail views 14 and 15 and return back to 13 no calls are made to the database like they are when code is in viewWillAppear.
So I'm wondering if the best way to solve this issue I'm having to, is to some how update the "people" instance from tableView 6 (as soon as "take offline is click") and in tableview 5 (as soon as "publish" is clicked to confirm a person). Publishing a user sets active status to 1. Objects with a status of 1 are included in the query result above and therefore shown in the main tableview list. Taking a person offline sets their active status to 0 meaning they don't show up in the main list.
1. Would it be good/right to access the "people" instance from my table views 5 and 6 then update that people array appropriately? Maybe after the update have a method similar to populatePeopleArrayWithCloudData that repopulates people. Then in viewWillAppear I just reload the table again hoping it detects the new people instance and uses that.
2. If so how do I do this? Can you show a clear example if possible...
3. If not what way would you suggest I do this and can you show a clear example.
Thanks for your time
Kind regards.
There are several approaches you can take for this but (assuming I understand your problem) I think that the best is to have a shared instance of your array of people. What you do is create an NSObject called ManyPeople and add the following:
ManyPeople.h:
#property (nonatomic, strong) NSMutableArray *manyPeople;
+(ManyPeople *) sharedInstance;
ManyPeople.m
+ (ManyPeople *) sharedInstance
{
if (!_sharedInstance)
{
_sharedInstance = [[ManyPeople alloc] init];
}
return _sharedInstance;
}
Now all your UIViewControllers can call ManyPeople *people = [ManyPeople sharedInstance]; and will get the same array. When make a call to the database you populate the shared instance. When you make changes to the settings you also make changes to the shared instance. The beauty is that when you return to your view controller 13 all you do is reloadData and you don't need to make a call to the database. The data source is already points to the updated array.
Strange title, I know, but here is the quickest way to explain my issue. The Parse service has a prebuilt Sign Up controller for letting new users sign up for whatever service you may have. There is no way to edit it's implementation, you can only handle events in it's delegate methods. So I can't just place this in the IBAction of the "Sign Up" button. What I want to do is, when a user touches "Sign up", is that I need to make a call to some API to check if something exists or not, and then if it does already exist then don't let the user sign up. Here is the delegate method meant to handle validation when the button is pressed:
// Sent to the delegate to determine whether the sign up request should be submitted to the server.
- (BOOL)signUpViewController:(PFSignUpViewController *)signUpController shouldBeginSignUp:(NSDictionary *)info {
Here is what I'm trying to place in it:
[self processJSONDataWithURLString:[NSString stringWithFormat:#"https://www.someapi.com/api/profile.json?username=%#",username] andBlock:^(NSData *jsonData) {
NSDictionary *attributes = [jsonData objectFromJSONData];
// Check to see if username has data key, if so, that means it already exists
if ([attributes objectForKey:#"Data"]) {
return NO; // Do not continue, username already exists
// I've also tried:
dispatch_sync(dispatch_get_main_queue(), ^{ return NO; } );
}
else
return YES; //Continue with sign up
dispatch_sync(dispatch_get_main_queue(), ^{ return YES; } );
}];
I get errors when I try to return anything, though. When I just do a straight return YES, "^(NSData *jsonData)" is underlined in yellow and I get "Incompatible block pointer types sending BOOL (^)NSData *_strong to parameter of type void(^)NSData *_strong".
Basically, in there any way to make an API call in this method to check something and then return YES or NO depending on the result?
Thanks!
No.
You are invoking an asynchronous method that is using the block as a callback. The processJSON… method is invoked and immediately returns from the call. After it runs in the background the block is invoked. You cannot "return" from within the block. The method was popped off the stack and returned some time previously.
You need to re-architect the logic of this. Invoking a refresh on the main queue is in the right direction.
Try this:
[self processJSONDataWithURLString:[NSString stringWithFormat:#"https://www.someapi.com/api/profile.json?username=%#",username] andBlock:^(NSData *jsonData) {
NSDictionary *attributes = [jsonData objectFromJSONData];
BOOL status=YES;
// Check to see if username has data key, if so, that means it already exists
if ([attributes objectForKey:#"Data"]) {
status=NO; // Do not continue, username already exists
[self performSelectorOnMainThread:#selector(callDelegate:) withObject:[NSNumber numberWithBool:status] waitUntilDone:YES];
}];
-(void)callDelegate:(NSNumber*) status
{
BOOL returnStatus = [status boolValue];
//now retutn returnStatus to your delegate.
}
But this is not the right way to do it, you gotta change the logic you have written to support asynchronous communication. You could consider mine, only if you want to do it your way.
Currently, I'm downloading data from a web server by calling a method fetchProducts. This is done in another separate thread. As I successfully download fifty items inside the method stated above, I post a notification to the [NSNotification defaultCenter] through the method call postNotificationName: object: which is being listened to by the Observer. Take note that this Observer is another ViewController with the selector updateProductsBeingDownloadedCount:. Now as the Observer gets the notification, I set the property of my progressView and a label that tells the progress. Below is the code I do this change in UI.
dispatch_async(dispatch_get_main_queue(), ^{
if ([notif.name isEqualToString:#"DownloadingProducts"]) {
[self.progressBar setProgress:self.progress animated:YES];
NSLog(#"SetupStore: progress bar value is %.0f", self.progressBar.progress);
self.progressLabel.text = [NSString stringWithFormat:#"Downloading %.0f%% done...", self.progress * 100];
NSLog(#"SetupStore: progress label value is %#", self.progressLabel.text);
[self.view reloadInputViews];
}
});
The idea is to move the progressView simultaneously as more items were being downloaded until it is finished. In my case, the progressView's animation will just start right after the items were already downloaded, hence a delay. Kindly enlighten me on this.