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));
}
Related
im using PFQueryTableViewController from Parse and i noticed when scrolling that images are being repeated. how can i prevent this?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object {
static NSString *CellIdentifier = #"Item";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
PFObject *photoObject = [object objectForKey:#"toObject"];
PFQuery *query = [PFQuery queryWithClassName:#"Photo"];
[query getObjectInBackgroundWithId:photoObject.objectId block:^(PFObject *gameScore, NSError *error) {
// Do something with the returned PFObject in the gameScore variable.
PFImageView *photo = (PFImageView *)[cell viewWithTag:1];
photo.file = [gameScore objectForKey:#"image"];
[photo loadInBackground:^(UIImage * _Nullable image, NSError * _Nullable error) {
}];
}];
return cell;
}
Your query returns the image asynchronously and places it in the cell object captured in your completion block. There is no guaranty that, when the query returns, you cell object hasn't already been reused by the tableview for another row.
Also, updating UI components in a background thread (which I suspect the completion block will be) is also a source of UI problems.
One way around this could be to capture the index path (instead of the cell) and use tableView.cellForRowAtIndexPath() in the completion block to make sure you're updating the right cell with the image. You should also dispatch this to the main thread in order to avoid conflicting with other updates (such as having the cell you're trying to update be victim to yet another reuse - less probable but not impossible)
This question already has answers here:
Cannot AddObject to NSMutableArray from Block
(2 answers)
Closed 8 years ago.
I have city names and coordinates stored on my core data database and trying to get the state.
In order to do this I am reverse geocoding like this
code moved below
The problem is the locationStr object is not getting stored in the tempCities array. I have tested inside the block and I know the locationStr is getting created and exists.
I've been trying to figure this out for hours. Can someone clear this up for me?
Tell me if you need any other info.
EDIT:
The array is being used to fill a table view. The code is in a helper method which returns an array (the tempCities array). I'm checking the array against nil and 0 right after the for loop.
Heres what the UISearchControllerDelegate method looks like in the View controller
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
NSString *searchText = searchController.searchBar.text;
if ([searchText length] >= 3)
{
self.resultsArray = [[[CityHelper sharedInstance]testWithSearch:searchText]mutableCopy];
[self.resultsTableView reloadData];
}
}
And in the CityHelper class
- (NSArray *) testWithSearch: (NSString *)search
{
NSArray *cities = [self getCitiesStartingWith:search];//stores NSManagedObject subclass instances with cityName, lat, and long.
NSArray *coords = [self coordinatesForCities:cities];
NSMutableArray *tempCities = [NSMutableArray new];
for (MMCoordinate *coordinate in coords) {
CLGeocoder *geocoder = [CLGeocoder new];
CLLocation *location = [[CLLocation alloc]initWithLatitude:[coordinate.latitude floatValue]
longitude:[coordinate.longitude floatValue]];
if (![geocoder isGeocoding]) {
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {
CLPlacemark *placemark = placemarks[0];
NSString *locationStr = [NSString stringWithFormat:#"%#, %#", placemark.locality, placemark.administrativeArea];
[tempCities addObject:locationStr];
}];
}
}
if (!tempCities) {
NSLog(#"its nil");
}
if ([tempCities count] == 0) {
NSLog(#"its 0");
}
return tempCities;
}
This always returns an empty (0 count) array
Essentially, the situation you have can be made clear with a simple code snippet:
- (void)someMethod
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// this will be executed in two seconds
NSLog(#"I'm the last!");
});
NSLog(#"I'm first!");
}
the block passed to dispatch_after is invoked after the method invocation ended, just like the block you passed to reverseGeocodeLocation:completionHandler:. You'll see on the console I'm first first, and then I'm the last!.
What should you do?
You need to solve that problem by introducing a callback to your method, because your method does things in the background and needs to call back when it's done. When you want to know how to declare a block for example in a method, this website explains how to use block syntax: http://fuckingblocksyntax.com/
In your case you need also to determine in the reverseGeocodeLocation:completionHandler block when to invoke the callback you should add to testWithSearch as a parameter. You could for example increase a counter every time you call reverseGeocodeLocation:completionHandler and decrease it every time when the completionHandler got invoked. When the counter reaches 0 you invoke the testWithSearch callback with the array result.
Example:
- (void)doSomething:(dispatch_block_t)callback
{
// we need __block here because we need to
// modify that variable inside a block
// try to remove __block and you'll see a compiler error
__block int counter = 0;
for (int i = 0; i < 15; i ++) {
counter += 1;
dispatch_async(dispatch_get_main_queue(), ^{
counter -= 1;
if (counter <= 0 && callback) {
callback();
}
});
}
}
Root cause is declaring the tempCities as a __block variable. Because of __block, iOS doesn't retain the tempCities on entering to the completion block. Since the completion block is executed later and the tempCities is a local variable, it actually gets deallocated at time when the completion block starts execution.
Please declare the tempCities as follows:
NSMutableArray *tempCities = [NSMutableArray new];
Sorry for the poorly worded title. I have a logic issue that I'm trying to get my head around. The view that I'm working in has a UICollectionView that displays a list of "tanks" associated with a user. This collection view displays a three items:
Tank Name
Tank Capacity
Last Image Stored
The last image stored part is where I'm having trouble. I'm making progress but its a matter of the logic behind it that I'm not sure on. Here is what the data looks like:
I have two classes that I'm interacting with; SavedTanks and SavedTankImages. The unique objectId from a saved tank is also stored as a value in SavedTankImages to allow a sort of pointer reference to the image. This logic works when the user loads a "tank" and can see all of the images they've stored associated with it.
However, for the purposes of this view, I only need to grab the first image from each tank and display that. This is where I need help. Here's what I have so far:
#pragma mark COLLECTION VIEW
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
photoHandler *cell = (photoHandler *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
_tankNameArray = [_array objectAtIndex:indexPath.section * 1 + indexPath.row];
cell.tankNameLabel.text = [_tankNameArray valueForKey:#"tankName"];
cell.tankCapLabel.text = [_tankNameArray valueForKey:#"tankCapacity"];
NSArray *objectId = [_array valueForKey:#"objectId"];
for (int i = 0; i < objectId.count; i++)
{
NSString *objectString = [[NSString alloc] init];
objectString = [objectId objectAtIndex:i];
PFQuery *imageQuery = [PFQuery queryWithClassName:#"SavedTankImages"];
[imageQuery whereKey:#"tankObjectId" equalTo:objectString];
[imageQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error)
{
for (PFObject *object in objects)
{
NSLog(#"OBJECT TEST: %#", object);
}
}
}];
}
return cell;
}
On OBJECT TEST: %#, this is the logged output:
2014-05-28 11:59:44.750 ReefTrack[305:60b] OBJECT TEST: <SavedTankImages:U6fRTuRo2c: (null)> {
tankImages = "<PFFile: 0x18a25890>";
tankObjectId = tsz4yvrIAN;
}
SavedTankImages: <x> is the objectId of the individual image, and tankObjectId is the tank the image is associated with. I'm getting close, but I need to know how I can effectively iterate and only grab the first item where tankObjectId matches the original objectId. Please forgive me if this sounds a little convoluted.
Thanks for the help in advance as usual.
UPDATE
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
photoHandler *cell = (photoHandler *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
_tankNameArray = [_array objectAtIndex:indexPath.section * 1 + indexPath.row];
cell.tankNameLabel.text = [_tankNameArray valueForKey:#"tankName"];
cell.tankCapLabel.text = [_tankNameArray valueForKey:#"tankCapacity"];
NSArray *objectId = [_array valueForKey:#"objectId"];
for (int i = 0; i < objectId.count; i++)
{
// NSString *objectString = [[NSString alloc] init];
// objectString = [objectId objectAtIndex:i];
PFQuery *imageQuery = [PFQuery queryWithClassName:#"SavedTankImages"];
[imageQuery whereKey:#"tankObjectId" equalTo:[objectId objectAtIndex:i]];
[imageQuery getFirstObjectInBackgroundWithBlock:^(PFObject *objects, NSError *error)
{
if (!error)
{
PFFile *imageFile = [objects valueForKey:#"tankImages"];
[imageFile getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error)
{
cell.parseImage.image = [UIImage imageWithData:data];
}
}];
NSLog(#"Heres your image: %#", objects);
}
}];
}
return cell;
}
The above code selects the first available image and makes it the background for every cell in collectionView. I want to get it so that it only returns the first image for the objectId. In other words
Tank 1 = tank 1 image 1
Tank 2 = tank 2 image 1
Tank 3 = tank 3 image 1
Right now this is what it's doing:
Tank 1 = tank 1 image 1
Tank 2 = tank 1 image 1
Tank 3 = tank 1 image 1
Simple solution, sort and take just the first result. Be aware this isn't as efficient as the next solution:
// sort by createdAt or use updatedAt
[imageQuery orderByDescending:#"createdAt"];
// change from find to getFirst
//[imageQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
[imageQuery getFirstObjectInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (!error)
{
NSLog(#"OBJECT TEST: %#", object);
}
}];
A more advanced solution if you'll often be querying on this is to store a pointer to the most recent object on the tank. You can either do this in your code or create a Cloud Code method to update it automatically after each SavedTankImages object is saved (it would simply load up the related SavedTanks and set mostRecentImage to point to the saved image, then save the SavedTanks).
If you have done this, then you can just use the include: method to load the mostRecentImage with the SavedTanks.
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
In my view controller's main file, I created a property for an NSArray object named finalStringsArray:
#property (strong, nonatomic) NSArray *finalStringsArray;
Then in viewDidLoad, I make sure to initialize the object:
self.finalStringsArray = [[NSArray alloc]init];
Further down the viewDidLoad method implementation, I query my server for data, get rid of some of the extra junk that the server sends me like blank space, and then I place my perfect strings inside of my finalStringsArray array:
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
NSString *parseString = [[NSString alloc]initWithFormat:#"%#", objects];
NSString *cURL=[self stringBetweenString:#"=" andString:#")" withstring:parseString];
NSString *newString = [cURL stringByReplacingOccurrencesOfString:#" " withString:#""];
NSString *newString2 = [newString stringByReplacingOccurrencesOfString:#"(" withString:#""];
NSString *newString3 = [newString2 stringByReplacingOccurrencesOfString:#"\\n" withString:#""];
_finalStringsArray = [newString3 componentsSeparatedByString:#","];
int index;
for(index = 0; index < _finalStringsArray.count; index++) {
NSString *string = [[NSString alloc]init];
string = _finalStringsArray[index];
NSLog(#"Count: %d", _finalStringsArray.count);
}
NSLog(#"Count: %d", _finalStringsArray.count);
} }
];}
All that matters in the above code is this statement: _finalStringsArray = [newString3 componentsSeparatedByString:#","];
This adds my finalized strings to my _finalStringsArray array object. You will notice that I am NSLogging the count property of my array: NSLog(#"Count: %d", _finalStringsArray.count);
When I perform these NSLogs, they always NSLog with the correct count of 2.
Here's my problem though. Further down, I have a method implementation that needs to use the count property of _finalStringsArray as well. But for some reason, it always NSLogs as "0" and I can't figure out why.
Below are the 3 method implementations that are below my viewDidLoad. I need to be able to access the count property of _finalStringsArray in the method implementation for
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section:
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
NSLog(#"all good string count3: %d", _finalParseStrings.count);
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1 ;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSLog(#"all good string count5: %d", [self.finalStringsArray count]);
return [self.finalStringsArray count];
}
findObjectsInBackgroundWithBlock: is an asynchronous method. Probably
numberOfRowsInSection: is called before the completion block has been called
and assigned a new array.
You probably should call [self.tableView reloadData] at the end of the completion
block to update the table view with the fetched data.
I don't know if PFQuery calls the completion block on the main thread.
If not then you have to dispatch the data source assignment and the reloadData call to the main queue.
You should use the proper setter & getter for that property that you declared.
I bet if you change this line:
_finalStringsArray = [newString3 componentsSeparatedByString:#","];
to this:
self.finalStringsArray = [newString3 componentsSeparatedByString:#","];
in your block function, you might have better luck.