I have an app that load data using AFNetworking, parsing income JSON data and populate table. I use different class to manage JSON and to populate UITableView.
I need to correctly set number of rows. Problem is, i guess its method load too early, before we get any data from web. I tried following:
-(NSInteger)numberOfElements{
return [self.dataDictArray count];
}
And then:
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.handler numberOfElements];
}
When handler is an instance type of class, that deal with JSON and declare -(NSInteger)numberOfElements.
How can i set correct number of elements in such situation? Apparently tableView load before we got web data, that's kind of confusing.
One way to do this is:
In success block of AFNetworking get method fire a notification
#define NOTIFICATION_NAME #"LoadNotification"
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_NAME object:self];
In viewDidLoad method of UITableViewController subclass set the class as observer of notification
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(receivedLoadingNotification:) name:NOTIFICATION_NAME object:nil];
In 'receivedLoadingNotification' method set the delegate and datasourceDelegate
-(void)receivedLoadingNotification:(NSNotification *) notification
{
if ([[notification name] isEqualToString:NOTIFICATION_NAME])
{
[self.tableView setDelegate:self];
[self.tableView setDataSource:self];
}
}
So, the controller will only call the dataSource and delegate methods when JSON data is successfully loaded from AFNetworking.
Hope it helps.
Related
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];
});
Let's say you have a UITableView that displays a list of file metadata, and you want to show the download_progress of each file in a UILabel of a custom UITableViewCell. (This is an arbitrarily long list - thus dynamic cells will be reused).
If you want to update the label without calling either reloadData or reloadRowsAtIndexPaths, how can you do it?
For those who are wondering - I don't want to call either of the reload... methods because there's no need to reload the entire cell for each percentage point update on download_progress.
The only solutions I've come across are:
Adding the cell as a key-value observer for the file's download_progress.
Calling cellForRowAtIndexPath... directly to obtain the label and change it's text.
However,
KVO in general isn't a fun api to work with - and even less so when you add cell reuse into the mix. Calling cellForRowAtIndexPath directly each time a percentage point is added feels dirty though.
So, what are some possible solutions? Any help would be appreciated.
Thanks.
As a corollary to Doug's response, here is what I ended up going with:
Each file has a unique identifier, so I made it responsible for posting notifications about updates to its attributes (think KVO, but without the hassle):
I made a FileNotificationType enum (i.e. FileNotificationTypeDownloadTriggered, and FileNotificationTypeDownloadProgress). Then I would send the progress into the NSNotification's userInfo NSDictionary along with the FileNotificationType.
- (void)postNotificationWithType:(FileNotificationType)type andAttributes:(NSDictionary *)attributes
{
NSString *unique_notification_id = <FILE UNIQUE ID>;
NSMutableDictionary *mutable_attributes = [NSMutableDictionary dictionaryWithDictionary:attributes];
[mutable_attributes setObject:#(type) forKey:#"type"];
NSDictionary *user_info = [NSDictionary dictionaryWithDictionary:mutable_attributes];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:unique_notification_id object:nil userInfo:user_info];
});
}
The file object also has a method to enumerate what types of notifications it could send:
- (NSArray *)notificationIdentifiers
{
NSString *progress_id = <FILE UNIQUE ID + FILENOTIFICATIONTYPE>;
NSString *status_id = <FILE UNIQUE ID + FILENOTIFICATIONTYPE>
NSString *triggered_id = <FILE UNIQUE ID + FILENOTIFICATIONTYPE>
NSArray *identifiers = #[progress_id, status_id, triggered_id];
return identifiers;
}
So when you update an attribute of a file elsewhere, simply do this:
NSDictionary *attributes = #{#"download_progress" : #(<PROGRESS_INTEGER>)};
[file_instance postNotificationWithType:FileNotificationTypeDownloadProgress andAttributes:attributes];
On the receiving end, my table view delegate implemented these methods to add / remove my custom UITableViewCells as observers for these notifications:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
File *file = [modelObject getFileAtIndex:indexPath.row];
for (NSString *notification_id in file.notificationIdentifiers)
{
[[NSNotificationCenter defaultCenter] addObserver:cell selector:#selector(receiveFileNotification:) name:notification_id object:nil];
}
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
[[NSNotificationCenter defaultCenter] removeObserver:cell];
}
Finally, the custom UITableViewCell has to implement the receiveFileNotification: method:
- (void)receiveFileNotification:(NSNotification *)notification
{
FileNotificationType type = (FileNotificationType)[notification.userInfo[#"type"] integerValue];
// Access updated property info with: [notification.userInfo valueForKey:#"<Your key here>"]
switch (type)
{
case FileNotificationTypeDownloadProgress:
{
// Do something with the progress
break;
}
case FileNotificationTypeDownloadStatus:
{
// Do something with the status
break;
}
case FSEpisodeNotificationTypeDownloadTriggered:
{
// Do something if the download is triggered
break;
}
default:
break;
}
}
Hopefully this helps someone who is looking to update tableview cells without having to reload them! The benefit over key-value observing is that you won't get issues if the File object is deallocated with the cell still observing. I also don't have to call cellForRow....
Enjoy!
I would create a custom cell, which I'm guessing you've done. Then I'd have the cell listen for a specific notification that your download progress method would post, then update the label there. You'd have to figure out a way for your download progress to specify a certain cell, maybe by a title string or something that would be unique that your download progress method could be told, so your cell update method could make sure the note was meant for it. Let me know if you need me to clarify my thought process on this.
I have a ViewController with a UITableView. As I wanted to split out the data handling I created an own class that answers UITableViewDataSource.
This class is supposed to first fetch data from CoreData and afterwards from a REST API.
How can the DataSource talk back to the ViewController to tell it to call reloadData on the TableView?
What's the best practice here?
I thought about:
KVO the DataSource's data and when the array change call reloadData
Handing over a block (with [self.table reloadData]) to the DataSource which gets executed every time the data changes in the DataSource
Make the table property public on the ViewController so the DataSource could call reloadData (which I don't really like as an idea)
Have a property on the DataSource which holds the ViewController with the Table to use it as a delegate (which sounds to me like a loop)
Are there any smart ways to do it? Or even common practice how to solve this?
Update:
I'm less interested in code how to do implement a certain design pattern. I'm more interested in the reasoning why to chose one pattern over the other.
Without more details, it sounds like you need a callback here. There are several methods that will work. If you have a 1 to 1 relationship (meaning your dataSource only needs to talk to the VC), then this is a good case for either:
1.) A delegate. Create your own delegate protocol for your dataSource and then have the VC adhere to that protocol (be the delegate).
2.) Do the same thing just using a block for a callback.
KVO will work just fine as well, but the above two are more in line with your scenario.
You could add a tableView property to your custom data source but that then blurs the lines of why you created that class in the first place.
For situations like this, I prefer delegates.
#class CupcakePan;
#protocol CupcakePanDelegate <NSObject>
- (void)cupcakesAreReadyForPan:(CupcakePan *)pan;
#end
#interface CupcakePan : NSObject <UITableViewDataSource>
#property (weak) id<CupcakePanDelegate> delegate;
#end
#implementation CupcakePan
- (void)bakingComplete {
[self.delegate cupcakesAreReadyForPan:self];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [cupcakes count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
return [make a CupcakeCell];
}
#end
#interface CupcakeViewController <CupcakePanDelegate>
#end
#implementation CupcakeViewController
- (void)cupcakesAreReadyForPan:(CupcakePan *)pan {
[_tableView reloadData];
}
#end
I frequently use NSNotificationCenter for these types of interactions.
In your datasource write the following code:
#define ABCNotificationName #"ABCNotificationName"
#define ABCNotificationData #"ABCNotificationData"
// ...
[[NSNotificationCenter defaultCenter] postNotificationName:ABCNotificationName object:self userInfo:#{ ABCNotificationData: data }];
In your view controller do the following:
-(void)loadView {
// setup your view
[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(datasourceUpdated:) name:ABCNotificationName object:dataSource];
}
-(void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(void)dataSourceUpdated:(NSNotification*)notification {
id data = notification.userInfo[ABCNotificationData];
// respond to the event
[self.tableView reloadData];
}
Note, that if you don't have any piece of data to communicate back to the controller it becomes even easier. In your datasource write the following code:
#define ABCNotificationName #"ABCNotificationName"
// ...
[[NSNotificationCenter defaultCenter] postNotificationName:ABCNotificationName object:self];
In your view controller do the following:
-(void)loadView {
// setup your view
[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(datasourceUpdated) name:ABCNotificationName object:dataSource];
}
-(void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(void)dataSourceUpdated {
// respond to the event
[self.tableView reloadData];
}
I need to update an image on the main view controller from a pop up view controller.
The button is called 'Feature2btn' on the Main View (EraViewController) but when I try the following code on the popup view controller it won't work.
It needs to be an immediate update as the main view is still showing in the background and does not reload so the change needs to be directly caused by the action on the pop up view.
- (IBAction)purchase:(id)sender {
HomeController = [[EraViewController alloc] initWithNibName:#"EraViewController" bundle:nil];
UIImage *image = [UIImage imageNamed:#"ico_plan.png"];
[(EraViewController*)HomeController setFeature2Btn:[feature2Btn setImage:[UIImage imageNamed:#"image.png"] forState:UIControlStateNormal];
}
There is (at least) two ways to do this:
You use a notification that one controller listens to and the other sends at the appropriate time.
You create a delegate protocol that the first controller implements and the second on calls.
The delegate one is a bit more complicated but generally considered good style. The notification one is not bad, either, but slightly less "elegant".
I will describe the notification based one here, because it seems ok for your case and would also allow to react to the purchase in multiple places by just registering for the notification there, too.
In the controller that has the image to be updated, register for a notification in viewDidAppear::
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(updateImage:) name:#"UpdateImageNotification" object:nil];
Implement the updateImage: method:
-(void)updateImage:(NSNotification*)note
{
NSString* newImageName = note.userInfo[#"imageFileKey"];
// ... update UI with the new image
}
Also make sure to deregister for that notification when the view goes away:
-(void)viewWillDisappear:(BOOL)animated
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super viewWillDisappear:animated];
}
In the other controller, that triggers the update, fire the notification at the appropriate place:
-(IBAction)purchase:(id)sender
{
// ...
NSDictionary* userInfo = #{#"imageFileKey" : newImageName};
[[NSNotificationCenter defaultCenter]
postNotificationName:#"UpdateImageNotification"
object:self userInfo:userInfo];
// ...
}
The object parameter in the notification context is to be used to specify if you want to listen to the notifications by any object or just by a very specific instance. In many cases the actual instance is not relevant, but you just discern the notifications by their name (like "UpdateImageNotification" in this case).
The userInfo dictionary is intended to carry along any information you need to provide with the notification. That's why I introduced a key "imageFileKey" that is associated with the new image name.
I think you made a small mistake. But before going ahead I just want to confirm whether following is the scenario. Correct me if I am wrong
1. You have mainViewController/HomeViewController (of type EraViewController), where you want to update the image
2. On mainViewController you have a popup screen, in which above code is written. The code is intended to change the button image on mainViewController/HomeViewController
If above is the scenario then I suggest following solution.
ERROR YOU MADE
You are creating a new object of EraViewController in the code you posted above and changing the image. According to OOPS concepts, new instance of that controller will be created(a second instance which is not visible on the screen) and you are applying new image in that instance. Now as that instance is not at all visible, you get a feeling that screen is not updating.
SOLUTION
There are at-least 3 solution to this problem
1. One solution will be Daniel Schneller gave in answer (with little bit modifications probably)
2. To achieve this through the delegates.
- You have to write the protocol in the PopViewController and have to implement that in the HomeViewController/mainViewController.
- As the image changes, in PopViewController, That has to be notified to the mainViewController, using delegate and protocol method.
- So that the mainViewController will get the notification and a protocol method will be executed. In that method you should have a code to update the image on the button.
3. (This is not suggested as this is not a good design)You have maintain the instance of the actual viewController which is visible on the screen (in a variable, lets say homeViewController). Then you can use following code in popupViewController
- (IBAction)purchase:(id)sender {
UIImage *image = [UIImage imageNamed:#"ico_plan.png"];
[(EraViewController*)homeViewController setFeature2Btn:[feature2Btn setImage:[UIImage imageNamed:#"image.png"] forState:UIControlStateNormal];
}
Hoep this helps.
Thanks
Try this,
- (IBAction)purchase:(id)sender
{
[[NSNotificationCenter defaultCenter] postNotificationName:#"updateimage" object:imageName];
}
in mainviewcontroller
-(void)viewDidLoad
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(updateiimage:) name:#"updateimage" object:nil];
}
- (void) updateiimage:(NSNotification *)notification
{
NSString * text =[notification object];
//update Image
}
I have a problem with a UITableView. I load data from a database server in a background thread and if it is finished it sends a notification. In the notification I update the data array of my view and use reloadData on the tableview.
Then the tableview deselects the selected row (that means the data is reloaded) and if I want to select another row I get EXC_BAD_ACCESS on the first line of didSelectRowAtIndexPath, even if it just is an NSLog.
If I don't assign the new array the background thread gives me to the variable data and don't use reloadData I have no problems with didSelectRowAtIndexPath but the tableview doesn't show the recent records. The user had to close the view and open it again to see the changes. That is really bad and I wanted to show the changes immediatly after the background thread finished to load the records from the server.
declared variables in the .h file:
-downloadThread is an NSThread,
-data is an NSArray,
-manager is my SQL interface.
-(void)viewDidLoad
{
...
[super viewDidLoad];
NSMutableArray *arr = [[manager getTeilnehmerList] retain];
data = arr;
[self.tableView reloadData];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(KundenUpdated:) name:#"ContactUpdate" object:nil]; // to be notified when updating thread is finished
downloadThread = [[NSThread alloc] initWithTarget:self selector:#selector(teilnehmerLoad:) object:nil]; //Thread to get actual data on Background
[downloadThread performSelector:#selector(start) withObject:nil afterDelay:2];
...
}
-(void)teilnehmerLoad:(id)sender
{
[manager loadTeilnehmerFromServerAndInsertIntoDatabase];
//data = [manager getTeilnehmerList];
[self.tableView reloadData];
[[NSNotificationCenter defaultCenter] postNotificationName:#"ContactUpdate" object:nil];
}
-(void)KundenUpdated:(NSNotification*)notifaction
{
#synchronized(self)
{
//needs function to select row that was selected before reload if the data record is still there after sync with server
[self.tableView reloadData];
NSLog(#"count data in kundenupdated: %i",data.count);
}
}
I suspect that teilnehmerLoad is called in your background thread, and it's calling reloadData, which is a no-no.
Change it (and/or KundenUpdated) to use performSelectorOnMainThread to do the reloadData.
Make sure you're not doing any other UI operations from your background thread.