Delete button in UIViewController which is showing the detail of a cell - iphone

I have an iPhone app that has a tableview which contains cells that when touched show a detail of that object. I would like to add a Delete button to the bottom of the detail view. When the user presses it the object which is represented by the cell should be removed and the app should return to the TableView.
In terms of best practices, which is the ideal way to accomplish this?

There are few ways in which you can signal the deletion. One of them is delegates. You can define your delegate like this,
#protocol DetailViewControllerDelegate
- (void)shouldDeleteDetailObject:(id)object
#end
And then your table view controller subclass adopt the protocol and implement the method like,
- (void)shouldDeleteDetailObject:(id)object {
[self.objectsArray removeObject:object];
[self.navigationController popViewControllerAnimated:YES];
}
And then you message [self.tableView reloadData]; in viewWillAppear: as sandy has indicated.
Your button action will be implemented as,
- (IBAction)deleteObject {
if ( self.delegate && [self.delegate respondsToSelector:#selector(shouldDeleteDetailObject:)] ) {
[self.delegate shouldDeleteDetailObject:self.detailObject];
}
}
And delegate should be an assigned property.
You can also look at notifications but this is a better route for this situation.

I think there is nothing serious about this, if you successfully delete the particular details after that on backing on previous view (tableview) you just use this
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.tableView reloadData];
}

Related

How to enumerate view controllers on iPhone?

I have a modal view controller which fetches a password. Since I don't want the password written to disk if my application is interrupted, I want to cear the password on applicationWillResignActive. (For those who may comment, I know the secure text field does not properly zeroize).
I've tried the following code, and my view controller is never located. For the first set of code (UIView) I believe its because I'm mixing and matching views and view controllers. I'm not sure why the second set of code (UIViewController) is not working since a few folks have suggested it.
How does one enumerate view controllers and locate a controller of interest? I simply want to send clearPassworAndPin to PasswordPromptController if present (since viewWillDisappear is not always sent when the view disappears).
Modified 7KV7 and Jhaliya answer is below (it worked). The 'if' statement using viewController.modalViewController was executed 5 times (once for each controller in the tab view). So the single modal controller of interest was sent the clearPasswords message 5 times.
for (UIViewController * viewController in viewsControllers)
{
if ([viewController isKindOfClass:passwordPromptClass])
{
[(PasswordPromptController *)viewController clearPassworAndPin];
}
else
{
if(viewController.modalViewController)
[self clearPasswords:[NSArray arrayWithObjects:viewController.modalViewController, nil]];
}
}
Using UIViews (no joy)
- (void)applicationWillResignActive:(UIApplication *)application
{
if(application.windows != nil)
[self clearPasswords:application.windows];
}
- (void)clearPasswords:(NSArray *)subviews
{
Class passwordPromptClass = [PasswordPromptController class];
for (UIView * subview in subviews)
{
if ([subview isKindOfClass:passwordPromptClass])
[(PasswordPromptController *)subview clearPassworAndPin];
}
}
Using UIViewController (no joy)
- (void)applicationWillResignActive:(UIApplication *)application
{
if(tabBarController.viewControllers != nil)
[self clearPasswords:tabBarController.viewControllers];
}
- (void)clearPasswords:(NSArray *)viewsControllers
{
Class passwordPromptClass = [PasswordPromptController class];
for (UIViewController * viewController in viewsControllers)
{
if ([viewController isKindOfClass:passwordPromptClass])
[(PasswordPromptController *)viewController clearPassworAndPin];
}
}
NSArray *array = [self.navigationController viewControllers];
yourViewController = [array objectAtIndex:yourChoiceOfIndex];
Hope it helps.
At the point where you present the PasswordPromptController as a modalViewController could you not store it as an instance variable? Then, in your applicationWillResignActive: callback you will have a handle to the VC to message against.
Be sure to release and nullify your reference to the PasswordPromptController reference when it gets dismissed.
you will have to go through the navigation stack to get the controllers.
Use UINavgationController below method to get all viewController in your navigation stack.
#property(nonatomic, copy) NSArray *viewControllers
hmm..., I have to say I like to give alternative suggestions/solutions as many folks have tried to answer you question specifically.
If you found it's not easy to find the PasswordPromptController by enumerating view controllers, you can just declare (alloc/init) that controller in your app delegate, whenever you need to use it in other controllers, get it through app delegate, do something like presenting as a modal view.
When you want to do something against it in your app delegate, e.g. clear the pwd, it's super easy because you have the reference to it.

when is the right time to reloadData

I have a DetailsViewController class and an ItemsViewController class. (Both derived from UITableViewController)
Selecting any of the items in the ItemsViewController brings up the DetailsViewController. In order to get it to show the new data on any but the first one, I currently have
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[[self navigationItem] setTitle:title];
[[self tableView] reloadData];
}
This works, however it feels like killing a fly with a sledgehammer. What is a better way to do this?
Thanks in advance,
Alan
Combining ideas from several comments here:
Adding BOOL needReload as a member variable to the Details Controller.
Then in the details controller:
- (void)setData:(DataClass *)value {
if (value == data)
return;
id pointer = data;
data = [value retain];
[pointer release]; // release after retain
needReload = TRUE;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(needReload){
[[self navigationItem] setTitle:title];
[[self tableView] reloadData];
needReload = FALSE;
}
}
If you know that only specific rows or sections will be changing, you can direct the bake view to reload only those rows or sections. Other than that, -reloadData is the way to go for most table views.
I assume the items on the detail table changes depending on the selected item on the items table. So, yeah, this should be alright.
Other than that, you can check if the same item is selected the last time and not call reloadData during that case.
Alan,
Your statement of "In order to get it to show the new data on any but the first one" concerns me - because it tells me that you likely have a single DetailsViewController instance.
In your first table view, ItemsViewController, you probably have a didSelectRowAtIndexPath: method that you're using to push the DetailsViewController onto the UINavigationController stack.
How I solve this issue is simply creating/destroying a new DetailsViewController every time my user taps between views. So, my didSelectRowAtIndexPath: often looks like:
- (void) didSelectRowAtIndexPath:(NSIndexPath*)indexPath
{
NSInteger selectedRow = indexPath.row;
// Create a new view controller
DetailsViewController *tmpVC = [[DetailsViewController alloc] initWithNibName:#"foo" bundle:nil];
// Tell our new view controller what data it should be using
tmpVC.tableData = [self.someArrayOfData objectAtIndex:selectedRow];
// Push view controller and release it
[self.navigationController pushViewController:tmpVC animated:YES];
[tmpVC release];
}
This example assumes that you have all the data necessary for both view controllers in your ItemsViewController - that may not be the case..?
Anyway, by doing it this way, your DetailsViewController automatically loads the data. When you tap "Back" to go back to ItemsViewController, the UINavigationController would release it, destroying it. Then, when the user taps a different cell, we run this code again, creating a brand-new controller with brand-new data - so of course when it displays, it will load the data automatically - it's never displayed before.
What it sounds like you may be doing in your code is retaining the DetailsViewController as a property of the ItemsViewController class and then reusing the object. This can also work as well if you're concerned about allocations (for example, if it is a very "heavy" allocation to make a DetailsViewController), but then I think the best place to call reloadData is not inside the class itself - but rather from the didSelectRowAtIndexPath: method of ItemsViewController.
The reason I promote the creation/destruction approach as opposed to the "flyweight pattern" approach is that it keeps your code more separate - the fewer linkages between view controllers, the better. Of course, ItemsViewController will always dependo on and know about DetailsViewController, but it shouldn't necessarily have to be the other way around - and if you add the reloadData call to viewWillAppear:animated:, you're implicitly adding a non-code dependency between the two. You know that when ItemsViewController is the "parent" in the navigation stack, that's the right behavior -- but what if you suddenly started reusing that view in other part of your app that doesn't require a reload? It's a performance hit for one, and moreover, it's the kind of hidden dependency that may end up in a nasty-to-trace bug someday. So, I'd keep Details stupid and make Items contain all the complexity, if it is indeed required to only have 1 DetailsViewController (as opposed to my first idea of recreating it each time).
I would propose the reloadData and setTitle to be in the viewDidLoad and in the setter - I assume you set a property in DetailsViewController that changes the datasource of the table. So viewDidLoad reloads and sets the title, if the property has been set, the setter reloads and sets the title if isViewLoaded and the new value is different than the old one.
- (void)setSmth:(SmthClass *)value {
if (value == smth) // if they are the same and SmthClass is immutable,
// otherwise use isEqual and [self.tableView reloadData]
// before returning...
return;
id pointer = smth; // if it's a retain property
smth = [value retain];
[pointer release]; // release after retain just to be extra safe
if ([self isViewLoaded]) {
[self.tableView reloadData];
[self setTitle:title];
}
}
- (void)viewDidLoad {
if (smth) {
[self.tableView reloadData]; // maybe redundant...
[self setTitle:title];
}
}
Or you can use Key-Value observing (NSKeyValueObserving protocol) to observe your property and reloadData on notification...

updating uitableview from the custom cell after changing it

I've implemented edit menu from my custom UITableViewCell class. I have a small problem of updating a table view from inside the custom table cell class. What is the best approach to do that?
TIA
Clarification: By edit menu I meant a standard Cut/Copy/Paste... menu, that can complies with a standard UIResponder protocol. I want to cut/copy/paste cells content, which resides in some data structure (kind of folders/files tree). The cell actually only reflects the data.
The menu shows up on tap & hold on table cell. The table is derived from UITableViewController and created on fly (not from the xib). Cut/Copy actions are allowed for folders & files, while Paste action is allowed only for folders. Actually I need to refresh only the folder cell, which shows the number of items inside.
So in my CustomCell in paste selector I do the following:
- (void)paste:(id)sender {
... Perform a paste of data...
MyTableViewController *myTable = (MyTableViewController *) delegate;
[myTable performSelector:#selector(updateData) withObject:nil afterDelay:0.3];
}
In MyTableViewController:
- (void) updateData
{
[self.tableView reloadData];
}
The thing is that all cells except of the one that was changed are redrawn. I see it in cellForRowAtIndex function. Even if I add in paste selector [self setNeedsDisplay] it doesn't help.
Also, my custom cell overrides setHighlighted function:
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated {
if (delegate)
[delegate copyableTableViewCell:self willHighlight:highlighted];
[super setHighlighted:highlighted animated:animated];
}
so the delegate (MyTableViewController) shows an edit menu there.
And again the question is why the changed cell doesn't refresh?
Thanks
Do you want to update a single cell or the whole tableview? What about some kind of delegates, or selectors?
Resolved. Calling in MyTableViewController:
- (void) updateData
{
[self.tableView performSelector:#selector(reloadData) withObject:nil afterDelay:0.3];
}
and it does the work...

Update UIViewControllers after switching of the tabbar selection

I have two view controllers in a tabbar which can both edit data. Therefore, I need to call a reload_data function whenever the user makes a switch on the tabbar. How can I catch the switch or the appearance of the viewcontroller. Somehow viewDidAppear is not called on a tabbar switch. And I do not want to use the tabbarController delegate for this, because several viewControllers are affected (and I cannot set them all as delegate). What is a good way to solve this?
e.g. this didn't work:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:YES];
[self reloadData];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:YES];
[self reloadData];
}
If you're using Interface Builder, make sure the class for the viewController your expecting to reload is defined (Select the ViewController in IB, then CMD-4, make sure class is defined to be the class you want viewWillAppear and viewDidAppear to be called in).
If you're not using IB, post your code for init/calling the viewController.

How do I display a placeholder image when my UITableView has no data yet?

I have an application that, on load, displays a UITableView with the user's data in it.
However, when the user first loads the application (before they've created any data), I'd like to display, instead of an empty table, a background image (with an arrow pointing to the 'add a record' navbar button). Once the user has added their first record, the tableview is displayed instead. I've seen numerous apps do this - the only example I can think of at present is Cha-Ching, before you have any budgets/etc set up. I can't for the life of me work out how to do this, though.
I initially added a navigationcontroller to the main window's xib, the rootViewController of which was a custom viewcontroller/xib. This rootViewController contained the background image with a hidden tableview above it, and a custom tableviewcontroller that managed the tableview. This seemed to work just fine, and if there was data it would load and display in the table. However, if I was to scroll the data offscreen, the app would crash, with this error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:
'*** -[UITextEffectsWindow tableView:cellForRowAtIndexPath:]: unrecognized selector sent to instance 0xd2d130'
I have no clue what a UITextEffectsWindow is, or why it was trying to manage my tableview. I presume something may be hooked up incorrectly in my view hierarchy...
If there's a much simpler/more straightforward way of doing this, I'd be very grateful if someone could explain it. How would you do this?
Thanks in advance.
Here's one solution that I've been satisfied with so far.
First, I created a transparent view that was the same size as my TableView. I add this view as a sibling of the TableView whenever the TableView's data source has no data in it. I completely remove the view when data is available because transparency can affect the smoothness of the scrolling animation with TableViews.
I simply added a transparent label to this view that says something to the effect of "No Data Available". Adding a little shadowing to this label really helped to reinforce the concept that the label was 'floating' over top of the empty TableView.
I like your method of using an image though. Done properly, it might save you some work if you don't have to localize a string like I currently do.
To achieve this using a UITableViewController subclass as my only view (within a UINavigationController as per the Apple template) I used the following approach:
Create a UIView of the size of my tableView in the XIB that contains your UITableViewController and tableView.
Add an ImageView set with my placeholder image to the UIView.
Wire up the UIView as an IBOutlet (in the example code below, I called it emptyTableView)
When it is time to show the placeholder from within the UITableViewController subclass :
[self.tableView addSubView:emptyTableView];
[self.tableView setScrollEnabled:FALSE];
Disabling the scroll is necessary otherwise the user will be able to move the placeholder image up and down. Just remember to enable it once the user adds an item.
To remove the image view
[emptyTableView removeFromSuperview];
To do this, I use the following controller instead of UITableViewController. It will automatically place a view over the table when it is empty, and remove it when it is filled.
Just call [self reloadData] instead of [self.tableView reloadData] so that it can check if the table became empty.
In your subclass, implement a makeEmptyOverlayView function that will create the view to show over an empty table.
#interface MyTableViewController : UITableViewController
{
BOOL hasAppeared;
BOOL scrollWasEnabled;
UIView *emptyOverlay;
}
- (void) reloadData;
- (void) checkEmpty;
#end
#implementation MyTableViewController
- (void)viewWillAppear:(BOOL)animated
{
[self reloadData];
[super viewWillAppear: animated];
}
- (void)viewDidAppear:(BOOL)animated
{
hasAppeared = YES;
[super viewDidAppear: animated];
[self checkEmpty];
}
- (void)viewDidUnload
{
if (emptyOverlay)
{
self.tableView.scrollEnabled = scrollWasEnabled;
[emptyOverlay removeFromSuperview];
emptyOverlay = nil;
}
}
- (void) reloadData
{
[self.tableView reloadData];
if (hasAppeared &&
[self respondsToSelector: #selector(makeEmptyOverlayView)])
[self checkEmpty];
}
- (void) checkEmpty
{
BOOL isEmpty(YES);
id<UITableViewDataSource> src(self.tableView.dataSource);
NSInteger sections(1);
if ([src respondsToSelector: #selector(numberOfSectionsInTableView:)])
sections = [src numberOfSectionsInTableView: self.tableView];
for (int i(0); i<sections; ++i)
{
NSInteger rows([src tableView: self.tableView numberOfRowsInSection: i]);
if (rows)
isEmpty = NO;
}
if (!isEmpty != !emptyOverlay)
{
if (isEmpty)
{
scrollWasEnabled = self.tableView.scrollEnabled;
self.tableView.scrollEnabled = NO;
emptyOverlay = [self makeEmptyOverlayView];
[self.tableView addSubview: emptyOverlay];
[emptyOverlay release];
}
else
{
self.tableView.scrollEnabled = scrollWasEnabled;
[emptyOverlay removeFromSuperview];
emptyOverlay = nil;
}
}
else if (isEmpty)
{
// Make sure it is still above all siblings.
[emptyOverlay retain];
[emptyOverlay removeFromSuperview];
[self.tableView addSubview: emptyOverlay];
[emptyOverlay release];
}
}
#end
If you use Three20, you can easily set any image you want as a place holder prior to your table being populated.
So, to solve this I did as discussed in the comments above:
I created a normal UIViewController subclass, which contained a UIImageView and a UITableView. The viewController conforms to the UITableViewDelegate and UITableViewDatasource protocols, and looks after the tableView. The viewController class simply shows or hides the imageView depending on whether data is available.
I was going wrong before by trying to manage both these views with a UITableViewController. A UITableViewController has to have a tableView as its view, whereas, with this solution, a viewController can contain both the image and the tableView, and implement the necessary protocols to manage the tableView.
Thanks for all the help!