Best practice for KVO observing model changes in UITableView - ios

Let's imagine a basic iPhone app with a table view to show a list of people and a details view to change the name of a person embedded in a navigation controller.
I'm using KVO to get notified in my table view controller that the name of a person was changed down in the details controller.
My question is when/where to add and remove my table view controller as observer for the name of each person object.
My approach:
#implementation PeopleTableViewController
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
Person *person = ...; // person for index path
[person addObserver:self forKeyPath:#"name" options:0 context:(__bridge void *)(PERSON_NAME_CTX)];
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
Person *person = ...; // person for index path
[person removeObserver:self forKeyPath:#"name"];
// This is not called when the view is removed from the hierarchy
// Can't use viewDidDisappear: because we are using a navigation controller
// and tableView:willDisplayCell: is not called when we return from the details controller
}
- dealloc {
// See comment in didEndDisplayingCell:
for (UITableViewCell *cell in self.tableView.visibleCells) {
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
Person *person = ...; // person for index path
[person removeObserver:self forKeyPath:#"name"];
}
}
Due to the navigation controller things are a bit tricky, because tableView: didEndDisplayingCell is not called when the view is removed from the view hierarchy. I can't remove the observer in viewWillDisappear:, because when the user returns from the details controller I still need to observe the person objects for changes.
Removing the observer in dealloc seems to work. My question: is this the right way to do it?

Usually you should call addObserver/removeObserver on viewWillAppear/viewWillDisappear methods respectively, because dealloc method is not balanced with this calls (I mean can be called few times than the methods above). Maybe one of the best solutions is use a NSFetchedResultsController in order to track any change to the data source.

Related

Model object not being released after removing observer

My UIViewController has a UITableView. Each custom cell is given a model object with weak association.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
List *list = self.lists[indexPath.row];
ListCoverTableViewCell *cell = (ListCoverTableViewCell *)[tableView dequeueReusableCellWithIdentifier:NormalCell forIndexPath:indexPath];
cell.list = list;
return cell;
}
Each cell then observes a property on the model object.
- (void)addProgressObserverToCell:(ListCoverTableViewCell *)cell
{
#try {
[cell.list.tasksManager addObserver:cell
forKeyPath:NSStringFromSelector(#selector(fractionCompleted))
options:0 context:nil];
} #catch (NSException *__unused exception) {}
}
addProgressObserverToCell: is called from viewWillAppear (in case the user taps on a cell and comes back), and in tableView's willDisplayCell: (for when the user scrolls).
A similar method removeProgressObserverFromCell gets called in viewWillDisappear (for when the user taps a cell and navigates away) and in tableView's didEndDisplayingCell (for when the user scrolls).
- (void)removeProgressObserverFromCell:(ListCoverTableViewCell *)cell
{
#try {
[cell.list.tasksManager removeObserver:cell
forKeyPath:NSStringFromSelector(#selector(fractionCompleted))
context:nil];
} #catch (NSException *__unused exception) {}
}
So far, everything is balanced. I add observers in viewWillAppear/willDisplayCell, and remove them in viewWillDisappear/didEndDisplayingCell.
To be safe (and defensive), I updated my ViewController's dealloc method to also remove all observers. I simply loop through the tableView's visible cells and call removeProgressObserverFromCell:.
By running this code in the dealloc, I'm finding the model objects stored within my UITableView's visibleCells are never released. How is my defensive removal of observers causing my model object to be retained?

How to Delete Row from Table View from other ViewController

So I have a DetailViewController which displays the details of row/cell from table view. Now I would like to add an option of DELETE on this controller. I added a Bar Button Item(trash) on it. How will I be able to delete the current row/data and remove it also from the TableViewController?
TableViewController
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
WishlistItem *wish = [self.wishlistItem objectAtIndex:indexPath.row];
DetailViewController *dvc = [self.storyboard instantiateViewControllerWithIdentifier:#"dvcID"];
dvc.wishItemStr = wish.wishlistItem;
dvc.dateItemStr = wish.targetDate;
dvc.descItemStr = wish.descWishItem;
[self.navigationController pushViewController:dvc animated:YES];
}
When you create the detailed view controller you must be initializing it with some data relevant to the row. So you can extent that initializer method (or add a new method if you like) which takes a block.
When the table view controller creates and loads the detailed view controller it initializes it with some block code that will delete the relevant row.
Example: (note I haven't compiled this).
Add this to the DetailViewController:
#property (copy, nonatomic) void (^deleteRowBlock)(void);
- (void) onDeletion:(void (^)(void)) deletionBlock;
The implementation of onDeletion is
- (void) onDeletion:(void (^)(void)) deletionBlock
{
self.deleteRowBlock = deletionBlock;
}
When the button is pressed in the DetailViewController call the block like this:
self.deleteRowBlock();
Then in didSelectRowAtIndexPath: add this:
[dvc onDeletion:^{
code to delete the row and update your data model
}];
Then when the button is pressed the "code to delete the row and update your data model" will get executed.
Or alternatively if you don't like blocks (but you should learn to like them) define a protocol with a method such as onDelete:(NSIndexPath*) row. The table view is a delegate of the detailed view and implements the protocol method, which you invoke when the button is pressed.
The detailed view would need to know its row number. Alternatively remove the NSIndexPath as the parameter to onDelete and have the tableView cache the row number of the currently presented detail view controller and when onDelete is called it deletes the row for the cached row number.
But it is preferable to use blocks
well, this is another solution,
1) pass self.wishListItem to DetailViewController, here is example
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
DetailViewController *dvc = [self.storyboard instantiateViewControllerWithIdentifier:#"dvcID"];
...
[dvc setWishListItem:self.wishListItem];
...
[self.navigationController pushViewController:dvc animated:YES];
}
2) in TableViewController implement viewWillAppear method like this
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.tableView reloadData];
}
3) in DetailViewController you have a delete button, right ? here is your button's action method
- (void)onDelete {
// your deleting stuff ...
[self.wishListItem removeObjectAtIndex:self.currentItemIndex]; // this line updates shared data
}
So TableViewController reloads it's data and keeps cells up to date once you get back (by touching back button, for example)
if you still have questions feel free to comment.

UITableView reloadData not working on MainTable in Split View Controller

I have a feature in my app where the user can change the color scheme of the app. The app uses a Split View Controller, with a MainTable and DetailView table. Everything works fine except for the MainTable. What is failing is that the MainTable reloadData method is not causing the cells to be redrawn.
It should be noted that I am changing globalHighContrast and sending the notification from a UIModalPresentationFormSheet viewController, so the tables are kind of visible on the screen while the viewController is active.
I am triggering the screen update from a notification, like this:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(reloadAllTables)
name:#"contrastModeChanged"
object:nil];
Then, to make sure that I call reloadData on the main thread, I am handling the notification like this:
-(void)reloadAllTables{
[self performSelectorOnMainThread:#selector(doReloadAllTables) withObject:nil waitUntilDone:NO];
}
-(void)doReloadAllTables{
[self showIcon];
if( globalHighContrast ){
theTable.backgroundColor = [Colors lightBkgColor];
self.view.backgroundColor = [Colors lightBkgColor];
} else {
theTable.backgroundColor = [Colors darkBkgColor];
self.view.backgroundColor = [Colors darkBkgColor];
}
[detailViewController configureView:currentMainMenu];
[detailViewController.subTable reloadData];
[theTable reloadData];
// desperate try to force it to work
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:currentMainMenu inSection:0];
[self tableView:theTable didSelectRowAtIndexPath:indexPath];
}
Both reloadAllTables and doReloadAllTables are being called, but
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
is not being called.
As soon as I tap a cell on the MainTable it does update correctly to the new color scheme.
Also, there is a desperate attempt to workaround this by trying to simulate the MainTable touch, but that doesn't work either.
You can try to put code for updating you scheme in -(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath method...

How to call tableView method directly?

I have a method constructed like this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//some table related stuff
}
However I cannot call this, so I basically copied and pasted the whole function and renamed as:
- (void)jumpCountry: (NSIndexPath *)indexPath {
//some table related stuff
}
and calling this method by using:
[self jumpCountry:countryIndex];
However my class looks ugly (and not preferred) because it has got the same two methods. So, how can I call the initial method directly(I know that it is assigned to a button which invokes that method). I am using iOS6.1. Basically, the reason why I want to directly call is I have another thread that listens notifications(from location services), once a notification is received, the table view should be changed. The notification itself already searches for NSIndexPath, so there won't be any problem with that.
You can just put
[self jumpCountry:countryIndex];
to your method:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
This method a delegate method of table view from UITableViewDelegate protocol and it gets call when user select a row in UITableView. you should not call this method by your self.
instead of you can create your method and do whatever you want to do and call it in viewDidLoad or any method.
To call programatically use
[tabelView selectRowAtIndexPath:scrollIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
Here scrollIndexPath is the indexpath of row to be selected
For creating indexpath
NSIndexPath *scrollIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];

UITableView reloadData doesn't work

I've read all similar questions and tried all suggestions, still nothing. Maybe someone can spot my flaw.
My view controller is initiated from another view controller, by one of two buttons. Button taps send NSNotification (with attached arrays), and this view controller anticipates this notification and then calls this method:
- (void)addContentToArray:(NSNotification *)aNotification {
array = [NSArray arrayWithArray:[aNotification object]];
([array count] == 6) ? (category = YES) : (category = NO);
[myTableView reloadData];
NSLog(#"%d", [array count]);
NSLog(#"%#", myTableView);
}
The method gets called every time, I can see that from changing array count. Here notification object is the array passed from previous view controller, and I assign these objects to my local array property - this is my UITableView source. So what I do is I try to reuse the UITableView to display elements of whatever array is being passed. And it works nicely for the first array passed (whichever first).
When I tap the second button, the new array is passed successfully (as mentioned before, I know that from log of [array count] which is different: 3 vs 6 objects in different arrays). However, what is not happening is that UITableView does not refresh (although the values passed when I select a row in the table are from the correct arrays, even though wrong values are displayed).
Here are UITableView data source methods:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [array count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Identifier"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"Identifier"];
}
cell.textLabel.text = [[array objectAtIndex:indexPath.row] objectForKey:#"name"];
if (category) {
cell.detailTextLabel.text = [[array objectAtIndex:indexPath.row] objectForKey:#"description"];
}
return cell;
}
So, what am I doing wrong?
A few other considerations that might help:
NSLog(#"%#", myTableView); returns (null), which is a bit worrying. myTableView here is UITableView from my nib file, which is correctly connected to the view controller, declared as property and synthesized
The view controller in question is a rightViewController of the PKRevealController, so when it is called repeatedly, viewWillAppear method is called, but not viewDidLoad (although, as I already mentioned, addContentToArray: method is being called every time as well)
Also, for those somewhat familiar with PKRevealController - when I try and log focusedController from my view controller, it says that frontViewController - the one that moves to reveal my view controller - is the one that is focused. Can that be the reason why myTableView is (null)?
I'd be grateful for any insight and help!
Need more code, that part where you created and call Myviewcontroller's addContentToArray method.
I think you used release code there for Myviewcontroller's object, try once with hide that part.
I managed to solve the issue by editing initWithNibName method (old line commented out)
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
//self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
self = [super initWithNibName:#"PurposeCategoryViewController" bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
Apparently, it did have something to do with the fact that my view controller (called PurposeCategoryViewController) was not the top/focused view controller in PKRevealController hierarchy. So, I just needed to specifically indicate my nib file.
What value are you returning in this delegate method:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
make sure it is 1

Resources