I have a UINavigationController inside a UITabBarController. The navigationcontroller has a UITableView and a form for editing items. The problem is that if a tab is tapped during editing, the form is just cleared and the user is dumped back to the UITableView.
Is there a way I can add a prompt to confirm navigation away from the edit view?
First, declare a BOOL in your .h to store the editing state. Also declare a temporary variable we will use later for storing the selected row.
BOOL isEditing;
NSUInteger selectedRow;
In your viewDidLoad, initialize the boolean to NO
- (void)viewDidLoad {
// initialization
isEditing = NO;
[super viewDidLoad];
}
You can then conform your view controller to UITextFieldDelegate and UIAlertViewDelegate. The text field delegate allows the controller to receive callbacks when editing ends and begins for the text fields and the alert view delegate allow it to receive callbacks when an alert view is dismissed.
#interface MyController : UIViewController <UITextFieldDelegate, UIAlertViewDelegate>
You then also need to set all the text field's delegates to be assigned to the controller. So in your cellForRowAtIndexPath when you add the text fields, just add this:
textField.delegate = self;
Once you have this, you are all set up to receive callbacks from the text field - so now implement the following two methods like so:
- (void)textFieldDidBeginEditing:(UITextField *)textField {
isEditing = YES;
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
isEditing = NO;
}
Now the key here is to make a separate method for pushing the next view, so just do something like this (like you would normally when the table view row is selected):
- (void)showNextView {
// in this method create the child view controller and push it
// like you would normally when a cell is selected
// to get the selected row, use the `selectedRow` variable
// we declared earlier.
}
You now need to implement the table view callback when the user selects a row - in this method we test if they are editing and show them a prompt if they are. If they aren't, we go to the next view.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
selectedRow = [indexPath row];
if (isEditing) {
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:#"Continue Editing?"
message:#"Continue Editing or discard edits"
delegate:self
cancelButtonTitle:#"Discard"
otherButtonTitles:#"Continue"];
[alert show];
[alert release];
return;
}
[self showNextView];
}
Finally, we need to implement the alert view delegate callback for when the alert view is dismissed:
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
if (buttonIndex != [alertView cancelButtonIndex]) return; // stay editing
[self showNextView];
}
Hope that all makes sense and is helpful to you!
Since you are using a UINavigationController, if you are pushing this "form" onto the stack you could set
#property(nonatomic) BOOL hidesBottomBarWhenPushed
That way the tab bar would be hidden until they are done with the form.
I solved this eventually by using a custom UIBarButtonItem which looks like a back arrow.
Related
iOS unfortunately doesn't have a dropdown picker like html does with the tag. I decided that I was finally going to create one for my app, and it looks and works great. My dropdown object is a subclass of UITextField. However, I changed something and now it only works some of the time.
User interaction is enabled, but I don't want the textfield to be editable. The class in which my dropdown subclass resides is UITextField delegate, and should receive delegate methods for UITextField.
I have - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField{ where I check to see if the textfield in question is a dropdown menu, and if it is, I call a method to instantiate a popover and disable editing, but the dropdown only appears on every other tap.
For example, i'll tap the "textfield" and my popover displays. I tap out so the popover goes away, then I tap on the "textfield" and nothing happens. I tap on the textfield once again and the popover appears. No idea why this is happening, here is what i'm doing:
.h
subclass : UIViewController<UITextFieldDelegate>
.m
dropdownTextField.delegate = self;
...
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField{
if(textField == self.measurementSelect){
NSLog(#"IM CALLED");
[self showPopover:textField];
return NO;
}
return YES;
}
-(void)showPopover:(id)sender{
if (_measurementPicker == nil) {
_measurementPicker = [[iPadMeasurementSelect alloc] initWithStyle:UITableViewStylePlain];
_measurementPicker.delegate = self;
}
if (_measurementPopover == nil) {
_measurementPopover = [[UIPopoverController alloc] initWithContentViewController:_measurementPicker];
[_measurementPopover presentPopoverFromRect:self.measurementSelect.frame inView:self.conversionView permittedArrowDirections:UIPopoverArrowDirectionLeft animated:YES];
}
else {
[_measurementPopover dismissPopoverAnimated:YES];
_measurementPopover = nil;
}
}
Every tap gets nslogged, so I assume my popover method is the culprit of this problem. Any ideas?
Let's rewrite by teasing apart existence of the UI elements and the visible state of the popover:
// canonical lazy getters for UI elements
- (iPadMeasurementSelect *)measurementPicker {
if (!_measurementPicker) {
_measurementPicker = [[iPadMeasurementSelect alloc] initWithStyle:UITableViewStylePlain];
_measurementPicker.delegate = self;
}
return _measurementPicker;
}
- (UIPopoverController *)measurementPopover {
if (!_measurementPopover) {
_measurementPopover = [[UIPopoverController alloc] initWithContentViewController:self.measurementPicker];
}
return _measurementPopover;
}
// now the show/hide method makes sense. it can take a bool about whether to show or hide
-(void)showPopover:(BOOL)show {
if (show) {
[self.measurementPopover presentPopoverFromRect:self.measurementSelect.frame inView:self.conversionView permittedArrowDirections:UIPopoverArrowDirectionLeft animated:YES];
} else {
[self.measurementPopover dismissPopoverAnimated:NO];
// if you want/need to create a new one each time it is shown, nil the popover here, like this:
// self.measurementPopover = nil;
}
}
When the textField begins editing, show the popover like this:
[self showPopover:YES];
And when the delegate gets the didEndEditing message:
[self showPopover:NO];
Are you able to edit images with QLPreviewController?
For example, preview an image and crop it? If not, what is the editing property on QLPreviewController for? It doesn't seem to change anything. (Previewing files is working fine)
Here is an example:
QLPreviewController *previewController=[[QLPreviewController alloc]init];
previewController.delegate=self;
previewController.dataSource=self;
[previewController setCurrentPreviewItemIndex:selectedIndex];
[previewController setEditing:YES animated:YES];
[self presentModalViewController:previewController animated:YES];
Delegate methods:
- (id <QLPreviewItem>)previewController:(QLPreviewController *)controller previewItemAtIndex:(NSInteger)index
{
file = [folder.files objectAtIndex:index];
controller.title = file.name;
CustomQLPreviewItem *customQLPreviewItem = [[CustomQLPreviewItem alloc] init];
customQLPreviewItem.previewItemURL = [NSURL fileURLWithPath:file.uri];
customQLPreviewItem.previewItemTitle = file.name;
return customQLPreviewItem;
}
- (NSInteger)numberOfPreviewItemsInPreviewController:(QLPreviewController *)controller
{
return folder.file.count;
}
QLPreviewController does not support editing in the way you are thinking.
the editing property is actually inherited from UIViewController and from here:
UIViewController Class Reference
editing A Boolean value indicating whether the view controller
currently allows the user to edit the view contents.
#property(nonatomic, getter=isEditing) BOOL editing Discussion If YES,
the view controller currently allows editing; otherwise, NO.
If the view is editable and the associated navigation controller
contains an edit-done button, then a Done button is displayed;
otherwise, an Edit button is displayed. Clicking either button toggles
the state of this property. Add an edit-done button by setting the
custom left or right view of the navigation item to the value returned
by the editButtonItem method. Set the editing property to the initial
state of your view. Use the setEditing:animated: method as an action
method to animate the transition of this state if the view is already
displayed.
Availability Available in iOS 2.0 and later. See Also –
setEditing:animated: – editButtonItem Related Sample Code BonjourWeb
iPhoneCoreDataRecipes Declared In UIViewController.h
I have a view controller with many views and a tableview.
The tableview's cells are customized, so there is another class for setting up the cells.
In each cell there is a button. The image of this button changes depending on the cell's content (this content gets read from a DB).
Basically, when the user presses the button, it changes itself to another image, a new status is written to the DB but the tableview does not update itself automatically.
The method for the button is in the custom cell class, so I've tried to instantiate my view controller (the one with the tableview) and execute a method for updating some labels in the views and the tableview:
ViewControllerWithTable *vc = [[ViewControllerWithTable alloc] init];
[vc updateEverything];
But this doesn't work.
The same "updateEverything" method, called from the same "ViewControllerWithTable" (adding a reload button) works perfectly.
Adding the "[tableView reloadData]" in the viewWillAppear method won't work because all the action is done in the same view.
What am I missing?
EDIT: adding some code to be more clear.
This is the method I use to update the tableview. It's inside the ViewController with the embedded tableview and it works when triggered by a button in one of the views:
- (void) updateEverything {
// lots of DB writing and reading, plus label text changing inside all the views
[tableView reloadData];
}
This is the IBAction for the button press and it's in the custom cell class:
-(void) btnPresaPressed:(UIButton *)sender {
AppDelegate *deleg = (AppDelegate *)[[UIApplication sharedApplication] delegate];
deleg.did = sender.tag;
NSString *s1 = NSLocalizedString(#"ALERT_TITLE", nil);
NSString *s2 = NSLocalizedString(#"ALERT_BODY", nil);
NSString *s3 = NSLocalizedString(#"YES", nil);
NSString *s4 = NSLocalizedString(#"NO", nil);
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:s1
message:s2
delegate:self
cancelButtonTitle:s4
otherButtonTitles:s3, nil];
[alertView setTag:1];
[alertView show];
}
This method shows an alert view that calls another method, always in the custom cell class:
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
AppDelegate *deleg = (AppDelegate *)[[UIApplication sharedApplication] delegate];
DbOperations *db = [[DbOperations alloc] init];
NSString *alrtTitle = [alertView buttonTitleAtIndex:buttonIndex];
NSString *s3 = NSLocalizedString(#"YES", nil);
NSString *s4 = NSLocalizedString(#"NO", nil);
switch (alertView.tag) {
case 1:
//
break;
case 2:
if ([alrtTitle isEqualToString:s3]) {
// DB writing and reading
ViewControllerWithTable *vc = [[ViewControllerWithTable alloc] init];
[vc updateEverything];
} else if ([alrtTitle isEqualToString:s4]){
//
}
break;
case 3:
if ([alrtTitle isEqualToString:s3]) {
//
}
break;
default:
break;
}
}
In this case, the updateEverything method don't work.
EDIT after you added more code:
In the following lines:
if ([alrtTitle isEqualToString:s3]) {
// DB writing and reading
ViewControllerWithTable *vc = [[ViewControllerWithTable alloc] init];
[vc updateEverything];
you are instantiating a new view controller altogether that has nothing to do with your original view controller that displayed the table view. So, you are sending the update message to the wrong object.
What you need is a mechanism for your cell to know which is the right controller to send the message to.
One easy solution would be using NSNotificationCenter:
the view controller register itself for a certain kind of notification:
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(updateEverything:)
name:kCellSentUpdateMessageNotification
object:nil];
your cell sends the notification, instead of calling the message directly:
[[NSNotificationCenter defaultCenter] postNotificationName:kCellSentUpdateMessageNotification object:nil];
OLD Answer:
You should call
[self.tableView reloadData]
from your updateEverything method implementation. This will reload the table data, effectively updating its rows appearance. The updateEverything method shall be called when tapping on the button in a row for this to work, obviously.
If that does not work, please provide more code.
Have you try to put
[[self tableView] reloadData];
I remember I had an issue like that, and this line on top solve my problem.
This is your problem:
The method for the button is in the custom cell class, so I've tried
to instantiate my view controller (the one with the tableview) and
execute a method for updating some labels in the views and the
tableview:
You created an entirely new view controller that has no knowledge of your tableview. The best thing to do is to create a property in your cell subclass and set it to your view controller when you set the cell. Then you can call your updateEverything method on that property.
I have a download running in background. It shows an UIAlertView under some fail condition.
When this alert happens, the application can be in any of the views it shows to the user, but only should be visible in one of them.
Can I delay the presentation of the UIAlertView to the moment the viewController it is associated with is displayed to the user (it's viewDidAppear method is invoked)?
Declare a property on the view controller that you want to show the view.
#interface DownloadViewController : UIViewController
{
UIAlertView *downloadAlertView;
}
#property (retain) UIAlertView *downloadAlertView;
#end
Then, when you detect the error, set the downloadAlertView property of the view controller (this will require you keeping a reference to this view controller by the object that is doing the downloading).
- (void)downloadFailed
{
UIAlertView *alertView = [[[UIAlertView alloc] init] autorelease];
alertView.title = #"Download Failed";
downloadViewController.downloadAlertView = alertView;
}
Then in your DownloadViewController implementation,
- (UIAlertView *)downloadAlertView
{
return downloadAlertView;
}
- (void)setDownloadAlertView:(UIAlertView *)aDownloadAlertView
{
// standard setter
[aDownloadAlertView retain];
[downloadAlertView release];
downloadAlertView = aDownloadAlertView;
// show the alert view if this view controller is currently visible
if (viewController.isViewLoaded && viewController.view.window)
{
[downloadAlertView show];
downloadAlertView = nil;
}
}
- (void)viewDidAppear
{
if (downloadAlertView)
{
[downloadAlertView show];
downloadAlertView = nil;
}
}
Quick explanation:
the first two methods are standard getter/setters, but the setter has added logic, so that if the view controller is currently visible, the alert is shown immediately.
if not, the alert view is stored by the view controller and shown as soon as the view appears.
This seems to be a white elephant for the iPhone. I've tried all sorts, even loops and I can't make it work.
I have a view that loads a table. However I've moved into object archiving and for development purposes I want to have an initial AlertView that asks if a user wants to use a saved database or download a fresh copy
(void)showDBAlert {
UIActionSheet *alertDialogOpen;
alertDialogOpen = [[UIActionSheet alloc] initWithTitle:#"DownloadDB?"
delegate:self
cancelButtonTitle:#"Use Saved"
destructiveButtonTitle:#"Download DB"
otherButtonTitles:nil];
[alertDialogOpen showInView:self.view];
}
I'm using an ActionSheet in this instance. And I have implemented the protocol:
#interface ClubTableViewController : UITableViewController <UITableViewDelegate, UITableViewDataSource, UIActionSheetDelegate>
Based on this I run this to check what button was pressed:
(void)actionSheet:(UIActionSheet *) actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
NSString *buttonTitle=[actionSheet buttonTitleAtIndex:buttonIndex];
if ( [buttonTitle isEqualToString:#"Download DB"] ) {
[self loadDataFromDB];
}
if ([buttonTitle isEqualToString:#"Use Saved"] ) {
self.rows = [NSKeyedUnarchiver unarchiveObjectWithFile:[self archivePath]];
if (self.rows == nil) {
[self loadDataFromDB];
}
}
}
The problem is my code to build the table executes before the user has made there choice. This causes all sorts of havok. As a result, how can I pause the code until a user has made there choice?
If you are using UITableView, you will not be able to "pause" the code, however this wouldn't be the method of choice anyways. A possible solution would be to load an empty table (return 0 in (UITableView *)tableView:numberOfRowsInSection) until the user has selected something, then reload the tableView's data with [tableView reloadData];
In your tableView:numberOfRowsInSection: just return zero until the user has made a decision.
Add [self.tableView reloadData]; at the end of the actionSheet delegate method. This will trigger all UITableViewDataSource methods again.