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.
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.
I have this very strange error happening when I'm changing view controllers in my iOs app.
First some background info.
I am retrieving an image from the web in a background NSOperation. This operation is started from another (background) operation that is instantiated in an collection view cell. The way this works is that the cell creates an object , then sets itself as an observer for that object, then creates an NSOperation with that object as a parameter. This first level operation will start the second operation that will get the image from the web and another NSOperation that will try to get the data from a file (if possible) and report it via delegation to the first operation. That first level operation will change a property on the observed object thus triggering the KVO. The collection/tableView cell will update from the - observeValueChange method.
Here is the problem:
Sometime the cell disappears (reused or deallocated) and when the background thread tries to set the value on the observed object it triggers an EXC_BREAKPOINT exception ([collectionViewCell message retain sent to deallocated instance]).
In order to prevent this I tried implementing -prepareForReuse and -dealloc on the cells. But the error keeps happening.
The flow seem like this:
-User loads VC that has collectionViewWithCells
-cell creates object and NSOperation 1
NSoperation 1 creates NSOperation2 (this is of two types get from web or get from file)
NSOpeartion 2 get image from internet or from a local file
NSoperation 2 sends data to NSOperation1
User has left this screen
NSOperation 1 tries to set data on observed object
-- CRASH
Here is the code inside the cell:
#interface CustomCollectionViewCell ()
#property (strong, nonatomic) NSOperationQueue *imagesOperationQueue;
#property (strong, nonatomic) ImageObject *imgObj;
#end
#implementation CustomCollectionViewCell
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
- (void)prepareForReuse{
[self clearDelegatesAndObservers];
[super prepareForReuse];
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
}
*/
- (void) getImage {
self.imgObj = [ImageObject newRefrenceWithId:obj_ref];
[self.imgObj addObserver:self forKeyPath:#"data" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
TaskImageReqCache *imgReq = [[TaskImageReqCache alloc] initWithUrl:imgUrl andImageObject:self.imgObj];
[self.imagesOperationQueue addOperation:imgReq];
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == self.imgObj) {
UIImage *img = [UIImage imageWithData:self.imgObj.data];
self.thumbnailImage.image = img;
}
}
- (void)dealloc
{
[self clearDelegatesAndObservers];
}
- (void)clearDelegatesAndObservers
{
[self.imagesOperationQueue cancelAllOperations];
self.thumbnailImage.image = nil;
[self.imgObj removeObserver:self forKeyPath:#"data"];
[self.pageListAdapter removeDelegateAtIndex:self.myIndexInCollection];
self.imgObj = nil;
}
In the first Level NSOperation this is where the exception breakpoint shows the crash happening:
- (void)didLoadDataFromFile:(NSData *)data
{
if (self.isCancelled) {
[self.opQueue cancelAllOperations];
[self completeOperation];
return;
}
if (!fileDownloadedFromWeb) {
self.observedObject.data = data; // CRASH
}
dataFromDisk = data;
fileReadDone = YES;
if (debugLog) {
NSLog(#"image loaded from local cache (%#)",self.sUrl);
}
}
Any suggestion on how to prevent this crash?
Thanks.
Edited to add:
what I am trying to achieve is: When a tableView cell is displayed a nsoperation is activated to get an image from the net. If the user scrolls quickly and the operation has not finished I need to end it and deallocate any data, and when the cell is reused start a new operation to get the appropriate image from the internet...
Based on comments below, we know that:
- (void)didLoadDataFromFile:(NSData *)data
is called on a different thread to dealloc, so there is a race condition. You need to access self.observedObject on the same thread as the thread it is deallocated on. I'm presuming "observedObject" is a weak reference?
dispatch_sync(dispatch_get_main_queue(), ^{
if (!fileDownloadedFromWeb) {
// Get a strong reference. This will retain observedObject - we must do this
// on the same thread as observedObject:dealloc is called, to prevent retaining
// an object during (or after) dealloc.
ObservedObject *strongRef = self.observedObject;
// This will do nothing if strongRef is nil.
strongRef.data = data;
}
});
A more structured approach would be to have the cell fetch all its images from a singleton cache (it looks as though at the moment there is no caching). The cell would obviously need to register itself as an observer for a particular URL in the cache, and the cache would notify the cell when the URL had downloaded. The cache should post that notification on the main thread.
The cache itself would manage all downloads, and there would be no background deallocation problem because it would be a singleton.
If you don't want to cache, that's fine. Use the same architecture, but call the cache an image fetcher instead. You can always add caching later if you want to.
EDIT - if your objects may be reused, rather than deallocated, as is the case for UITableViewCells, then the cell needs to be careful to ignore notifications about images that relate to a previous fetch. Either of these models will work
a) The cell retains a reference to the NSOperation until the NSOperation calls it back, or until prepareForReuse is called. Any callback from an unrecognised NSOperation must be a previous fetch (that we tried to cancel), and should be ignored. I don't really recommend this model, having the cell know about the operation AND vice versa seems silly.
b) The NSOperation sends a notification when it completes (on the main thread), and in the user info specifies the url/path that was requested. The UITableViewCell remembers what url/path it was trying to fetch, and ignores notifications that relate to other images. It unobserved that path in dealloc/prepareForReuse.
This was getting to long to be a comment so I'll make it an answer.
The reason why it's crashing has to do with the fact that UICollectionViewCells get recycled and deallocated. ARC is has put a [cvcell retain] in the wrong place. So, there are a few options:
One way to fix this is to just not create a NSOperation from a UICollectionViewCell.
Force the users to stay on the UICollectionViewController / UICollectionView so that it stays in memory.
Keep a property / pointer to the UICollectionViewController / UICollectionView so that it stays in memory even when the user has left it. (Make sure you retain it as strong or retain).
NOTE: All of these solutions do the same thing, force ARC to put the retain call somewhere else or to remove it entirely.
Cells get reused and reassigned frequently without your control so you should avoid assigning pending requests or operations to them.
Instead handle operations in your collection view data source (the view controller), and keep track of the operations not per cell but per indexPath's in a dictionary.
Even better keep this as a good experience and use something trusted and tested such as SDwebImage.
Please be gentle, this is my first ever post.
When I update a subclassed NSManagedObject and perform a save using a fetched results controller, instead of calling NSFetchedResultsChangeUpdate in "didChangeObject" it calls NSFetchedResultsChangeDelete. The save works and it updates the objects, but then immediately deletes it from the UITableView. If I quit and return, the object re-appears in the tableview with the updates so I know it is saving it to the DB.
I am developing code for a simple task app using XCODE 5. I am using an iPhone 4S and not the simulator to test it. It was working fine until I made some mods to another part of the code (I thought unconnected and now it doesn't work.
I have a sort order and when I update an object and it changed in the sort order, there was a nice animation for the moving of the UITableViewCells....now I have to force a fetch again and do [self.tableView reloadData]. This is a hack as I do not get any animations, but it is the only way I can get it to update:
I have a prepare for segue method:
- (void) prepareForSegue: (UIStoryboardSegue *) segue sender: (id) sender
{
// configure the destination view controller:
if ( [segue.destinationViewController isKindOfClass: [ShowEditTaskTableViewController class]])
{
if ([sender isKindOfClass:[UITableViewCell class]] )
{
NSIndexPath *indexPath = [self.taskTableView indexPathForCell:sender];
[self.taskTableView deselectRowAtIndexPath:indexPath animated:YES];
// Pass the selected task to the new view controller.
ShowEditTaskTableViewController *detailViewController = segue.destinationViewController;
Task *info = [self.fetchedResultsController objectAtIndexPath:indexPath];
detailViewController.editedObject = info;
}
else
{
NSIndexPath *indexPath = [self.taskTableView indexPathForCell:sender];
[self.taskTableView deselectRowAtIndexPath:indexPath animated:YES];
ShowEditTaskTableViewController *detailViewController = segue.destinationViewController;
Task *info = (Task *)[NSEntityDescription insertNewObjectForEntityForName:#"Task" inManagedObjectContext:self.managedObjectContext];
detailViewController.editedObject = info;
}
}
}
There is nothing special here. "Task" is my NSManagedObject.
I have a rewind (EDIT: changed reverse to rewind after comment) segue and before it is called, I set the variables which will be set stored in the editedObject.:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"addNewTaskSave"]) {
// Note: This is an unwind segue to go back to the previous screen.
self.dueDate = self.taskDatePicker.date;
self.description = self.taskDescriptionTextView.text;
self.priority = self.taskPrioritySegment.selectedSegmentIndex;
}
}
and in the RootViewController it calls:
- (IBAction)addNewTaskSave:(UIStoryboardSegue *)segue
{
ShowEditTaskTableViewController *controller = segue.sourceViewController;
controller.editedObject.shortDesc = controller.description;
controller.editedObject.priority = #(controller.priority);
controller.editedObject.dueDate = controller.dueDate;
controller.editedObject.completed = #0;
NSError *error;
if (![self.managedObjectContext save:&error]) {
...
}
.....
}
It uses the standard didChangeObject delegate methods. This worked fine until I clearly changed something.
Now after the save, it sets the NSFetchedResultsChangeDelete option and deletes the table row.
The fix is to add:
self.fetchedResultsController = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
...
}
[self.taskTableView reloadData];
However, this means that what is actually observed is just a straight update of the Table View with no animation.
Without this code, you can observe an animated delete of the table row. If I quit and restart, my edited object is there.
I have scoured SO which has been my constant companion for the last 4 weeks (the time I have been coding for IOS) and cannot find anything on this behaviour.
Any help would be greatly appreciated.
EDIT:
before the save, controller.editedObject isDeleted = NO, isUpdated = YES, so I can't see why it is setting NSFetchedResultsControllerChangeDelete.
I'm just going to discuss one piece of your code; and this may not have anything to do with the fetch results controller issue described.
Your so-called unwind/reverse segue has me stumped. I don't think prepareForSegue:sender: is relevant for an official unwind segue. So maybe you're not really using an official unwind segue (which is created by control-dragging from a button to the "exit" icon beneath a storyboard scene).
Unless you're referring to an unwind segue, there's no such thing as a "reverse" segue that takes you back to a previous screen. A push segue, for example, doesn't have a separate counterpart called a "pop" segue.
I suspect that you have segues crisscrossing between two scenes in storyboard. In other words, you created a segue from scene A to scene B, and you created a segue from scene B to scene A. If that's really the case, I would avoid doing that because it's unconventional. Maybe spend some time reviewing segues.
Ok, so I'll briefly address the fetched results controller issue. If there are user-driven changes to the data in a table view populated by a fetched results controller, then you will need to set a flag and temporarily "turn off" the fetched results controller delegate methods. This issue is mentioned briefly in the docs for NSFetchedResultsControllerDelegate protocol under "User-Driven Updates".
It appears my scouring of SO was not very good. see here https://stackoverflow.com/a/18998335/3482632
I added a UISegmentControl to filter and used the int value of its segment index.
int index = self.filterSegment.selectedSegmentIndex;
NSPredicate *predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:#"filter == %d", index]];
[fetchRequest setPredicate:predicate];
"filter" in my NSManagedObject subclass is an NSNumber. when I changed the NSPredicate to
NSPredicate *predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:#"filter == %#", #(index)]];
everything works fine.
It's this case again:
I wish to fill the prototype cells with the names of the friends selected in the UIPickerView over there. I have programatically filled the picker with the string representation of my Player object data, and set its properties using the delegate functions.
The "New Game Friends View" you see here has its own viewcontroller subclass, as has the table view, which I attempt to embed into a UIView on the "New Game Friends View". The table view IS an instance of my WHGFriendTableViewController class. I know this because this function does not throw any exceptions:
- (IBAction)addBtnClicked:(id)sender {
WHGFriendTableViewController* tabView = (WHGFriendTableViewController*) [[self childViewControllers] objectAtIndex:0];
NSInteger row = [friendPicker selectedRowInComponent:0];
[[tabView selectedFriends] addObject:[[self friendList] objectAtIndex:row]];
[[tabView tableView] reloadData];
}
Now the problem is: while the function above does not throw any exceptions, it still does not work. It appears that nothing really happens when I insert the objectAtIndex:row into the NSMutableArray selectedFriends (which is a property) in the table view's view controller.
This:
NSLog(#"New length: %d", [[tabView selectedFriends] count]);
prints 0 after inserting the new object. I have no idea why. Printing the count of [self friendList] gives three, just as I expect. The reloadData message does not make anything appear in the table view.
Any ideas why I cannot insert new data into the table view with my code, when this seems to be working with no exceptions whatsoever? Thanks in advance!
Have you alloced & init your NSMutableArray selectedFriends?
Also have you set the dataSource and delegate of your table view?
how can I add an object in my array each time I enter to my TableView
I put this code in viewDidLoad and viewDidAppear methods but it seems to doesn't work
:
- (void)viewDidLoad
{
[super viewDidLoad];
if (!myArray) {
myArray = [[NSMutableArray alloc] init];
}
[peopleListe insertObject:[NSDate date] atIndex:0];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
NSLog(#"%#",myArray);
}
when I put this code in a button it works
Thank you for your help
viewDidLoad is called once, when the view loads.
viewWillAppear is called every time you go into that view.
If you want to do something each time a view appears, put the code in viewWillAppear.
EDIT: It's possible that your array is getting dealloc'd. Try setting a breakpoint in dealloc as a simple way to see if that's the case:
- (void)dealloc {
NSLog(#"BYE!); // <-- put your breakpoint here
}
If it is, you'll have to (a) store your data somewhere else, or (b) keep this view/controller from being dealloced.
Also, who is your tableViewDelegate? That will have to implement methods returning the number of items in the table view and so on. I recommend having a read through the docs to get all those relationships sorted out.
You don't want to have that array as a property/ivar of your view controller. The view controller may, and will, get deallocated when it's not used (e.g. if it's inside the navigation controller, and you tap the "back" button to go to the previous screen.) When the view controller gets deallocated, your array obviously ceases to exist.
I suggest creating keeping that array in a separate place, e.g. in a singleton data object, or even (as a quick short-term solution) your app delegate.
About the code you posted: keep in mind that [UIViewController viewDidLoad] is only called once during the view controller's lifecycle. It may get called more than once, but that would mean that the original instance has been dealloc'd (and your original array is gone).