I have a UITableView that uses paging. All the delegates, and datasource are set.
My table view fetches a list of ten cars over the network and displays them by sending a page number (currentPage). During this fetch request I also get the pageCount which is the number of pages that contains cars on the server. Each page contains 10 cars.
I create a loading cell on the row that equals self.allCars.count which is my car array. This cell then fetches the next ten, and adds them to the self.allCars.count array. A loading cell is then created again for self.allCars.count + 1 etc. (I hope you get the picture, if not please ask).
On first launch the list contains All Cars which is the default request. However, the user can change it from a drop down. For example, they can select Blue Cars. This is passed into the fetchCars methods as the params parameter.
There is an unwanted behaviour in my code however: When I scroll down through the list, with the default paramter selected, and I scroll down three pages (three network calls to fetchCars...) and the array now contains 30 cars displayed in the tableView. However I now want to start a different search from scratch, so I go to the drop down, and select to filter by only blue cars (donePickerBlue). This method removes all the car objects, sets the currentPage back to 1, calls the network for the blue cars, and reloads the data. The unwanted behaviour occurs here. Because there had been 30 cells/indexPath.rows, the network call is called 3 times. This is because the indexPath.row < self.allCars.count is not true. This is where I am stuck, I can't seem to figure out how to fix it, so that if the search parameter is change (blue in this case) that it should treat it as new, I thought the [tableView reloadData] would handle this, but unfortunately it remembers how many index paths there are.
Its something i've been stuck on for a while. I've a feeling im missing something very simple to fix it.
Header file
#property (nonatomic) NSInteger currentPage;
#property (nonatomic) NSInteger pageCount;
Implementation
-(void)viewDidLoad{
...
self.currentPage = 1;
...
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
if (self.allCars.count ==0) {
return 0;
}
else{
if (self.currentPage<self.pageCount)
return self.allCars.count+1;
}
return 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell * cell = nil;
if (self.allCars.count!=0) {
if(indexPath.row <self.allCars.count){//here is where the problem occurs
cell=[self customCellForIndexPath:indexPath tableView:tableView];
}
else {
cell=[self loadingCell];
}
}
else{
// Disable user interaction for this cell.
cell = [[UITableViewCell alloc] init];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
return cell;
}
-(UITableViewCell *)loadingCell{
UITableViewCell * cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
UIActivityIndicatorView * activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
activityIndicator.center = cell.center;
cell.backgroundColor = [UIColor lightGrayColor];
[cell addSubview:activityIndicator];
cell.tag=kLoadingCellTag;
[activityIndicator startAnimating];
return cell;
}
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
if (cell.tag==kLoadingCellTag) {
self.currentPage++;
[self performSelector:#selector(getCars:withParams) withObject:nil afterDelay:1.5f];
}
}
-(void)getCars{
[self getCars:url withParams:params];
}
-(void)getCars: (NSURL *)url withParams: (NSString *)params{
NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:0 timeoutInterval:80];
[request setHTTPBody:[params dataUsingEncoding:NSUTF8StringEncoding]];
[request setHTTPMethod:#"POST"];
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForResource=1;
NSURLSession * session = [NSURLSession sessionWithConfiguration:sessionConfig];
NSURLSessionDataTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse * httpResp = (NSHTTPURLResponse *)response;
NSDictionary * dataDict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
if (data) {
switch (httpResp.statusCode) {
case 200:{
dispatch_async(dispatch_get_main_queue(), ^{
self.pageCount = [dataDict[#"message"][#"total_pages"] intValue];
NSArray * carsArray = dataDict[#"message"][#"results"];
for (NSDictionary *cDict in carsArray) {
Car *car = [Car carWithID:[cDict[#"car_id"] stringValue] ];
car.car_name=cDict[#"car_name"];
car.car_description = cDict[#"car_description"];
[self.allCars addObject:car];
}
[self.tableView reloadData];
});
break;
}
default:
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Error");
});
break;
}
}
else{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Error");
});
}
}];
[task resume];
}
//reset list to start new search
-(void)donePickingBlue{
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES];
self.currentPage=1;
[self.allCars removeAllObjects];
[self getCars:url withParams:blue];
}
Edit
I seem to have resolved the the problem by doing the following;
//reset list to start new search
-(void)donePickingBlue{
self.currentPage=1;
[self.allCars removeAllObjects];
[self.tableView reloadData];//after removing all the cars, now we call reload, as there are no cars. I was calling reload in `[self getCars:....]` just below, and thought this was enough.
[self getCars:url withParams:blue];
}
I was able to answer my own problem. The answer can be seen in the Edit above incase anybody else has the same problem.
It should have been;
//reset list to start new search
-(void)donePickingBlue{
self.currentPage=1;
[self.allCars removeAllObjects];
[self.tableView reloadData];//after removing all the cars, now we call reload, as there are no cars. I was calling reload in `[self getCars:....]` just below, and thought this was enough.
[self getCars:url withParams:blue];
}
If you want to download cars page by page, willDisplayCell: is pretty good choice. But you must change the condition a little, to prevent downloading the same data multiple times. Also, I recommend you to change data model and provide ability to determine a page for particular cars. That's what I mean:
-(void)tableView:(UITableView *)tableView
willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath{
// 10 cells on page
NSUInteger currentPage = indexPath.row / 10;
// Check, if cars for the current page are downloaded
if (carsOnPagesDict[#(currentPage)] != nil) {
// Add a stub to indicate that downloading started
// You can use this later to display correct cell
// Also it prevents getCars: from calling multiple times for the current page
carsOnPagesDict[#(currentPage)] = #"downloading";
// I removed delay for simplicity
[self getCars:url withParams:params forPage:currentPage];
}
}
Also, change getCars method:
-(void)getCars:(NSURL *)url withParams:(NSString *)params forPage:(NSUInteger)page{
// Creating request...
// ...
// Processing response...
// ...
// Array obtained:
NSArray *carsArray = dataDict[#"message"][#"results"];
// Storing required data to the array
NSMutableArray *cars = [NSMutableArray arrayWithCapacity:carsArray.count];
for (NSDictionary *cDict in carsArray) {
Car *car = [Car carWithID:[cDict[#"car_id"] stringValue] ];
car.car_name=cDict[#"car_name"];
car.car_description = cDict[#"car_description"];
[cars addObject:car];
}
// Save cars to the dictionary for the page given
carsOnPagesDict[#(page)] = cars;
// ...
// Resuming tasks...
}
You may consider using CoreData to store that cars.
Related
I made a clone of UITableView called TableView with its own dataSource and delegate that mimics the original UITableView but is intended to do some things differently. I also made a GoogleSuggest class with its own delegate that requests google autocomplete suggestions from a known URL.
The GoogleSuggest class has this method:
- (void)requestSuggestionsForText:(NSString *)text {
[NSThread detachNewThreadSelector:#selector(asyncRequestSuggestionsForText:)
toTarget:self
withObject:text];
}
When called it dispatches this private background thread:
- (void)asyncRequestSuggestionsForText:(NSString *)text;
When it receives results it calls this delegate method:
- (void)googleSuggestDidReceiveResult:(GoogleSuggestResult *)result;
Everything worked fine with little controlled experiments until I put it all together in the main ViewController.
Initially, this method returned a "UI API called on a background thread" error:
#pragma mark - GoogleSuggestDelegate
- (void)googleSuggestDidReceiveResult:(GoogleSuggestResult *)result {
_googleSuggestions = result.suggestions;
[_tableView reloadData];
}
Then I replaced the last line with this and it worked:
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
Now, I'm getting a "-[__NSCFNumber length]: unrecognized selector sent to instance 0xbbe7252cd9143595" error.
The result.suggestions is a simple NSMutableArray with NSString variables, no NSNumbers anywhere.
This works and I get to see all results logged:
- (TableViewCell *)tableView:(tableView *)tableView cellAtIndex:(NSUInteger)index {
TableViewCell *cellView = [[TableViewCell alloc] init];
NSString *result = [_googleSuggestions objectAtIndex:index];
NSLog(#"Result: %#", result);
// cellView.titleLabel.text = result;
return cellView;
}
This also works and I get to see all results logged:
- (TableViewCell *)tableView:(tableView *)tableView cellAtIndex:(NSUInteger)index {
TableViewCell *cellView = [[TableViewCell alloc] init];
NSString *result = [_googleSuggestions objectAtIndex:index];
NSLog(#"Result: %#", result);
cellView.titleLabel.text = #"example text";
return cellView;
}
This fails when I try to assign the result to the titleLabel.text:
- (TableViewCell *)tableView:(tableView *)tableView cellAtIndex:(NSUInteger)index {
TableViewCell *cellView = [[TableViewCell alloc] init];
NSString *result = [_googleSuggestions objectAtIndex:index];
NSLog(#"Result: %#", result);
cellView.titleLabel.text = result;
return cellView;
}
It makes no sense, it's clearly an NSString variable assigned to an object that has no problem with NSString variables like shown in the working examples above.
How do you properly implement async search results?
How do you properly update UI elements from a background thread?
What is the correct way of using NSDiffableDataSourceSnapshot and - (void)tableView:(nonnull UITableView *)tableView prefetchRowsAtIndexPaths:(nonnull NSArray<NSIndexPath *> *)indexPaths.
It seems that every time prefetch reloads table view, table view asks for more prefetching, after calling apply snapshot, creating infinite loop.
- (void)reloadViews {
//[self.tableView reloadData];
NSMutableArray *items = [NSMutableArray new];
for (TCHChannel* channel in self.channels) {
[items addObject:channel.sid];
}
if ([items count] == 0) {
return;
}
NSDiffableDataSourceSnapshot<ConversationSectionType*, NSString*> *snapshot =
[[NSDiffableDataSourceSnapshot<ConversationSectionType*, NSString*> alloc] init];
ConversationSectionType *main = [ConversationSectionType new];
main.section = kMain;
[snapshot appendSectionsWithIdentifiers:#[main]];
[snapshot appendItemsWithIdentifiers:items intoSectionWithIdentifier:main];
[self.diffDataSource applySnapshot:snapshot animatingDifferences:NO];
}
And here is prefetch method:
- (void)tableView:(nonnull UITableView *)tableView prefetchRowsAtIndexPaths:(nonnull NSArray<NSIndexPath *> *)indexPaths {
for (NSIndexPath *indexPath in indexPaths) {
TCHChannel *channel = [self channelForIndexPath:indexPath];
NSMutableSet *currentChannelIds = [NSMutableSet new];
for (ConversationListViewModelUpdateOperation *op in self.modelQueue.operations) {
[currentChannelIds addObject:[op channelId]];
}
if ([currentChannelIds containsObject:channel.sid]) {
continue;
}
NSParameterAssert(channel != nil);
ConversationListViewModelUpdateOperation *op = [[ConversationListViewModelUpdateOperation alloc] initWithChannel:channel cache:self.channelViewModelsCache];
op.completionBlock = ^{
dispatch_async(dispatch_get_main_queue(), ^(void){
[self reloadViews];
});
};
[self.modelQueue addOperation:op];
}
}
Model queue is just operation queue:
- (NSOperationQueue*)modelQueue {
if (_modelQueue == nil) {
_modelQueue = [[NSOperationQueue alloc] init];
_modelQueue.maxConcurrentOperationCount = 4;
}
return _modelQueue;
}
Is there a way to use prefetching with diffable data sources without apply asking for more indexes?
EDIT:
So calling reloadData in prefetch methods makes infinite loop.. According to https://andreygordeev.com/2017/02/20/uitableview-prefetching/
WARNING: do not call tableView.reloadData() or
tableView.reloadRows(...) from tableView(_ tableView: UITableView,
prefetchRowsAt indexPaths: [IndexPath]) method! These methods provoke
UITableView to call prefetchRowsAt... and thus lead to infinity loop.
Soo.. how has Apple intended for prefetching to be used with Diffable Data Sources? ... -.-
This question already has answers here:
Table View with Images, slow load and scroll
(4 answers)
Closed 7 years ago.
I'm creating a UITableViewController to display the roster of a hockey team. The tableViewController makes calls to the web to get the player's stats and a small picture to display in the tableViewCell. However, when I scroll through the TableView, it isn't smooth. It's incredibly jagged. How can I make it so (if this will decrease its work load) the player's pictures don't load until they're on-screen? Here is my current code (I've subclassed UITableViewCell):
EDIT: I've edited my code to follow a comment below. The property imagesCache is actually a UIMutableDictionary (confusing, sorry). However, now I get the error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** setObjectForKey: object cannot be nil (key: http://app-assets3.sportngin.com/app_images/noPhoto-square.jpg?1428933774)'
*** First throw call stack:
(0x1865f6530 0x1975cc0e4 0x1864e1348 0x1000496a8 0x185f87168 0x1874d3be8 0x187425374 0x187414ecc 0x1874d694c 0x1000acf94 0x1000b7db8 0x1000b02c4 0x1000ba5d4 0x1000bc248 0x197dfd22c 0x197dfcef0)
libc++abi.dylib: terminating with uncaught exception of type NSException
Here is my code:
#import "RosterTableTableViewController.h"
#import "TFHpple.h"
#import "RosterListing.h"
#import "RosterListingCellTableViewCell.h"
#interface RosterTableTableViewController ()
#property (nonatomic, strong) NSMutableArray *rosters;
#property (nonatomic, strong) NSMutableDictionary *imagesDictionary;
#property NSMutableDictionary *imageCache;
#end
#implementation RosterTableTableViewController
- (void) loadRoster
{
NSURL *RosterURL = [NSURL URLWithString:#"http://www.lancers.com/roster/show/1502650?subseason=197271"];
NSData *RosterHTMLData = [NSData dataWithContentsOfURL:RosterURL];
TFHpple *RosterParser = [TFHpple hppleWithHTMLData:RosterHTMLData];
// Get the data
NSString *RosterNumberPathQueryString = #"//tbody[#id='rosterListingTableBodyPlayer']/tr/td[#class='number']";
NSArray *RosterNumberNodes = [RosterParser searchWithXPathQuery:RosterNumberPathQueryString];
NSString *RosterNamePathQueryString = #"//tbody[#id='rosterListingTableBodyPlayer']/tr/td[#class='name']/a";
NSArray *RosterNameNodes = [RosterParser searchWithXPathQuery:RosterNamePathQueryString];
NSString *RosterImagePathQueryString = #"//tbody[#id='rosterListingTableBodyPlayer']/tr/td[#class='photo']/a/img";
NSArray *RosterImageNodes = [RosterParser searchWithXPathQuery:RosterImagePathQueryString];
NSMutableArray *rosterItems = [[NSMutableArray alloc] initWithCapacity:0];
for (int i = 0; i < RosterNumberNodes.count; ++i) {
RosterListing *thisRosterListing = [[RosterListing alloc] init];
thisRosterListing.playerNumber = [[[RosterNumberNodes objectAtIndex:i] firstChild] content];
thisRosterListing.playerName = [[[RosterNameNodes objectAtIndex:i] firstChild] content];
thisRosterListing.playerURL = [[RosterNameNodes objectAtIndex:i] objectForKey:#"href"];
#try {
thisRosterListing.playerImageURL = [[RosterImageNodes objectAtIndex:i] objectForKey:#"src"];
}
#catch (NSException *e) {}
/*
NSLog(#"%#", thisRosterListing.playerNumber);
NSLog(#"%#", thisRosterListing.playerName);
NSLog(#"%#", thisRosterListing.playerURL);
NSLog(#"%#", thisRosterListing.playerImageURL);
*/
[rosterItems addObject:thisRosterListing];
}
self.rosters = rosterItems;
}
- (instancetype) initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
self.navigationItem.title = #"Roster";
self.imageCache = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self loadRoster];
// Load the Cell NIB file
UINib *nib = [UINib nibWithNibName:#"RosterListingCellTableViewCell" bundle:nil];
// Register this NIB, which contains the cell
[self.tableView registerNib:nib forCellReuseIdentifier:#"RosterCell"];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 54;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
return self.rosters.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Get a new or recycled cell
RosterListingCellTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"RosterCell" forIndexPath:indexPath];
RosterListing *thisRosterListing = [self.rosters objectAtIndex:indexPath.row];
cell.playerNumberLabel.text = thisRosterListing.playerNumber;
cell.playerNameLabel.text = thisRosterListing.playerName;
__block UIImage *image = [self.imageCache objectForKey:thisRosterListing.playerImageURL];
cell.imageView.image = image;
if(image == nil) {
//If nil it's not downloaded, so we download it,
//We MUST download in a separate thread otherwise the scroll will be really slow cause the main queue will try to download each cell as they show up and every time they show up
NSURL *imageURL = [NSURL URLWithString: thisRosterListing.playerImageURL];
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:imageURL
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
//Completion Handler is executed in an async way
if([self.imageCache objectForKey:thisRosterListing.playerImageURL] == nil)
self.imageCache[thisRosterListing.playerImageURL] = image;
//We need to execute the image update in the main queue otherwise it won't work
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
RosterListingCellTableViewCell *aCell = (RosterListingCellTableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
aCell.imageView.image = image;
}];
}];
[dataTask resume];
}
return cell;
}
Working with images in UITableViewCells can be a bit tricky at first, I do have a code that might help you, give me a second while I search it.
Basically what you want to do is check that the row you downloaded the image for is still been displayed (as the user can scroll faster than images are downloaded) and after download ended storage it locally so you won't have to download it again.
EDIT: Here is the code, sorry the lateness
#propery NSMutableDictionary *imagesDictionary;
...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
//...
//We load the model information for that cell
NSDictionary *cellInfo = self.dataModel[indexPath.row];
__block UIImage *image = [self.imagesDictionary objectForKey:[cellInfo objectForKey:#"avatar"]];
cell.avatarView.image = image;
if(image == nil) {
//If nil it's not downloaded, so we download it,
//We MUST download in a separate thread otherwise the scroll will be really slow cause the main queue will try to download each cell as they show up and every time they show up
NSURL *imageURL = [NSURL URLWithString:URL];
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:imageURL
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
//Completion Handler is executed in an async way
if([self.imagesDictionary objectForKey:[cellInfo objectForKey:#"avatar"]] == nil)
if(error == nil) {
// no error
image = [UIImage imageWithData:data];
if (image == nil) {
//nil image, in my case I use a default undefined image
image = [UIImage imageNamed:#"undefined_user"];
}
//Now we are sure image is never nil
[self.imagesDictionary setObject:image forKey:[cellInfo objectForKey:#"avatar"]];
//We need to execute the image update in the main queue otherwise it won't work
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
UITableviewCell *aCell = (UITableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
aCell.avatarView.image = image;
}];
}];
[dataTask resume];
return cell;
I have issue with redrawing images of uicollectionview cells - images are downloaded from URL parsed from JSON. I have 10 cells and with the start of app only first 6 are displayed, but images are not loaded, when I scroll down to those other cells, they have their images, and when I scroll back to the top, first 4 cells have their images as well, only cells 5 and 6 are not redrawn(they were visible for the whole time). I've been trying to debug this for long time but no success.
in cellForItemAtIndexPath I'm calling SDWebImage(but it doesn't really matter):
[cell.backgroundImage setImageWithURL:[NSURL URLWithString:timeSnap.thumbnailImageURL] placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
but when cellForItemAtIndexPath is called for the first time, timeSnap.thumbnailImageURL(where timeSnap entity is model for cell) is not initialised yet and so it's null
after initialisation of timeSnap.thumbnailImageURL I have:
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView reloadData];
});
edit1:
adding my code, it's a bit complex - first I'll get JSON from server API with items to be displayed in collection view, then for each item I have to get another JSON from server API, from which I can build URL for timeSnap.thumbnailImageURL
viewController class:
- (void)viewDidLoad
{
[super viewDidLoad];
[self refreshNewTimeSnapsFeed];
}
- (void)refreshNewTimeSnapsFeed {
Adapter *adapter = [AppDelegate instance].adapter;
[adapter getNewTimeSnapsWithCompletion:^(NSArray* jsonResponse) {
[self newTimeSnapsFeedRefreshed:jsonResponse];
}];
}
- (void)newTimeSnapsFeedRefreshed:(NSArray*)jsonResponse {
Adapter *adapter = [AppDelegate instance].adapter;
for (NSDictionary *timeSnapDict in jsonResponse) {
TimeSnap *timeSnap = [[TimeSnap alloc] initWithJSON:timeSnapDict];
[adapter getTimeSnapSnapsforId:timeSnap.id withCompletion:^(NSArray* jsonResponse) {
[timeSnap getSnapsJSON:jsonResponse];
[timeSnap getTimeSnapThumbnailImageURL];
}];
[self.timeSnaps addObject:timeSnap];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView reloadData];
});
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
TimeSnapCell *cell = (TimeSnapCell*)[collectionView dequeueReusableCellWithReuseIdentifier:#"TimeSnapCell" forIndexPath:indexPath];
TimeSnap *timeSnap = self.timeSnaps[indexPath.row];
cell.label.text = timeSnap.name;
[cell.backgroundImage setImageWithURL:[NSURL URLWithString:timeSnap.thumbnailImageURL] placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
return cell;
}
adapter class:
- (void)getNewTimeSnapsWithCompletion:(void (^)(NSArray* jsonResponse))completion {
NSString *urlAsString = [NSString stringWithFormat:#"..."];
NSURL *url = [[NSURL alloc] initWithString:urlAsString];
[self getJSONWithCompletion:completion fromURL:url forKey:#"Items"];
}
- (void)getTimeSnapSnapsforId:(NSNumber*)id withCompletion:(void (^)(NSArray* jsonResponse))completion {
NSString *urlAsString = [NSString stringWithFormat:#"..."];
NSURL *url = [[NSURL alloc] initWithString:urlAsString];
[self getJSONWithCompletion:completion fromURL:url forKey:#"Items"];
}
- (void)getJSONWithCompletion:(void (^)(NSArray* jsonResponse))completion fromURL:(NSURL*)url forKey:(NSString*)key {
//code for getting JSON from server API, then parsing into NSArray *items
dispatch_async(dispatch_get_main_queue(), ^{
if(completion){
completion(items);
}
});
}
You need to reload the cell when the second JSON response comes back:
for (NSDictionary *timeSnapDict in jsonResponse) {
TimeSnap *timeSnap = [[TimeSnap alloc] initWithJSON:timeSnapDict];
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.timeSnaps.count inSection:0];
[self.timeSnaps addObject:timeSnap];
[adapter getTimeSnapSnapsforId:timeSnap.id withCompletion:^(NSArray* jsonResponse) {
[timeSnap getSnapsJSON:jsonResponse];
[timeSnap getTimeSnapThumbnailImageURL];
[self.collectionView reloadItemsAtIndexPaths:#[indexPath]];
}];
}
Your adapter class executes the completion block on the main queue, so you don't need to use dispatch_async here (unless you don't consider that part of the adapter's contract).
remove the dispatch into the reloadData and also remove reloadData and add just this code
dispatch_async(dispatch_get_main_queue(), ^{
[cell.backgroundImage setImageWithURL:[NSURL URLWithString:timeSnap.thumbnailImageURL] placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
});
I have tableview where is name and status. Status is changed when come apple push notification (APNS).
But I have this problem. What can I do, if notification didn't come? Or if user tap on close button of this message.
I try to update table by using ASIHTTPRequest:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
HomePageTableCell *cell = (HomePageTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
NSManagedObject *device = [self.devices objectAtIndex:indexPath.row];
cell.nameLabel.text = [device valueForKey:#"name"];
if ([[device valueForKey:#"status"] isEqualToNumber:#1])
{
cell.status.text = #"Not configured";
cell.stav.image = [UIImage imageNamed:#"not_configured.png"];
}
if ([[device valueForKey:#"status"] isEqualToNumber:#2])
{
//some other states
}
return cell;
}
I try this to change status before cell is loading...
- (void) getStatus:(NSString *)serialNumber
{
NSURL *url = [NSURL URLWithString:#"link to my server"];
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
__weak ASIHTTPRequest *request_b = request;
request.delegate = self;
[request setPostValue:#"updatedevice" forKey:#"cmd"];
[request setPostValue:serialNumber forKey:#"serial_number"]; //get status of this serial number
[request setCompletionBlock:^
{
if([self isViewLoaded])
{
[MBProgressHUD hideHUDForView:self.view animated:YES];
if([request_b responseStatusCode] != 200)
{
ShowErrorAlert(#"Comunication error", #"There was an error communicating with the server");
}
else
{
NSString *responseString = [request_b responseString];
SBJsonParser *parser = [[SBJsonParser alloc] init];
NSDictionary *result = [parser objectWithString:responseString error:nil];
status = [result objectForKey:#"status"];
NSInteger statusInt = [status intValue]; //change to int value
//here I want to change cell status in SQLite, but don't know how
//something with indexPath.row? valueForKey:#"status"???
}
}
}];
[request setFailedBlock:^
{
if ([self isViewLoaded])
{
[MBProgressHUD hideHUDForView:self.view animated:YES];
ShowErrorAlert(#"Error", [[request_b error] localizedDescription]);
}
}];
[request startAsynchronous];
}
Or it is better way to change status in my table view if apple notification didn't come or user didn't tap on notification message? Thanks
EDIT:
I don't know how to store data to NSManagedObject *device. Can you help me with this?
I try this, but it didn't works: (on place where you write)
NSInteger statusInt = [status intValue]; //change to int value
NSManagedObject *device = [self.devices objectAtIndex:indexPath.row];
[device setValue:statusInt forKey:#"status"];
EDIT2:
I get it, but problem is with reload table data
NSString *responseString = [request_b responseString];
SBJsonParser *parser = [[SBJsonParser alloc] init];
NSDictionary *result = [parser objectWithString:responseString error:nil];
NSString *status = [result objectForKey:#"status"];
NSInteger statusInt = [status intValue]; //change to int value
NSManagedObject *device = [self.devices objectAtIndex:indexPath.row];
[device setValue:statusInt forKey:#"status"]; //there is problem in save statusInt
// [device setValue:#5 forKey:#"status"]; //if I do this it is ok status is integer16 type
and second problem is in that reload table data. I put there this
[self.tableView reloadData]
but It reloading again and again in loop, what is wrong? I thing there is infinite loop, if I didn't reload table data changes will be visible in next app load. I think problem is that I call
- (void) getStatus:(NSString *)serialNumber atIndexPath:(NSIndexPath *)indexPath
{}
in
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
}
Better should be in viewDidLoad or viewDidApper, but I don't know how make loop for all devices and call
[self getStatus:[device valueForKey:#"serialNumber"] atIndexPath:indexPath];
on that place.
EDIT3:
what if I do it like this:
- (void)viewDidLoad
{
[self updateData];
[self.tableView reloadData];
}
-(void)updateData
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Device"];
request.returnsDistinctResults = YES;
//request.resultType = NSDictionaryResultType;
request.propertiesToFetch = #[#"serialNumber"];
NSArray *fetchedObjects = [self.managedObjectContext
executeFetchRequest:request error:nil];
NSArray *result = [fetchedObjects valueForKeyPath:#"serialNumber"];
//there I get all serialNumbers of my devices and than I call method getStatus and get "new" status and than update it in Core Data.
}
Is that good way to solve this problem? I think better will be if I call getStatus method only one times and get array of statuses.
Maybe I can set all serialNubers in one variable ('xxx','yyyy','zzz') and on server do SELECT * FROM Devices WHERE serialNumber in (serialNuber).
Do you think this could work? I don't have experience how to take data from array to string like ('array_part1','array_part2'....)
Where in your code do you call [UITableView reloadData]?
You should call reloadData on your tableview once you have retrieved the new data from the server. As your server call is async the server call will run on a separate thread while the main thread continues, therefore I presume you have the following problem...
- (void) ...
{
[self getStatus:#"SERIAL_NUMBER"];
[self reloadData]; // This will be called before the async server call above has finished
}
Therefore you are reloading the original data and therefore the new data, which may have loaded a few seconds after, wont be shown.
To fix this, adjust the [getStatus:] method to call the [UITableView reloadData] method on server response.
[request setCompletionBlock:^
{
if([self isViewLoaded])
{
[MBProgressHUD hideHUDForView:self.view animated:YES];
if([request_b responseStatusCode] != 200)
{
ShowErrorAlert(#"Comunication error", #"There was an error communicating with the server");
}
else
{
NSString *responseString = [request_b responseString];
SBJsonParser *parser = [[SBJsonParser alloc] init];
NSDictionary *result = [parser objectWithString:responseString error:nil];
status = [result objectForKey:#"status"];
NSInteger statusInt = [status intValue]; //change to int value
// Store the server response in NSManagedObject *device,
// which will be used as the data source in the tableView:cellForRowAtIndexPath: method
// Once stored, check the tableview isn't NULL and therefore can be accessed
// As this call is async the tableview may have been removed and therefore
// a call to it will crash
if(tableView != NULL)
{
[tableView reloadData];
}
}
}
}];
ASIHTTPRequest is also no longer supported by the developers, I suggest you look into AFNetworking.
Update
In response to the problem you are now having with setting the statusInt within the device NSManagedObject
NSManagedObject *device = [self.devices objectAtIndex:indexPath.row];
[device setValue:statusInt forKey:#"status"]; //there is problem in save statusInt
This is caused as statusInt is an NSInteger which is a primary datatype and not an NSObject as expected by [NSManagedObject setValue:forKey:]. From the documentation for [NSManagedObject setValue:forKey:], the methods expected parameters are as follows.
- (void)setValue:(id)value forKey:(NSString *)key
Therefore you need to pass, in this case, an NSNumber. The problem with NSInteger is that it's simply a dynamic typedef for the largest int datatype based on the current system. From NSInteger's implementation you can see the abstraction.
#if __LP64__
typedef long NSInteger;
#else
typedef int NSInteger;
#endif
If your current system is 64-bit it will use the larger long datatype.
Now, technically the returned status value from the server can be stored as it is without any conversion as an NSString. When you need to retrieve and use the primary datatype of int you can use the [NSString intValue] method you have already used.
Although it's best practice to use a NSNumberFormatter which can be useful for locale based number adjustments and ensuring no invalid characters are present.
NSString *status = [result objectForKey:#"status"];
NSNumberFormatter * f = [[NSNumberFormatter alloc] init];
NSNumber * statusNumber = [f numberFromString:status];
NSManagedObject *device = [self.devices objectAtIndex:indexPath.row];
[device setValue:statusNumber forKey:#"status"];
To retrieve the primary datatype when you wish to use the int within your code, simply call the [NSNumber intValue].
NSNumber *statusNumber = [device objectForKey:#"status"];
int statusInt = [statusNumber intValue];
As for the problem you are having with the infinite loop, this is caused by called [... getStatus:atIndexPath:], which contains the method call reloadData, from within [UITableView tableView:cellForRowAtIndexPath:].
This is because reloadData actually calls [UITableView tableView:cellForRowAtIndexPath:].
Therefore your code continuously goes as the following...
Initial UITableView data load -> tableView:cellForRowAtIndexPath: -> getStatus:atIndexPath: -> Server Response -> reloadData -> tableView:cellForRowAtIndexPath: -> getStatus:atIndexPath: -> Server Response -> reloadData -> ...
Unfortunately you cant just force one cell to update, you have to request the UITableView to reload all data using reloadData. Therefore, if possible, you need to adjust your server to return an unique ID for devices so you can adjust only the updated device within your NSManagedObject.
A suggested alteration for the getStatus method could be just to use the serialNumber if this is stored within the NSManagedObject as a key.
- (void) getStatus:(NSString*)serialNumber