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.
Related
I want to fetch data from server with multiple calls inside for loop. I'm passing different parameter each time. I know it is possible to fetch data like, I'm fetching in code below :
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
// url with feedItem data.
NSURL *url = ....
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
dispatch_async(dispatch_get_main_queue(), ^{
// adding object to table data source array
[dataSourceArray addObject:[placeData objectForKey:#"data"]];
// reloading table view.
[self.tableView reloadData];
});
}
} failure:^(NSError *error) {
}];
}
The problem is, Whenever I add data to dataSourceArry, It is not adding sequentially. It is adding according to response of API calls. Please let me know, If it is not clear.
In your case, I would allocate a mutable array first and set [NSNull null] at each position:
NSInteger count = [[feed objectForKey:#"content"] count];
NSMutableArray *dataSourceArray = [NSMutableArray arrayWithCapacity:count];
for (NSInteger i = 0; i < count; ++i) {
[dataSourceArray addObject:[NSNull null]];
}
Then, I would use something called dispatch groups (see more here http://commandshift.co.uk/blog/2014/03/19/using-dispatch-groups-to-wait-for-multiple-web-services/):
__block NSError *apiCallError = nil; // Just to keep track if there was at least one API call error
NSInteger index = 0;
// Create the dispatch group
dispatch_group_t serviceGroup = dispatch_group_create();
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
// Start a new service call
dispatch_group_enter(serviceGroup);
// url with feedItem data.
NSURL *url = ...
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
dispatch_async(dispatch_get_main_queue(), ^{
// Add data to Data Source
// index should be the correct one, as the completion block will contain a snapshot of the corresponding value of index
dataSourceArray[index] = [placeData objectForKey:#"data"];
}
dispatch_group_leave(serviceGroup);
} failure:^(NSError *error) {
apiCallError = error;
dispatch_group_leave(serviceGroup);
}];
index++;
}
dispatch_group_notify(serviceGroup, dispatch_get_main_queue(),^{
if (apiCallError) {
// Make sure the Data Source contains no [NSNull null] anymore
[dataSourceArray removeObjectIdenticalTo:[NSNull null]];
}
// Reload Table View
[self.tableView reloadData];
});
Hope it works for you.
This might be of help for you,
//keep dictionary property which will store responses
NSMutableDictionary *storeResponses = [[NSMutableDictionary alloc]init];
//Somewhere outside function keep count or for loop
NSInteger count = 0;
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
//Find out index of feddItem
NSInteger indexOfFeedItem = [[feed objectForKey:#"content"] indexOfObject:feedItem];
NSString *keyToStoreResponse = [NSString stringWithFormat:#"%d",indexOfFeedItem];
// url with feedItem data.
NSURL *url = ....
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
//instead of storing directly to array like below
// adding object to table data source array
[dataSourceArray addObject:[placeData objectForKey:#"data"]];
//follow this
//increase count
count++;
[storeResponses setObject:[placeData objectForKey:#"data"] forKey:keyToStoreResponse];
// reloading table view.
if(count == [feed objectForKey:#"content"].count)
{
NSMutableArray *keys = [[storeResponses allKeys] mutableCopy]; //or AllKeys
//sort this array using sort descriptor
//after sorting "keys"
for (NSString *key in keys)
{
//add them serially
[dataSourceArray addObject:[storeResponses objectForKey:key]];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
}
} failure:^(NSError *error) {
}];
}
Edit : The answer I have given is directly written here,you might face compilation errors while actually running this code
Don't reload your table each time in the loop. After the loop finishes fetching data , do a sorting on your datasourcearray to get the desired result and then reload table.
This is because you're calling web-services asynchronously so it's not give guarantee that it's give response in sequence as you have made request!
Now solutions for that :
You should write your api like it's give all data at a time. So,
You not need to make many network call and it will improve
performance also!
Second thing you can make recursive kind of function, I mean make another request from completion handler of previous one. In this case once you got response then only another request will be initialize but in this case you will have to compromise with performance!! So first solution is better according to me!
Another thing you can sort your array after you get all the responses and then you can reload your tableView
Try the following :-
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
// url with feedItem data.
NSURL *url = ....
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
// adding object to table data source array
[dataSourceArray addObject:[placeData objectForKey:#"data"]];
// reloading table view.
dispatch_sync(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
});
} failure:^(NSError *error) {
}];
}
There are some similar questions that already exist on StackOverflow. I did check them out, and in most cases it returns nil because the NSMutableArray has not been initialised. But in my case I did initialise it.
Here's part of my code :
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
self.storedTimeZones = [[NSMutableArray alloc] init];
NSData *storedData = [[NSUserDefaults standardUserDefaults] dataForKey:#"timeZones"];
if (storedData != nil ) {
self.storedTimeZones = [NSKeyedUnarchiver unarchiveObjectWithData:storedData];
}
NSString *str = [self.smallRegionArray[indexPath.row] stringByReplacingOccurrencesOfString:#"_" withString:#" "];
[self.storedTimeZones addObject: str];
NSLog(str); //the string was printed successfully.
NSLog(#"%lu", (unsigned long)self.storedTimeZones.count); //'0' was printed
}
update
#Caleb was right, [NSKeyedUnarchiver unarchiveObjectWithData:storedData returned nil. I solved it by doing this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
self.storedTimeZones = [[NSMutableArray alloc] init];
NSData *storedData = [[NSUserDefaults standardUserDefaults] dataForKey:#"timeZones"];
NSMutableArray *ary = [NSKeyedUnarchiver unarchiveObjectWithData:storedData];
if (ary != nil ) {
self.storedTimeZones = ary;
}
NSString *str = [self.smallRegionArray[indexPath.row] stringByReplacingOccurrencesOfString:#"_" withString:#" "];
[self.storedTimeZones addObject: str];
NSLog(str);
NSLog(#"%lu", (unsigned long)self.storedTimeZones.count); //Now it prints `1`
}
But in my case I did initialise it.
You did, but then you replaced it. Here's the initializing line:
self.storedTimeZones = [[NSMutableArray alloc] init];
but then you assign a different value:
self.storedTimeZones = [NSKeyedUnarchiver unarchiveObjectWithData:storedData];
and at this point, self.storedTimeZones may be either a non-mutable instance of NSArray, or some entirely different object, or nil. I'd guess it's the latter since no exception is thrown when you call -addObject:. That the count is 0 also makes sense if self.storedTimeZones is nil, since the result of messaging nil is nil or 0 or NO, depending on what type you expect.
All you really need to do to properly diagnose the problem is to examine self.storedTimeZones near your last NSLog statement. Set a breakpoint and look at the contents of that property. Useful commands will be:
po self.storedTimeZones
po [self.storedTimeZones class]
You're trying to assign the object to the entire mutable array, you're not adding a object to it.
That's not the way it works.
Try this:
[self.storedTimeZones addObject:
[NSKeyedUnarchiver unarchiveObjectWithData:storedData]];
Or this
[self.storedTimeZones addObjectsFromArray:[NSKeyedUnarchiver unarchiveObjectWithData:storedData]];
Or this
self.storedTimeZones = [NSArray alloc]; // Note: not a mutable array
[self.storedTimeZones initWithArray:[NSKeyedUnarchiver unarchiveObjectWithData:storedData]];
You've never told us if the contents of the keyedUnarchiver is an array, but we expect it is.
In either case, if this is the only time you add info to the array, you don't need to make it mutable.
Good luck.
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
getting a "Thread 1: EXC_BAD_ACCES(code=2, adress=0xbf7fffffc)" error at
NSArray *tempArray ] [lijstString componentsSeperatedByString:#","];
What can i do about this?
This is the whole codepart:
-(NSMutableArray *)lijstArray{
NSString *lijstString = self.details[#"lijst"];
NSArray *tempArray = [lijstString componentsSeparatedByString:#", "];
self.lijstArray = [NSMutableArray array];
for (NSString *lijstValue in tempArray) {
[self.lijstArray addObject:[lijstValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
}
return self.lijstArray;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 2;
}
Your lijstArray getter function is infinitely recursive. Assuming lijstArray is an #property, every time you use self.lijstArray you are calling the instance method lijstArray if used as a getter or setLijstArray if used as a setter.
You are using self.lijstArray three times. The first use on the left part of the assignment operator is only calling [self setLijstArray: ... ] so while that will trample the _lijstArray iVar, it will not cause recursion.
You cause recursion in two places, though once is enough. First is with [self.lijstArray addObject: ... ] which is the same as [[self lijstArray] addObject: ... ]. This causes infinite recursion.
And then with return self.lijstArray which is the same as return [self lijstArray] -- again the lijstArray instance method is calling itself. This also causes infinite recursion.
Incidentally the stack trace would be informative-- you'd have a very deep stack.
try this:
-(NSMutableArray *)lijstArray{
if(!_lijstArray){ //If you need get new lijstArray always, comment this line, and below "}"
NSString *lijstString = self.details[#"lijst"];
NSArray *tempArray = [lijstString componentsSeparatedByString:#", "];
_lijstArray = [NSMutableArray array];
for (NSString *lijstValue in tempArray) {
[_lijstArray addObject:[lijstValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
}
}
return _lijstArray;
}
Check to make sure the line:
self.details[#"list"];
Is not null. Also check to make sure tempArray and lijstString are being allocated and initialized.
- (void)searchTableList {
NSString *searchString = searchBar.text;
NSString *str=[[stories valueForKeyPath:#"name"] componentsJoinedByString:#"#"];
NSLog(#"desired string:%#",str);
NSMutableArray *array = [[NSMutableArray alloc]init];
array = [str componentsSeparatedByString:#"#"];
//Never attempt to use compare with an array of dictionaries have to extract strings first first
for (NSString *tempStr in stories) {
NSComparisonResult result = [tempStr compare:searchString options:(NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch) range:NSMakeRange(0, [searchString length])];
if (result == NSOrderedSame) {
[filteredContentList addObject:tempStr];
}
}
}
First of all, filteredContentList is never allocated in your code above, so it will always point to nil. Add smth like filteredContentList = [NSMutableArray array] in viewDidLoad.
Secondary, you rely on isSearching boolean flag to detect wether your are dealing with search results table view or general table view of you controller. This is, IMHO, bad practice.
You should rely on tableView parameter, which is being passed to every method of table views delegates (in your case -- your UITableViewController). Set tags or compare tableView parameter to self.tableView.
Last thing -- you do not need to call reloadData at viewDidLoad.