I've created the master-detail view app using xcode and let xcode generate all the things it should do. I added a few buttons to the TableView and finally hooked up a responder method to these buttons: looking something like this (not all code pasted):
-(IBAction)orderBy:(UIBarButtonItem *)sender
{
int lv_temp = sender.tag;
choice = lv_temp;
switch (lv_temp) {
case 0:
break;
case 1:
{
//This gives us all the different sections available.
typeSections = [Items valueForKeyPath:#"#distinctUnionOfObjects.type"];
//Do some more stuff and create the Dictionary so we hold an array with a key for each section
[ItemsByType2 setObject:ItemsTemp forKey:[NSString stringWithFormat:#"%#", id]];
[ItemsTemp removeAllObjects];
}
The end result is that I finally have a NSDictionary object containing arrays with the section ID's as a key. I'd like to reload these sections so we have a kind of sorting.
The problem is, how and where to call the reloadData of the generated tableView? Looks like the tableView itself is not accessible in the Sender method (which sounds kind of logical to me btw) but I do not know how to call the method so the tableView will call it's methods numberOfSections, RowsPerSection etc (pseudocode btw).
Any ideas?
Cheers!
Laurens
-(IBAction)orderBy:(UIBarButtonItem *)sender {
int lv_temp = sender.tag;
choice = lv_temp;
switch (lv_temp) {
case 0:
break;
case 1: {
//This gives us all the different sections available.
typeSections = [Items valueForKeyPath:#"#distinctUnionOfObjects.type"];
//Do some more stuff and create the Dictionary so we hold an array with a key for each section
[ItemsByType2 setObject:ItemsTemp forKey:[NSString stringWithFormat:#"%#", id]];
[ItemsTemp removeAllObjects];
}
[self.tableView reloadData];
}
Related
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.
Hello: I'm using a collection view within my app, and I've noticed that it's taking longer than expected to refresh using reloadData. My collection view has 1 section, and I'm testing it with 5 cells (each of which has 2 buttons and a label). I put some logs into my code to show how long the system is actually taking to refresh. Interestingly enough, the logs indicate that it's refreshing faster than it is. On a device, for example, it will take up to ~0.2sec (noticeable), but here are the logs:
0.007s From the time reloadData is called to the time cellForItemAtIndexPath is called the first time
0.002s Per cell to load and be returned
0.041s From the time reloadData is called to the time where cell #5 is returned
There isn't anything particularly intensive in the cellForItemAtIndexPath function (basically just finds a dictionary with 3 values within an NSArray at the indexPath's row). Even when I removed this and just returned a cell with a blank button, I saw the same behavior, however.
Does anyone have any idea as to why this may be happening? It's only happening on a physical device (iPad Air), by the way. Thanks!
EDIT #1
Per #brian-nickel's comment, I used the Time Profiler instrument, and found that it does indeed spike each time reloadData is called. Here's a screenshot:
#ArtSabintsev, here is the function surrounding the reloadData call, followed by the cellForItemAtIndexPath:
//Arrays were just reset, load new data into them
//Loop through each team
for (NSString *team in moveUnitsView.teamsDisplaying) { //CURRENT TEAM WILL COME FIRST
//Create an array for this team
NSMutableArray *teamArr = [NSMutableArray new];
//Loop through all units
for (int i = [Universal units]; i > 0; i--) {
//Set the unit type to a string
NSString *unitType = [Universal unitWithTag:i];
//Get counts depending on the team
if ([team isEqualToString:currentTeam.text]) {
//Get the number of units of this type so that it supports units on transports. If the territory is a sea territory and the current unit is a ground unit, check the units in the transports instead of normal units
int unitCount = (ter.isSeaTerritory && (i == 1 || i == 2 || i == 8)) ? [self sumOfUnitsInTransportsOfType:unitType onTerritory:ter onTeam:team] : [ter sumOfUnitsOfType:unitType onTeam:team];
//Get the number of movable units on this territory
int movableCount = 0;
if (queue.selectedTerr != nil && queue.selectedTerr != ter) { //This is here to prevent the user from selecting units on another territory while moving units from one territory
movableCount = 0;
} else if (ter.isSeaTerritory && (i == 1 || i == 2 || i == 8)) { //Units on transports - can be an enemy territory
movableCount = [self sumOfUnitsInTransportsOfType:unitType onTerritory:ter onTeam:team];
} else if ([Universal allianceExistsBetweenTeam:team andTeam:ter.currentOwner] || i == 3 || i == 9) { //Other units - only planes can be on an enemy territory
movableCount = [ter sumOfMovableUnitsOfType:unitType onTeam:team];
}
//See if there are units of this type on this territory on this team
if (unitCount > 0) {
//Add data to this team's dictionary
NSMutableDictionary *unitInfo = [NSMutableDictionary new];
[unitInfo setObject:#(i) forKey:#"UnitTag"];
[unitInfo setObject:unitType forKey:#"UnitType"];
[unitInfo setObject:#(unitCount) forKey:#"Count"];
[unitInfo setObject:#(movableCount) forKey:#"MovableCount"];
[unitInfo setObject:team forKey:#"Team"];
//Add the dictionary
[teamArr addObject:unitInfo];
//Increment the counter
if (unitsOnCT) { //Must check or it could cause a crash
*unitsOnCT += 1;
}
}
}
}
//Add the team array
[moveUnitsView.unitData addObject:teamArr];
}
//Reload the data in the collection view
[moveUnitsView.collectionV reloadData];
And my cellForItemAtIndexPath's relevant code:
//Dequeue a cell
UnitSelectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"UnitSelectionCell" forIndexPath:indexPath];
//Get the team array (at the index of the section), then the unit's data (at the index of the row)
NSMutableDictionary *unitData = (moveUnitsView.unitData[indexPath.section])[indexPath.row];
//Get values
int unitTag = [[unitData objectForKey:#"UnitTag"] intValue];
int count = [[unitData objectForKey:#"Count"] intValue];
int movableCount = [[unitData objectForKey:#"MovableCount"] intValue];
NSString *unitType = [unitData objectForKey:#"UnitType"];
//Set the cell's values
[cell.upB addTarget:self action:#selector(upMoveUnits:) forControlEvents:UIControlEventTouchUpInside]; [cell.upB setTag:unitTag];
[cell.iconB setBackgroundImage:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[Universal imageNameForUnit:unitType team:[unitData objectForKey:#"Team"]] ofType:nil]] forState:UIControlStateNormal];
[cell.iconB setTitle:[Universal strForExpDisplay:count] forState:UIControlStateNormal];
[Universal adjustTitlePlacementOfB:cell.iconB autosize:FALSE]; //Don't autosize because this is a collection view
cell.unitTypeL.text = unitType;
cell.unitTypeL.adjustsFontSizeToFitWidth = cell.unitTypeL.adjustsLetterSpacingToFitWidth = TRUE;
//Set fonts
[Universal setFontForSubviewsOfView:cell];
//Return the cell
return cell;
When the collection view is initialized, cells are registered using:
[moveUnitsView.collectionV registerNib:[UINib nibWithNibName:#"UnitSelectionCell" bundle:nil] forCellWithReuseIdentifier:#"UnitSelectionCell"];
EDIT #2
#roycable and #aaron-brager pointed out that this could be caused by using imageWithContentsOfFile:. To test this out, I changed cellForItemAtIndexPath to this:
//Dequeue a cell
UnitSelectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"UnitSelectionCell" forIndexPath:indexPath];
//Get the team array (at the index of the section), then the unit's data (at the index of the row)
NSMutableDictionary *unitData = (moveUnitsView.unitData[indexPath.section])[indexPath.row];
//Get values
int unitTag = [[unitData objectForKey:#"UnitTag"] intValue];
[cell setBackgroundColor:[UIColor redColor]];
[cell.upB removeTarget:nil action:NULL forControlEvents:UIControlEventTouchUpInside];
[cell.upB addTarget:self action:#selector(upMoveUnits:) forControlEvents:UIControlEventTouchUpInside]; [cell.upB setTag:unitTag];
//Return the cell
return cell;
Strangely, this doesn't seem to fix the issue. It's literally doing no intensive tasks in that function but it still seems to be lagging (and the Time Profiler seems to confirm that).
In response to the requests for code from Universal, instead of posting code I'll just summarize it:
+units just returns 17
+unitWithTag: uses a switch to return an NSString corresponding to a number between 1-17
+allianceExistsBetweenTeam: sees if an array contains one of the strings
+setFontForSubviewsOfView: is a recursive function that basically uses this code
Unfortunately, this doesn't seem very relevant since the issue is still occurring with the oversimplified cellForItemAtIndexPath function.
I also implemented #aaron-brager's new suggestions. I removed the target before adding a new one, and I made the changes to Time Profiler. I didn't see anything really pop out... Here's the screenshot. Everything related to UIImage is irrelevant to this question, as is NSKeyedArchiver, so the only other things that really make sense are strings, arrays, and dictionaries:
Any and all help is greatly appreciated - I really need to get this fixed (hence the bounty). Thank you!
Edit #3 - Solution identified
So, it turns out that the issue wasn't in either of those functions. The issue was the function (let's call it Function A) that called the update function above (let's call it Function B). Right after Function A called Function B, it performed a CPU-intensive task. I wasn't aware of the fact that reloadData is at least partially asynchronous, so I'm assuming the CPU-intensive task and reloadData ended up racing for CPU time. I solved my problem by adding the following right before return cell;:
if (indexPath.row == [self collectionView:collectionView numberOfItemsInSection:indexPath.section] - 1) {
[self performSelector:#selector(performMyCPUIntensiveTask:) withObject:myObject afterDelay:0.1];
}
I hope this helps someone else in the future. Thank you to everyone who helped, I sincerely appreciate it.
Make sure you're on the main thread when you call reloadData.
NSLog("on main thread: %#", [NSThread isMainThread] ? #"YES" : #"NO");
If you're not then use GCD to send the message on the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
[moveUnitsView.collectionV reloadData];
});
(Not 100% sure about syntax, I just typed this into the browser)
Some possibilities:
Your presumably recursive function to set the fonts is probably expensive.
A few of the other Universal functions look like they might be expensive.
It does not appear that you ever remove the button target and every time a cell is reused, you are adding additional targets to it.
imageWithContentsOfFile: skips the cache; use imageNamed: instead.
Currently I have a custom view table cell and a text field just above it. I want to get the text from the UItextfield and put that into an NSMutableArray.
Pseudocode:
String text = _textfield.text;
[array addObject:text];
NSLog{array}
In my header file I have created the textfield and the array.
I currently receive the error : 'CustomTableView:[340:11303] array: (null)' when I NSLog.
I am not to sure why the text from the textfield is not getting added to the array. If any one is able to help it will be greatly appreciated.
Note - My textfield is above the custom cell not in it. I have even tried just adding a string to the array directly and logging it and I get the same error. So I would assume that this is something to do with the array.
did you initialize your Array.take a MutableArray and initialize it.
NSMutableArray *array=[NSMutableArray alloc]init];
You mentioned that you have declared the textfield and the array in your header file...
Have you initialised the variable array?
e.g.
array = [NSMutableArray new];
It looks like you are not actually creating the array. In Objective C, you do not create things in header file, you declare them. The implementation files(.m files) do all the work.
Try this:
NSString *text = _textfield.text;
array = #[text]
NSLog( #"%#", array );
This is how you should print your array,
NSLog(#"%#", array);
It looks as if your a newbie to ios.Go through the objective-c and Read the apple documentation carefully.
NSString * text = self.textfield.text;
NSMutableArray *array = [NSMutableArray alloc] init];
[array addObject:text];
NSLog(#"%#",array);
For me this is what worked...
I have taken one textfield inside tableviewcell. I am creating textfields based on dynamic data. My requirement is , I need to get textfields text which are created dynamically.
For getting text in another method
NSIndexPath *indexPath = [tableViewObj indexPathForCell:customCell];
if (indexPath.row==0)
{
[arrayPhoneNumbers addObject:customCell.textFieldObj.text];
NSLog(#"array is :%#",arrayPhoneNumbers);
}
else if(indexPath.row==1)
{
[arrayPhoneNumbers addObject:customCell.textFieldObj.text];
NSLog(#"array is :%#",arrayPhoneNumbers);
}
else if(indexPath.row==2)
{
[arrayPhoneNumbers addObject:customCell.textFieldObj.text];
NSLog(#"array is :%#",arrayPhoneNumbers);
}
Like this I have added textfield text to array. Let me know if you have any doubts.
Just have a quickly question (more of a curiosity thing) based on a problem I just solved (I will post the answer to my problem in the post, which can be found here: My former question
The thing is that I have this UITableView which contains custom cell objects. Every time you enter this view, I generate new cells for the UITableView like this:
if (cell == nil)
{
[[NSBundle mainBundle] loadNibNamed:#"UploadCellView" owner:self options:nil];
cell = customCell;
}
Which happens in the standard method:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
Now the problem is that my custom cell objects listens for NSNotifications about upload objects happening in the background, so they can update its model data to their labels and progress bars etc. It happens like this (this is a method from the custom cell objects):
-(void) uploadProgress: (NSNotification*)notification
{
NSDictionary *userInfo = [notification userInfo];
NSNumber *uploadID = [userInfo valueForKey:#"uploadID"];
if (uploadID.integerValue == uploadActivity.uploadID)
{
UIProgressView *theProgressBar = (UIProgressView*)[self viewWithTag:progressBarTag];
[theProgressBar setProgress:(uploadActivity.percentageDone / 100) animated:YES];
UILabel *statusText = (UILabel*)[self viewWithTag:percentageTag];
[statusText setText:[NSString stringWithFormat:#"Uploader - %.f%% (%.01fMB ud af %.01fMB)", uploadActivity.percentageDone, uploadActivity.totalMBUploaded, uploadActivity.totalMBToUpload]];
}
}
When an upload finish they simply do this:
-(void) uploadFinished: (NSNotification*)notification
{
NSDictionary *userInfo = [notification userInfo];
NSNumber *uploadID = [userInfo valueForKey:#"uploadID"];
if (uploadID.integerValue == uploadActivity.uploadID)
{
[self setUploadComplete];
[[ApplicationActivities getSharedActivities] markUploadAsFinished:uploadActivity];
NSLog(#"BEGINNING RELOAD");
[parentTable reloadData];
NSLog(#"ENDING RELOAD");
}
}
Now the problem is when they call their owning tableview. When the view which the tableview is contained within dismisses, the old custom cell objects are still alive in the background getting NSNotfications. And when that upload is then done, the old custom cell objects from the former table views still tries to call that parentTable property which was set at that time, now resulting in calling random junk memory.
The way I solved this was to keep an array of all cell objects getting created in the table and then make them stop listening when the view is dismissed like this:
-(void) viewWillDisappear:(BOOL)animated
{
for (UploadCell *aCell in lol)
{
[aCell stopListening];
}
[self.navigationController popViewControllerAnimated:YES];
}
But this seems like a bit of a hack. How would I go about making sure that the custom cell objects are deleted when the view is dismissed? Because when the view is intialized again, new cells are simply made anyways, so I have no use for the old ones.
The custom view cells have a strong property pointer to the tableview they get associated with, but I thought the ARC would make sure that TableView pointer would not get invalidated then? Obviously it is somehow. Maybe because of the containing view being deleted when popped?
Sounds like the cells have a retain property pointing back to your UITableViewDataSource class.
They should instead have an assign property, then they will be released properly when the table view is released (which it currently cannot be if your cells are retaining it).
Also, the cells should shut down notifications when they are dropped out of the tableview, by overriding the cells didMoveToSuperview method:
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
if ( [self superview] == nil )
{
[self unsubscribeFromYourNotifications];
}
}
That is so if they scroll off screen they will not be wasting resources updating things.
Have you considered a separate update model that keeps a map between uploadIDs and cells that listens for the notification? That way, the cells aren't responsible for updating the table themselves, the update model would do it. When the table goes away, you can shut down the update model.
I'm loading a TableView from Core Data and it works like a charm. The data contains two fields: Category and Distance. The initial load of the table uses an array with the objects sorted based on Distance. I have a button in the Navigation Bar that I want the user to use to toggle between a Distance-sorted view (the default) and a Category-sorted view. My code for the toggle is:
-(void)toggleView {
NSString *baseItem = #"Proximity View";
NSString *currTitle = self.title;
NSComparisonResult result;
result = [baseItem compare:currTitle];
if (result == 0) {
self.title = NSLocalizedString(#"Category View",#"Categories");
tpData = tpDataCat; //tpDataCat is an array sorted by Category
[self.tblView reloadData];
} else {
self.title = NSLocalizedString(#"Proximity View",#"Distances");
tpData = tpDataDist; //tpDataDist is an array sorted by Distance
[self.tblView reloadData];
}
[baseItem release];
[currTitle release];
}
When I click the toggle button and fire `toggleView, the app just crashes. Any help would be greatly appreciated!!
You shouldn't be releasing baseItem and currTitle.
I would recommend reading the Memory Management Programming Guide; it's an excellent document that can provide background on the appropriate ownership of objects and when releasing would be required.