Tableview cell data changed on API callback after scrolling - ios

My application list an array of feeds. Feeds are displayed in a tableview. Each cell has a likebutton and a feed data object. When the likebutton is clicked, an API call will happen and it is written inside the table cell subclass. On success of the API call I need to update the likebutton image and feed data object. But if I scroll the tableview after starting the API call and before receiving Onsuccess, the data object and likebutton I refer in the Onsuccess method will have a different index(due to cell reuse). How can I refer the data object when the API call was started? My code is given below.
#import "FeedCell.h"
- (IBAction)likeAction:(id)sender
{
[APIManager unlikeORunlikePost:self.feedObject.entityID withSuccess:^(id response)
{
//Here I want to get the 'self.feedObject' which was passed to the API manager
//If I try to get 'self.feedObject' , that object will be different from what I passed initially if the tableview is scrolled before entering this success block
}
andFailure:^(NSString *error)
{
}];
}

weakly capture the feed object in the block and compare it to the cells current feed object, if they are the same then the cell hasn't been reused.

The block is only capturing self, which is the cell, and as you've found this is changed if you have scrolled the table and the cell is reused.
Capture the specific data object instead:
- (IBAction)likeAction:(id)sender
{
FeedObject *feedObject = self.feedObject;
[APIManager unlikeORunlikePost:feedObject.entityID withSuccess:^(id response)
{
[feedObject doSomething]; // This will now be the original object
//Here I want to get the 'self.feedObject' which was passed to the API manager
//If I try to get 'self.feedObject' , that object will be different from what I passed initially if the tableview is scrolled before entering this success block
}
andFailure:^(NSString *error)
{
}];
}
However:
Should your cell be doing much more after this completion call? Isn't it the responsibility of a different object?
Returning the affected object in the completion block would be neater

Related

How to make API call in UITableVIewCell

I have a Scenario in which I have to make a API request to update UILables in TableViewCell .
The problem is that for each cell I have to make a Unique API request. The API url is same but the parameter is different.
Currently I am making calls in cellForRowAtIndex and In success block I am using dispatch_async to update the array and reloading the UITableView.
My cellForRowAtIndexMethod :
if(!apiResponded) //Bool value to check API hasn't responded I have to make API request
{
cell.authorLabel.text = #"-------";// Set Nil
NSString *userId =[CacheHandler getUserId];
[self.handleAPI getAuthorList:userId]; //make API Call
}
else
{
cell.authorLabel.text = [authorArray objectAtIndex:indexPath.row];// authorArray is global Array
}
My success Block of API Request :
numOfCallsMade = numOfCallsMade+1; //To track how manny calls made
apiResponded = YES; // to check API is reponded and I have to update the UILables
dispatch_async(kBgQueue, ^{
if(!authorArray)
authorArray = [[NSMutableArray alloc]init];
NSArray *obj = [responseData valueForKey:#"aName"];
if(obj == nil)
{
[authorArray addObject:#"N/A"];
}
else
{
[authorArray addObject:[obj valueForKey:#"authorName"]];
}
dispatch_async(dispatch_get_main_queue(), ^{
if(numOfCallsMade == [self.mCarsArray count]) // this is to check if I have 10 rows the 10 API request is made then only update
[self.mTableView reloadData];
});
});
When I run this code I am getting Same Value for each Label. I don't know my approach is good or not. Please any one suggest how can Achieve this.
From your code, I’m not really sure what you want to achieve. All I know is that you want to make a request per each cell, and display received data. Now I don’t know how you’d like to store your data, or how you’ve setup things, but I’ll give you a simple suggestion of how you could set this up, and then you can modify as needed.
I assume you only need to make this request once per cell. For simplicity, we could therefore store a dictionary for the received data (author names?).
#property (nonatomic, strong) NSMutableDictionary *authorNames;
We need to instantiate it before usage, inside init or ViewDidLoad, or wherever you see fit (as long as it's before TableView calls cellForRowAtIndexPath:).
authorNames = [[NSMutableDictionary alloc] init];
Now in cellForRowAtIndexPath, you could do the following:
NSInteger index = indexPath.row
cell.authorLabel.text = nil;
cell.tag = index
NSString *authorName = authorNames[#(index)];
if (authorName) { // Check if name has already exists
cell.authorLabel.text = authorName;
} else {
// Make request here
}
In your requests completion block (inside CellForRowAtIndexPath:), you add this:
NSString *authorName = [responseData valueForKey:#“aName”];
authorNames[#(index)] = authorName; // Set the name for that index
if (cell.index == index) { // If the cell is still being used for the same index
cell.authorLabel.text = authorName;
}
When you scroll up and down in a TableView, it will reuse cell that are scrolled outside of the screen. That means that when a request has finished, the cell could have been scrolled offscreen and reused for another index. Therefore, you want to set the cell tag, and when the request has completed, check if the cell is still being used for the index you made the request for.
Potential issues: When scrolling up and down fast, when your requests are still loading, it could potentially make multiple requests for each cell. You'll have to add some way to just make each request once.
You can declare a method in your custom cell and then call it from cellForRowAtIndex , the method will call the API and update the label present only in that cell.
So for each cell you will have separate method calls & each success block will update the particular cell Label text only.

populating tableview datasource inside init method from remote server

I have a tabBarView which have two tableViews. each of these tableViews will represent some news from a remote server. I want to populate tableView's datasource when tableViewController's init method is called. so I have put the needed networking operation inside init method. My init method is this:
- (instancetype) init{
self = [super init];
[NewsManager fetch:10 remoteNewsOfLanguage:#"fa" withOffsett:1 andCompletionHandler:^(NSMutableArray *news) {
self.newsList = news;
}];
self.tabBarItem.title = #"my title";
return self;
}
newsList is an array holding news loaded from server.
But when I run my project the order of invocation for tableViewController's methods is like the following:
tableViewController's init method is called and finished (but the completion handler block is not called yet)
tableViewController's viewDidLoad method is called ( it is called when the tableViewController is added to tabBarView's viewControllers array)
tableViewController's delegate method tableView:numberOfRowsInSection is called
the network operation's completionHandler block is called and the newsList array is set to the retrieved news from server
So my problem is that before my newsList array is populated the method tableView:numberOfRowsInSection is called and so my tableView is not filled with any news. How should I solve this issue?
Thanks
you should reload table data after you get data from server. then only your table will show updated data.
[NewsManager fetch:10 remoteNewsOfLanguage:#"fa" withOffsett:1 andCompletionHandler:^(NSMutableArray *news) {
self.newsList = news;
[yourTableview reloadData];//add this line
}];
The added line does the job and makes the new data to be loaded in the tableView but there is a small point that I think you should consider
[tableView reloadData]
will be executed in a thread other than mainThread and this will cause a 5 to 10 seconds delay for the data to be loaded on the tableView.
to prevent this you should somehow tell it to run the reloadData method on the main thread. this is done with the dispatch_async. So you should call [tableView reloadData] like this:
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});

How to stop Delegate Methods to Run before Data gets loaded from JSON?

I am using AFNetworking GET method in ViewDidLoad. My UITableView delegate methods runs before data is loaded in Arrays. I am getting error of NSArray beyond bounds .
Please help me through it . Its my first time o JSON .
I searched Stackoverflow and google .But didn't got proper answer.
You shouldn't be refreshing your table view before the download completes if you don't want it pulling in data from blank arrays.
You should be refreshing your data in the success block of your AFNetworking call.
[connectionMgr GET:#"yourURL" parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) { //Note that 204 is considered a success message
//Reload your table view
[self.tableView reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) { //Note that this is called even if the download is cancelled manually
//Failure
}];
EDIT
Since you're using a UITableViewController, you should put a check in your numberOfRowsInSection to see if the array is nil or if it contains 0 objects. Then it won't try to generate any cells.
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (array == nil || array.count < 1) {
return 0;
} else {
return array.count; //Or whatever you're using
}
}
I'm assuming that you aren't using array.count for the number of cells, otherwise you probably wouldn't be having this issue.
I would suggest having an NSArray property that will contain the data you are going to put in the table view. numberOfRowsForSection: return the count of the array. In your success block, set the array equal to the data you have returned & call reloadData. This way your table view will try creating 0 cells when there's no data & as many as you need after you have received the data.

iOS: How to share data between different views of Tab Bar Controller

My app has two views managed by a Tab Bar Controller. One of the views is Google Map (GMSMapView using their SDK) and the other is a TableView showing a list of the same data. The markers on the map are the same data in the TableView (just alternate presentations of the same data).
I fetch the data from an NSURLSessionDataTask. I'm wondering what is the best way to share that data between the two views. Obviously, I don't want to fetch the data twice for each view. But I'm not sure what is the best practice for making that shared data available/synched between the two views.
A similar question was asked but not answered here.
You can create a model class which holds the map related data in an array/dictionary/custom class objects. You can make this model class as a singleton(can be initialized only once). Both view controllers (i.e the map and table view) can refer to this model to populate date in different views now.
Model Class
-----------
#property (strong, nonatomic) MyCustomDataRepresentationObj *data;
+ (id)sharedModel {
static MyModelClass *sharedModel = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedModel = [[self alloc] init];
});
return sharedModel;
}
-(void)fetchMapDataWithCompletionBlock:(void(^)(id response, NSError *error)onComplete
{
// Check if data is available.
// Note: You can add a refresh data method which will fetch data from remote servers again.
if (!data) {
__weak MyModelClass *weakSelf = self;
// Make HTTP calls here, assume obj is returned value.
// Convert network response to your data structure
MyCustomDataRepresentationObj *objData = [MyCustomDataRepresentationObj alloc] initWith:obj];
// Now hold on to that obj in a property
weakSelf.data = objData;
// Return back the data
onComplete(objData, error);
} else {
onComplete(objData, nil); // Return pre fetched data;
}
}
Now in view controllers you would have to call the model class method which will inturn make the network call(if needed) and returns data in completion block.
View Controller 1
-----------------
-(void)viewDidLoad
{
// This is where the trick is, it returns the same object everytime.
// Hence your data is temporarily saved while your app is running.
// Another trick is that this can be accessed from other places too !
// Like in next view controller.
MyModel *myModelObj = [MyModel sharedModel];
// You can call where ever data is needed.
[myModelObj fetchMapDataWithCompletionBlock:^(id response, NSError *error){
if (!error) {
// No Error ! do whats needed to populate view
}
}];
}
Do the same in other view controller.
View Controller 2
-----------------
-(void)viewDidLoad
{
// Gets the same instance which was used by previous view controller.
// Hence gets the same data.
MyModel *myModelObj = [MyModel sharedModel];
// Call where ever data is needed.
[myModelObj fetchMapDataWithCompletionBlock:^(id response, NSError *error){
if (!error) {
// No Error ! do whats needed to populate view
}
}];
}
Note: I have just jotted down these lines of code here, there might be syntax errors. Its just to get the basic idea.
A UITabBarController act as a Container.
So from your 2 child ViewControllers, you can access the TabBarViewController with the property parentViewController.
So if you want to share the same data with your 2 child ViewControllers, you can fetch and store your data in your UITabBarController. And, from your UIViewControllers, you can access it like this
MyCustomTabBarController *tabBar = (MyCustomTabBarController*)self.parentViewController;
id data = tabBar.myCustomData;
Use Singleton Patterns create a singleton class and initialize singleton instance in your AppDelegate.m this way you can access your singleton class instance from your AppDelegate by using
How about a data fetching object? Make a new class that makes requests for your data bits and stores the results internally.
You then could get the data into your ViewController with a number of different methods:
Direct Reference Associate this object with each ViewController as a property on the ViewControllers before setting the viewControllers property on the Tab Bar Controller.
Your interface to this new class could include the set of fetched results, as well as a method (with a callback when the request finished perhaps) to tell the object to fetch more results.
Notification Center Your object could post notifications when it has more data, and just include a method to start requesting more data.
Delegate + Registration You could create a protocol for objects that want to get told about changes to the data set, make sure all of your necessary ViewControllers conform, and have a delegates NSArray property on your data fetching object. This is far more manual than Notification Center, but it's slightly easier if you need a very robust interface.
Needless to say, there are a lot of ways to handle this, but they all start with designating a class to do the specific task of fetching/storing your data.

UICollectionView refresh data

I am using a CollectionView which displays an array of objects.
On clicking a button i fill this array with a new set of data.
I want to refresh the CollectionView with this data.
Is there any statement to update this instead of comparing each items and selectively deleting and adding? The reloadData usually ends up in the following error.
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
In Short, I am looking for the following steps...
1)Fill the datasource array, show the data.
2)Fill the datasource array with new data, refresh the CollectionView to show the data.
Thanks in Advance
Try - (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(BOOL finished))completion.
In your case, you want "an all new set of data", so to speak, so e.g:
[myCV performBatchUpdates:^{
// one of:
// a)
[myCV deleteSection:someIndexSetForTheEntireSection];
[myRealDataSource empty:someIndexSetForTheEntireSection];
//
// OR b)
[myCV deleteItemsAtIndexPaths:someSetOfIndexPaths];
[myRealDataSource removeIndexPaths:someSetOfIndexPaths];
// Either case:
NSArray *indexPaths = [myRealDataSource getNewDataAndReturnIndexPaths];
// if a)
[myCV insertSections:newIndexSetForNewSection];
// Either case:
[myCV insertItemsAtIndexPaths:newIndexSetForInsertions];
}
completion:^(BOOL finished) {
NSLog(#"Done.");
// Maybe signal something else if you want.
}];
performBatchUpdates:completion: will expect the deletions & insertions from the original data source check entering the function to add up to the correct data source size leaving the method. It will loudly complain otherwise.
If you only have one section (section 0), you can be much more general than specific index paths if you are always "removing everything" and "inserting a complete new set".
Another option to to use KVO to listen on insertions and removals from the data source and simply reloadData, reloadItemsAtIndexPaths: or reloadSections: as appropriate.
I prefer the reactive KVO version, as I tend to use collection views with Core Data, and not pseudo-static or in-memory NSArray's.
To figure out the CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION issue, I'd setup a breakpoint on all exceptions, and try to discover what is really triggering the issue. Likely your datasource is gone and there's a bad access when you try to read/write from it.
Suppose you arrive on your view then you can add data to your array in viewDidLoad method like so:
-(void)viewDidLoad
{
[super viewDidLoad];
// If you have already data
self.arr_sample=(NSArray *)array;
/*
When you want to download data from server than you have to call reloadData
method of collection because all delegate method already called before view load.
So loading data will take time to load data than you have to call all delegate
method of collectionview by calling reloadData method.
*/
[self loadData];
// Do any additional setup after loading the view from its nib.
}
but first of all you have set the delegate of collectionview .
do you want to download data from server than you can call reloaddata method of collection view. such as
-(void)loadData
{
// Do your downloading code her and call to reload data method of collectionview
[collectionview reloadData];
}
now again do you want to refill your array with new data on your button click than you can do
-(void)refillData
{
// Download new data code here and follow
[array removeAllObjects];
array=(NSArray *)newarray;
[collectionview reloadData];
}

Resources