PFQueryTableView trouble populating table view - ios

I'm currently working on a PFQueryTableView and trying to get it to populate with data from an array that's pulled from ViewDidLoad. UPDATE: I've moved the function to an NSObject and implemented a singleton to be used across multiple classes in an effort to silo the operation away from the view controller. Below is the updated code:
+ (NSArray *)savedTankArray
{
PFUser *userName = [PFUser currentUser];
NSString *userNameString = [userName objectForKey:#"username"];
PFQuery *query = [[PFQuery alloc] initWithClassName:#"SavedTanks"];
[query whereKey:#"userName" equalTo:userNameString];
[query setValue:#"SavedTanks" forKeyPath:#"parseClassName"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error)
{
if (!error)
{
// The find succeeded.
NSLog(#"Successfully retrieved %lu Tanks.", objects.count);
// Do something with the found objects
for (PFObject *object in objects)
{
NSString *tankNameString = [[NSString alloc] init];
NSString *tankCapacityString = [[NSString alloc] init];
tankNameString = [object valueForKey:#"tankName"];
tankCapacityString = [object valueForKey:#"tankCapacity"];
NSLog(#"%#", tankNameString);
NSLog(#"%#", tankCapacityString);
_savedTankArray = [objects objectAtIndex:0];
}
}
else
{
// Log details of the failure
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
NSLog(#"TANK NAME ARRAY: %#", _savedTankArray);
return [_savedTankArray savedTankObjects];
}
While the NSLogs inside of the function work just fine, my problem is a bit expanded now, and I feel as though I'm missing something really simple here.
By the time I get to #"TANK NAME ARRAY: %#"... obviously it's returning null because its outside of the portion that handles the query. This doesn't help me much if I'm trying to bring the data in through another class.
I've tried so much over the past few days and I can't imagine I'm missing something terribly complex. I'm sorry for re-opening this but I can't wrap my head around it at this time.
Any ideas on how I could handle this? I appreciate the help as always.

There may be other trouble, but for sure this line:
tableData = [NSArray arrayWithObjects:objects, nil];
is a mistake. This will create a single-element array whose first element is the array of results. I think you can fix and simplify as:
tableData = objects;
For your question on how to proceed, I think you can carry on in this class the way one would in any table view controller. Answer the table datasource methods by referring to tableData (i.e. it's count for numberOfRowsInSection:, and tableData[indexPath.row] to configure a cellForRowAtIndexPath:, and so on).

New answer for the edited new question:
It appears that the mixup is with calling the asynch service. I'll give two kinds of advice here. First, the simplest possible table-containing view controller that gets its data from an asynch service, and second, a little class that wraps the parse asynch service. First the VC:
// in a vc with a table view .m
#interface MyViewController ()
#property(weak,nonatomic) IBOutlet UITableView *tableView;
#property(strong,nonatomic) NSArray *array; // this class keeps the array
#end
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[ClassThatHandlesMyQuery doQuery:^(NSArray *results) {
self.array = results;
[self.tableView reloadData];
}];
}
See how the query class method in the other class takes a block parameter? This is required because the query happens asynchronously.
// do the normal table view stuff
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.array.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
PFObject *pfObject = self.array[indexPath.row];
cell.textLabel.text = [pfObject valueForKey:#"someStringProperty"];
return cell;
}
That should be pretty much all you need in the vc. Now let's look at your query method. It makes three mistakes: (a) No block parameter to let the caller get the asynch result, (b) it mishandles the array in the query completion block, (c) at the end of the method, it wrongly supposes that a variable _savedTankArray is initialized, in the block. That code appears below the block, but it actually runs before the block runs.\
Let's fix all three problems. First declare a public method:
// ClassThatHandlesMyQuery.h
+ (void) doQuery:(void (^)(NSArray *))completion;
See how it takes a block as param? Now implement:
// ClassThatHandlesMyQuery.m
+ (void) doQuery:(void (^)(NSArray *))completion {
// your query code. let's assume this is fine
PFUser *userName = [PFUser currentUser];
NSString *userNameString = [userName objectForKey:#"username"];
PFQuery *query = [[PFQuery alloc] initWithClassName:#"SavedTanks"];
[query whereKey:#"userName" equalTo:userNameString];
[query setValue:#"SavedTanks" forKeyPath:#"parseClassName"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// the job is MUCH simpler here than your code supposed.
// log the result for fun
NSLog(#"did we get objects? %#", objects);
// hand it back to the caller
// notice there's no array kept in this class. it's not needed
// and it would be awkward to do it at the class (not instance) level
completion(objects);
} else {
NSLog(#"bad news from parse: %#", error);
completion(nil);
}
}
// this is important
NSLog(#"hi mom!");
// watch your log output. 'hi mom' will appear before either message
// from the block. why is that? because that block runs later
// after the network request completes. but the hi mom NSLog runs
// just before the network request starts. this is why it's wrong to expect
// any variable set in the block to be initialized here
}
Believe it or not, that's it. You should be able to write exactly the mini view controller class and the mini query classes as described here, and see data from parse in a UITableView. I suggest you build something just like this (exactly like this) first just to get going

Related

Parse.com returns empty objects

I have an NSArray called "malls" that contains a large number of NSDictionaries (each a specific mall) that I uploaded to Parse.com. I want my users to be able to access this information to create map annotations.
I've tried to do this in 2 different ways:
I tried uploading the entire array as a property of a single object:
this is the upload:
in the dataBank.h file:
#property (strong, nonatomic) NSMutableArray* malls;
in the .m file
PFObject *obj = [PFObject objectWithClassName:#"malls"];
obj[#"mallsData"] = self.malls;
[obj saveInBackground];
I try to get the data from parse:
-(NSMutableArray *)createAnnotationsFromParse
{
__block NSMutableArray* data = [[NSMutableArray alloc]init];
__block NSMutableArray* annots = [[NSMutableArray alloc]init];
PFQuery* query = [PFQuery queryWithClassName:#"malls"];
[query getObjectInBackgroundWithId:#"Eaib9yfTRe" block:^(PFObject *object, NSError *error) {
data = [object objectForKey:#"mallsData"];
annots = [self createAnnotations:data];
}];
return annots;
}
The problem is getObjectInBackground is asynchronous and always returns before getting the data from the server. I tried moving the "return annots" inside the code block but that gives the following error: "incompatible block pointer types".
I uploaded 5 "mall" objects to class "malls2". Each object has 2 properties- name and address:
for(int i = 0; i < 5; i++)
{
PFObject *mallsObj = [PFObject objectWithClassName:#"malls2"];
mallsObj[name] = [[self.malls objectAtIndex:i]objectForKey:name];
mallsObj[address] = [[self.malls objectAtIndex:i]objectForKey:address];
[mallsObj saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if(succeeded)
NSLog(#"yay");
else
NSLog(#"%#", error.description);
}];
}
then I try to get it back:
-(NSMutableArray *)createAnnotationsFromParse
{
__block Annotation* anno = [[Annotation alloc]init];
__block NSMutableArray* annots = [[NSMutableArray alloc]init];
PFQuery* query = [PFQuery queryWithClassName:#"malls2"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if(error)
NSLog(#"%#", error.description);
else
{
for(int i = 0; i < [objects count]; i++)
{
//createAnnotationWithTitle is a func in a different class that creates the annotation
anno = [anno createAnnotationWithTitle:[[objects objectAtIndex:i] objectForKey:name] andAddress:[[objects objectAtIndex:i]objectForKey:address]];
}
[annots addObject:anno];
}
}];
return annots;
}
I get 5 objects but they're all empty.
It's a basic misunderstanding about asynchronous methods with block parameters. The trick is to get out of the habit of thinking that code that appears later in a source file runs later. The assumption works in this function:
- (void)regularFunction {
// these NSLogs run top to bottom
NSLog(#"first");
NSLog(#"second");
NSLog(#"third");
}
This will generate logs: first, second, third. Top to bottom, but not in this one:
- (void)functionThatMakesAsynchCall {
// these NSLogs do not run top to bottom
NSLog(#"first");
[someObject doSomeAsynchThing:^{
NSLog(#"second");
}];
NSLog(#"third");
}
That function will generate logs - first, third, second. The "second" NSLog will run well after the "third" one.
So what should you do? Don't try to update the UI with results of a parse call until after it completes, like this:
// declared void because we can't return anything useful
- (void)doSomeParseThing {
// if you change the UI here, change it to say: "we're busy calling parse"
PFQuery* query = [PFQuery queryWithClassName:#"malls2"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if(!error) {
// change the UI here, say by setting the datasource to a UITableView
// equal to the objects block parameter
}
}];
// don't bother changing the UI here
// don't bother returning anything here
// we just started the request
}
But what if doSomeParseThing is really a model function, whose only job is to fetch from parse, not to know anything about UI? That's a very reasonable idea. To solve it, you need to build your model method the way parse built their's, with block parameter:
// in MyModel.m
// declared void because we can't return anything useful
+ (void)doSomeParseThing:(void (^)(NSArray *, NSError *))block {
PFQuery* query = [PFQuery queryWithClassName:#"malls2"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
block(objects, error);
}];
}
Now your view controller can call, leave the query work to your model and the UI work to the vc:
// change UI to 'busy' here
[MyModel doSomeParseThing:^(NSArray *objects, NSError *error) {
// update UI with objects or error here
}];
Figured it out. It looked like I was getting "empty objects" (can be seen here postimg.org/image/ot7ehn29b ) but once I tried to access data from the objects I saw there was no problem. Basiclly I was tricked by the PFObjects in the array showing "0 objects" and assumed it meant they came back from Parse.com empty. Here's how I checked, just for reference:
PFQuery *query = [PFQuery queryWithClassName:#"malls2"];
NSArray *array = [query findObjects];
NSLog(#"%#", [[array objectAtIndex:0] objectForKey:#"name"]; // I have a string property called "name" in my Parse object.

Parse.com querying all users in a tableview

How would I go about displaying a list of all users in the parse.com database in a tableview and then when they click on each table, display that particular user's information.
All I know is that in order to query the users I must use:
PFQuery *query = [PFUser query];
Thank you in advance and any help is much appreciated.
You're really asking a specific question about Parse queries, but it seems like you don't understand queries in general, so just start with a general query against Parse:
PFQuery *query = [PFQuery queryWithClassName:#"_User"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (error) {
// Something went wrong
} else {
// objects is an array of PFObject containing your results
}
}];
As you figured out, you can also do a user query by making that first line:
PFQuery *query = [PFUser query];
The rest is the same.
Lyndsey Scott is right, this is basic stuff in the docs. I'm posting this here because of the one "gotcha" which is the class name (_User instead of User) if you use the first method.
What you generally will do is call [myTableView reloadData] inside the success block since you now have an array of users. In your didSelectCellAtIndexPath: perform a seque with a new viewcontroller, and in your prepareForSegue method, pass the user object to your pushed view controller so it knows what user to show.
I assume you know how to use table view. So I'll implement it this way:
1.create a property
#property (nonatomic) NSArray *users;
2.In viewDidAppear execute the query:
PFQuery *query = [PFUser query];
[query setLimit:1000];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
self.users = objects;
[self.tableView reloadData];
}
}];
3.You need to implement table view code, the most important is cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"userCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
PFUser *user = self.users[indexPath.row];
cell.textLabel.text = user.username;
return cell;
}
Beside that you also need all of the required table view's data source code to tell it the number of cell. If you'll need some help with that just tell.

Parse: Using PFObject subclasses with PFCloud calls

I have a PFObject subclass called MediaFile. In order to get back instances of my subclass from calls to the server, I've seen how you can construct queries out of Parse subclasses like this:
PFQuery *query = [MediaFile query];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) { ... }];
However, most of the server calls in my app are custom Cloud calls, which return PFObjects. As soon as I've retrieved these objects I want to treat them as MediaFile objects and call custom methods on them that are defined in the MediaFile class. Casting does not work, because it does not actually construct a MediaFile object. What I've done to overcome this problem is construct new MediaFiles for each PFObject I get back using [MediaFile object], and then copy all the data into each one using a method I wrote called loadFromObject::
[PFCloud callFunctionInBackground:#"func" withParameters:#{} block:^(id objects, NSError *error) {
for (PFObject *object in objects) {
MediaFile *mf = [[MediaFile object] loadFromObject:object];
[array addObject:mf];
}
}];
In MediaFile.m:
- (MediaFile *) loadFromObject:(PFObject *)object {
NSArray *keys = [object allKeys];
for (NSString *key in keys) {
id obj = [object objectForKey:key];
[self setObject:obj forKey:key];
}
return self;
}
This works, but is there a more elegant way to address this problem?
How does your custom class defined?
Because their subclassing mechanism is relying on Key-Value-Coding(KVC).
Here's KVC Programming Guide. Or you can check out the guide over here.
The following sample should work.
In the header:
#interface CustomObject : PFObject <PFSubclassing>
#property (nonatomic, strong) NSString *propertyName;
In the implementation:
#import <Parse/PFObject+Subclass.h>
#pragma mark - columns of Parse table
#dynamic propertyName;
#pragma mark - regiester parse subclass in runtime
+ (void)load {
#autoreleasepool {
[self registerSubclass];
}
}
#pragma mark - parse cloud table name
+ (NSString *)parseClassName
{
return #"CustomObject"; // table name
}
TL;DR:
Make sure your subclass of PFObject is registered with Parse by the time your Cloud code returns those objects from a net call.
If you've subclassed PFObject using the instructions Parse includes in it's documentation guide, you can just make the following call before the first net call that returns your custom subclass:
CustomSubclass.initialize()
A good place to add this code is in your app delegate's application(didFinishLaunchingWithOptions:) method, where it will be likely to run before any other code related to your subclass.
Long version with explanation
My situation was a little different from yours, but I believe the problem is largely the same. My problem was that I was fetching objects through a PFQuery and the objects returned in the query callback were just generic PFObjects.
Here's what my code looked like:
// Scene is a PFObject subclass
func getLatestScenes(completion: ((scenes: [Scene]) -> Void)?) {
var query = PFQuery(className: SceneClassName)
query.findObjectsInBackgroundWithBlock { (results: [AnyObject]?, error: NSError?) -> Void in
if let scenes = results as? [Scene] {
// This would never run, as the members of `results` were only PFObjects
completion?(scenes: scenes)
} else {
// Code would always skip to the empty result case
completion?(scenes: [])
}
}
}
Individual members of the results array would print out in the debugger with descriptions that clearly stated that they were supposed to be of the class Scene, as intended, but when inspected they were only PFObjects.
(lldb) po results![0]
<Scene: 0x7fc743a8f310, objectId: YyxYH9dtBp, localId: (null)> {
creator = "<PFUser: 0x7fc74144b320, objectId: FV3cmDI1PW>";
sceneDataFile = "<PFFile: 0x7fc743a96920>";
}
(lldb) p results![0]
(PFObject) $R6 = 0x00007fc743a8f310 {
NSObject = {
isa = 0x00007fc743a8f310
}
...
// Info on object...
...
}
The problem here was that the Scene object was not properly registered with Parse as a PFObject subclass. The initialize method was correctly overridden like so:
override class func initialize() {
struct Static {
static var onceToken : dispatch_once_t = 0;
}
dispatch_once(&Static.onceToken) {
self.registerSubclass()
}
}
but the initalize function wasn't being called by the time the query was called. The issue is that a class's initialize method is called right before the first message is sent to that class, but there was no message being sent to Scene before the query was performed. Adding the following line to my AppDelegate's application(didFinishLaunchingWithOptions:) solved the issue
Scene.initialize()
After that, Parse was able to infer which class on the client the incoming data the objects should be, and constructed the results array with that class.
It's simple after all. What worked for me is a simple assignment of the returned PFObject to the PFObject's Objective-C subclass. So in your case,
[PFCloud callFunctionInBackground:#"func" withParameters:#{} block:^(id objects, NSError *error) {
for (PFObject *object in objects) {
MediaFile *mf = object; // simple as that
[array addObject:mf];
}
}];

How access variables within method block?

I'm trying to access a method block but I have no idea how to:
__block NSString *username;
PFUser *user = [[self.messageData objectAtIndex:indexPath.row] objectForKey:#"author"];
[user fetchIfNeededInBackgroundWithBlock:^(PFObject *object, NSError *error) {
username = [object objectForKey:#"username"];
NSLog(#"%#", username); //returns "bob";
}];
NSLog(#"%#", username); //returns null
How do I access the variable 'username' from this code outside of the block?
Actually you are accessing the variable username outside the block. You are getting null because the block runs in another thread and you set the value after the block finish it's execution. So, your last line has been already executed in main thread while the block was running , so it's value was not set when last line was executed.That's why you are getting null.
fetchIfNeededInBackgroundWithBlock is an asynchronous method. That's why your last NSLog returns null because this it is performed before username was retrieved. So what you want is probably to call some method inside the block to be sure that it executes after you fetched your user data. Something like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyUserCell *userCell = (MyUserCell *)[tableView dequeueReusableCellWithIdentifier:MyUserCellIdentifier];
PFUser *user = [[self.messageData objectAtIndex:indexPath.row] objectForKey:#"author"];
userCell.user = user;
[user fetchIfNeededInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (object == userCell.user && !error) {
username = [object objectForKey:#"username"];
cell.textLabel.text = userName;
}
}];
}
UPDATE: The answer is updated to for the case when the block is called inside tableView:cellForRowAtIndexPath: method as requested.
NOTE: Here you will probably need a custom cell to store a reference to the current user, because if you are reusing your cells the block callback might be called after the same cell was reused for a different indexPath (so it will have a different user).
I would suggest using NSOperationQueue as presented in WWDC. See this article for reference, it think it would be helpful:
https://stavash.wordpress.com/2012/12/14/advanced-issues-asynchronous-uitableviewcell-content-loading-done-right/
Below is the example of what I do: try it:
Write this below import statement
typedef double (^add_block)(double,double);
Block - Write this in view did load
__block int bx=5;
[self exampleMethodWithBlockType:^(double a,double b){
int ax=2;
//bx=3;
bx=1000;
NSLog(#"AX = %d && BX = %d",ax,bx);
return a+b;
}];
NSLog(#"BX = %d",bx);
Method:
-(void)exampleMethodWithBlockType:(add_block)addFunction {
NSLog(#"Value using block type = %0.2f",addFunction(12.4,7.8));
}

Parse specific, how to save query results on an NSArray

Im very new to iOS and PFQuery and I need your help please
IM trying to store the array of objects obtained form PFQuery into a local NSArray, Im trying to do it inside if (!error) but it does not leave the block, once the block terminates so does the values for it on my array.
//InstallersDirectory.m
#interface InstallersDirectoryTVC ()
#property (nonatomic, strong) NSArray *supervisors;
#end
//more code goes here
- (void)viewDidLoad
{
[super viewDidLoad];
PFQuery *query = [PFQuery queryWithClassName:#"InstallersInfo"];
[query whereKey:#"supervisor" equalTo:#"yes"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
for (PFObject *object in objects) {
self.supervisors = [objects valueForKey:#"supervisor"];
}
}else {
NSLog(#"Error, %# %#",error,[error userInfo]);
}
}];
Everything works inside the block, like self.supervisors.count or NSLog, but it doesn't leave the block at all. Could you please tell me how I can get those values store definitely to self.supervisors?
Thanks !!
edit | comment
If you still need help on this issue, here is a suggestion or two:
first, self.supervisors is a NSArray. As a NSArray it has to be fully initialized and populated at creation time. So, even though you are iterating through your results, you are creating a new array each time which means only the last iteration would likely be stored in self.supervisors.
try this instead,
#property (nonatomic,strong) NSMutableArray *supervisors;
in your block:
[self.supervisors addObject:object];
//note: this will put the entire returned object in your mutable array
then later outside your block:
to list all the supervisors:
for (PFObject *supervisor in self.supervisors) {
NSLog(#"supervisor info:%#",supervisor);
}
To get the 4th supervisor in the list:
PFObject *superVisorNumber4 = [self.supervisors objectAtIndex:4];
To get the phone number from that supervisor (making this up :)
NSString *phone = [superVisorNumber4 objectForKey:#"phone"];
hope that helps

Resources