I am working through an iOS book and I've been extending an example program for practice when I stumbled into a issue I don't understand completely.
I'm currently working with a UINavigationController that has a rootView, a detailView and a third level view that I intend to turn into a modal view controller.
Everything is working as expected. My rootViewController is a UITableView that allows a user to select a row. The row will open a detailViewController for the given object that displays:
Item Title
Item Serial Number
Item Value
Purchase Date
On this DetailViewController there is a "Change Purchase Date" button that presents another ViewController to select a new date:
After Selecting a date and hitting the "Set Purchase Date" button I have the viewWillDisappear method save the changes to the Item Object in the third ViewController and then pop a view off the stack to return to the DetailViewController:
FILE: PurchaseDateViewController.m:
- (void) viewDidDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// Clear first responder
[[self view] endEditing:YES];
// "Save" the changes made to the creationDate
[_item setDateCreated:[purchaseDatePicker date]];
/* Debugging Log */
// Create a NSDateFormatter that will turn the date into a simple string
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateStyle:NSDateFormatterMediumStyle];
[dateFormatter setTimeStyle:NSDateFormatterNoStyle];
// NSLogging
NSLog(#"Original Date: %#", [dateFormatter stringFromDate:_itemsOldDate]);
NSLog(#"DatePicker Date: %#", [dateFormatter stringFromDate:[purchaseDatePicker date]]);
NSLog(#"Stored Date: %#", [dateFormatter stringFromDate:[_item dateCreated]]);
// Reload the DetailViewController to update the changes if the NavigationController is used to return to the DetailViewController
}
#pragma mark Action Methods to Update the Data Source
- (IBAction)setPurchaseDate:(id)sender {
// "Save" the changes made to the creationDate
[_item setDateCreated:[purchaseDatePicker date]];
// Move back 1 level on the NavigationController Stack
[[self navigationController] popViewControllerAnimated:YES];
}
- (void)cancelChange {
// "Reset" the changes made to the creationDate
[_item setDateCreated:_itemsOldDate];
// Move back 1 level on the NavigationController Stack
[[self navigationController] popViewControllerAnimated:YES];
}
All of the methods listed above work.
When using the "Set Purchase Date" button and PurchaseDateViewController is popped off the stack, the DetailViewController is updated immediately.
However, when I press the NavigationController back button, the view is NOT updated and the old date remains present on the DetailViewController. What's really weird is that the Item Object has been updated behind the scenes as my NSLog statements proved. This makes me believe that for some reason -viewWillAppear is not being called again.
This is confirmed if I hit the NavigationController's back button again and reselect the same row. When I take this testing approach, the date is updated to new date when re-entering the DetailViewController from the rootViewController. Since the DetailViewController is removed from the NavigationController stack and then instantiated again (therefore calling the DetailViewController's viewWillAppear again) the change is made.
Why isn't the viewWillLoad method getting called in this case?
How come the change is not present when using the UINavigationControls and is there a way to explicitly reload the view?
What is the recommend approach to overcoming this problem?
While writing this post I did find a way to force the viewWillAppear method to get called, but I feel there must be a more 'standardized' approach to overcoming this issue. What would the best practice be?
Here is the 'cheat' that I used to get around the problem:
PurchaseDateViewController.m
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
[viewController viewWillAppear:animated];
}
- (void) viewDidDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// Clear first responder
[[self view] endEditing:YES];
// "Save" the changes made to the creationDate
[_item setDateCreated:[purchaseDatePicker date]];
/* Debugging Log */
// Create a NSDateFormatter that will turn the date into a simple string
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateStyle:NSDateFormatterMediumStyle];
[dateFormatter setTimeStyle:NSDateFormatterNoStyle];
// NSLogging
NSLog(#"Original Date: %#", [dateFormatter stringFromDate:_itemsOldDate]);
NSLog(#"DatePicker Date: %#", [dateFormatter stringFromDate:[purchaseDatePicker date]]);
NSLog(#"Stored Date: %#", [dateFormatter stringFromDate:[_item dateCreated]]);
// Reload the DetailViewController to update the changes if the NavigationController is used to return to the DetailViewController
[self navigationController:[self navigationController] willShowViewController:_detailViewController animated:YES];
}
The will be giving you a problem:
- (void) viewDidDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
Should be
- (void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
Typo? Or does that fix things?
Related
This may look like a previously asked question(PageViewController delegate functions called twice) but the thing is I could not apply that solution to my problem.
As you might notice that I m developing a calendar application and Using UIPageViewController to manage my yearly calendar display. As you can see I'm using UIPageViewControllerTransitionStylePageCurl and when user curls the page(either forward/backward) their respective delegate is getting called twice which is giving me either 2 year increment or decrement based on the delegate that got executed. Now I need to find out what is causing this issue and stop it from getting executed twice.
I know it is important to return a viewController in those delegates which gives my next page or previous page thus I m just refreshing the viewController's view so that I can render the view with new data. I also tried another delegate called willTransitionToViewControllers but wont get me anywhere because willTransitionToViewControllers will get executed only after
viewControllerAfterViewController and viewControllerBeforeViewController.
Someone help me understand and solve this issue.
- (void)addCalendarViews
{
CGRect frame = CGRectMake(0., 0., self.view.frame.size.width, self.view.frame.size.height);
pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
options:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:UIPageViewControllerSpineLocationNone] forKey:UIPageViewControllerOptionSpineLocationKey]];
pageViewController.doubleSided = YES;
pageViewController.delegate = self;
pageViewController.dataSource = self;
YearCalendarViewController *yearController = [[YearCalendarViewController alloc] init];
NSArray *viewControllers = [NSArray arrayWithObject:yearController];
[self.pageViewController setViewControllers:viewControllers
direction:UIPageViewControllerNavigationDirectionReverse
animated:YES
completion:nil];
[self addChildViewController:pageViewController];
[self.view addSubview:pageViewController.view];
[pageViewController didMoveToParentViewController:self];
CGRect pageViewRect = yearController.view.bounds;
self.pageViewController.view.frame = pageViewRect;
viewCalendarMonth = [[SGMonthCalendarView alloc] initWithFrame:frame];
[self.view addSubview:viewCalendarMonth];
arrayCalendars = #[pageViewController.view, viewCalendarMonth];
}
-(UIViewController*)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
NSDate *currentDate = [[SGSharedDate sharedManager] currentDate];
NSDateComponents *currentDateComp = [NSDate returnDateComponentsForDate:currentDate];
self.nextDate = [NSDate dateWithYear:currentDateComp.year+1 month:currentDateComp.month day:currentDateComp.day];
[[SGSharedDate sharedManager] setCurrentDate:nextDate];
{
//I m reloading viewController's view here to display the new set of data for the increamented date.
}
NSLog(#"After Dates====>%#", nextDate);
return viewController;
}
-(UIViewController*)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
NSDate *currentDate = [[SGSharedDate sharedManager] currentDate];
NSDateComponents *currentDateComp = [NSDate returnDateComponentsForDate:currentDate];
nextDate = [NSDate dateWithYear:currentDateComp.year-1 month:currentDateComp.month day:currentDateComp.day];
[[SGSharedDate sharedManager] setCurrentDate:nextDate];
{
//I m reloading viewController's view here to display the new set of data for the decremented date.
}
NSLog(#"Before Dates====>%#", nextDate);
return viewController;
}
The UIPageController tends to cache ahead (and back) so when you swipe forward from view controller N , N+1 loads BUT so does N+2 silently so in the event you swipe twice your content is already loaded.
But since you have an invisible side-effect (incrementing date) in the delegate method you are getting hit by this.
My suggestion is to remove the dating side-effect from your delegate and bind the date code to the presented view controller somehow ( a delegate or a property ) then trap the didFinishAnimating method of the pageController.
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {
if (completed) {
if ([[self.pageViewController.viewControllers firstObject] isKindOfClass:[MyCalenderViewController class]]) {
currentDate = [self workOutCurrentDateForVC:[self.pageViewController.viewControllers firstObject]];
}
}
}
Side effects are a code smell. Try and avoid.
EDIT
I just noticed you are recycling the view controller and reloading it with the new date point. Don't do this . You need to treat a view controller as a single page of your book. So generate a new one and set it up for the month...
Briefly as an example...
#interface YearViewController : UIViewController
#property NSDate *focusedYear;
-(instancetype)initWithYear:(NSDate)adate;
#end
#implementation YearViewController
-(instancetype)initWithYear:(NSDate)adate {
self = [super initWithNibName:nil bundle:nil];
if(self) {
self.focusedYear = adate;
....
}
}
#end
So in the page controller delegate you just serve up a new "page" based on the date that the previous one had (and the direction you are paging)
-(UIViewController*)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
YearViewController *yvc = (YearViewController *)viewController;
NSDate *currentDate = yvc.focusedYear;
NSDateComponents *currentDateComp = [NSDate returnDateComponentsForDate:currentDate];
NSDate *nextDate = [NSDate dateWithYear:currentDateComp.year+1 month:currentDateComp.month day:currentDateComp.day];
YearViewController *freshYVC = [[YearViewController alloc] initWithDate:nextDate];
return freshYVC;
}
The comment about side effects still stands.
I have what appears to be a simple problem.
I have a table view controller (part of a 2 tabbed tab bar) which is populated by the user tapping the plus button in the navigation bar and filling in some information. That takes the user to an "Add Entry" view controller.
I have a second table view (second tab of the tab bar) which also has a plus button in the navigation bar which also calls the Add Entry. However, with this table view, I am already populating a textField and the datePicker to be related to the information it came from.
In the prepareForSegue, I'm setting the date and the text field. That works. However, I'm not entirely sure where to place the code in the Add Entry to say "If you're called from tab 1, leave everything blank and if you're called from tab 2, set the date picker".
In the prepareForSegue:
if ([segue.identifier isEqualToString:#"Create New Entry From Event"])
{
AddEntryViewController *addEntryViewController = (addEntryViewController *)segue.destinationViewController;
[addEntryViewController setSelectedEvent:self.occasion.title];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"MMMM d, yyyy"];
NSDate *dateFromString = [[NSDate alloc] init];
dateFromString = [dateFormatter dateFromString:sectionTitle];
[addEntryViewController setSelectedDate:dateFromString];
}
The setSelectedDate is:
- (void)setSelectedDate:(NSDate *)selectedDate
{
_selectedDate = selectedDate;
}
If I set the viewWillAppear to:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.occasionTextField.text = self.selectedEvent;
[self.datePicker setDate:self.selectedDate animated:YES];
}
I get a crash when calling the Add Entry from any other screen but this one which of course isn't desirable.
So I need a way to leave all text fields and the date picker as blank when called from anywhere in the app (which works without the self.datePicker line) and to only SET the datePicker when being called from THAT particular table view.
Any thoughts on this would be really great!
in interface define a BOOL like this.
#property (assign, nonatomic) BOOL fromTabOne;
and add
#synthesize fromTabOne;
in viewWillAppear
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (!self.fromTabOne) {
self.occasionTextField.text = self.selectedEvent;
[self.datePicker setDate:self.selectedDate animated:YES];
}
}
in prepareForSegue
if ([segue.identifier isEqualToString:#"Create New Entry From Event"])
{
AddEntryViewController *addEntryViewController = (addEntryViewController *)segue.destinationViewController;
[addEntryViewController setSelectedEvent:self.occasion.title];
if (viewOne) { //if you're on first tab
[addEntryViewController setFromTabOne:YES];
} else {
[addEntryViewController setFromTabOne:NO];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"MMMM d, yyyy"];
NSDate *dateFromString = [[NSDate alloc] init];
dateFromString = [dateFormatter dateFromString:sectionTitle];
[addEntryViewController setSelectedDate:dateFromString];
}
}
The error you mentioned in comment probably caused by a nil NSDate or wrong locale settings. Make a nil check before setting. I guess the NSDateFormatter couldn't format your string.
I'm following along with the Big Nerd Ranch ios book, chapter 11. It uses a table view in the ItemsViewController to display a list of items and if you click on an item it transfers control to a DetailViewController that shows the details about the individual item. The DetailViewController.xib has outlets for these fields declared in the header file DetailViewController.h
__weak IBOutlet UITextField *nameField;
__weak IBOutlet UITextField *serialNumberField;
__weak IBOutlet UITextField *valueField;
__weak IBOutlet UILabel *dateLabel;
The application worked fine up to a point. If i clicked on an item, the new view would open but the fields were empty. Therefore, the tutorial introduced us to a way to have the fields populated with the values of each item. After introducing this new code to populate the fields, the application crashes with the following errors once I click on an item to view its details
2014-02-12 07:18:30.720 Homepwner[3180:a0b] -[UIView setText:]: unrecognized selector sent to instance 0x89ef8c0
2014-02-12 07:18:30.724 Homepwner[3180:a0b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIView setText:]: unrecognized selector sent to instance 0x89ef8c0'
Based on the code excerpts below can you explain why the application might be crashing when I click on one of the items in the list to open it in the detail view?
This is the code we are given to populate the fields
DetailViewController.m
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[nameField setText:[item itemName]];
[serialNumberField setText:[item serialNumber]];
[valueField setText:[NSString stringWithFormat:#"%d", [item valueInDollars]]];
// Create a NSDateFormatter that will turn a date into a simple date string
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateStyle:NSDateFormatterMediumStyle];
[dateFormatter setTimeStyle:NSDateFormatterNoStyle];
// Use filtered NSDate object to set dateLabel contents
[dateLabel setText:[dateFormatter stringFromDate:[item dateCreated]]];
// Change the navigation item to display name of item
[[self navigationItem] setTitle:[item itemName]];
}
In ItemViewController.m, we added the middle three lines in this function so that the DetailViewController has its item before viewWillAppear: gets called
- (void)tableView:(UITableView *)aTableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
DetailViewController *detailViewController = [[DetailViewController alloc] init];
NSArray *items = [[BNRItemStore sharedStore] allItems];
BNRItem *selectedItem = [ items objectAtIndex: [indexPath row]];
[detailViewController setItem:selectedItem];
[[self navigationController] pushViewController:detailViewController animated:YES];
}
Since the nameField property is initialized through a NIB/Storyboard, the most likely reason for the problem that you are seeing is an incorrect connection done in the Interface Builder. It appears that the nameField outlet is connected to the parent of UITextEdit, as opposed to the UITextEdit itself.
Removing and re-creating the nameField property should fix the problem. First, remove the property from the code. Then, control-drag from the text field into the DetailViewController's class extension inside DetailViewController.m, and name the property nameField.
I'm automating date selection on UIDatePicker using KIF. I added the accessibility label and set the target for the picker if the date changes.
Ref: http://bit.ly/140ICwo
+(id) changeDate: (NSDate *) myDate
{
[s addStep:[KIFTestStep stepToEnterDate:myDate ToDatePickerWithAccessibilityLabel:#"datePicker"]];
[self wait:s timeInSeconds:3];
[s addStep:[KIFTestStep stepToTapViewWithAccessibilityLabel:#"Done" traits:UIAccessibilityTraitButton]];
return s;
}
- (void) ViewDidLoad
{
...
datePicker.maximumDate = lastAvailableDate;
datePicker.date = (dateValue ? dateValue : [NSDate date]);
[datePicker addTarget:self action:#selector(dateChangedAction:)
forControlEvents:UIControlEventValueChanged];
self.datePicker.accessibilityLabel = #"datePicker";
self.footerLabel.accessibilityLabel = #"datelabel";
}
- (IBAction)dateChangedAction:(id)sender
{
[dateValue release];
dateValue = [datePicker.date retain];
dateCell.detailTextLabel.text = [[[self class] sharedFormatter] stringFromDate:dateValue];
[self setDateTitleText:[[[self class] sharedFormatter] stringFromDate:dateValue]];
}
The Picker rotates and stops at the given date however the "dateChangedAction" function is not getting called, hence the label which displays the selected date is not getting updated.
If I run the app with out KIF everything works fine. Also I tried to manually select a date when running KIF to check it it updates the label but it seems like the UI gets frozen and I cannot click any UI controls.
Looks like the problem is related to this posting
http://bit.ly/10xtbqU
Any help is very much appreciated.
Thanks
I run into the same problem you're just missing
[picker sendActionsForControlEvents:UIControlEventValueChanged];
to trigger the dateChangedAction callback,
in other words try this:
+ (id)stepToEnterDate:(NSDate*)date ToDatePickerWithAccessibilityLabel:(NSString*)label
{
NSString *description=[NSString stringWithFormat:#"Enter date to Date picker with accessibility label '%#'",[date description]];
return [self stepWithDescription:description executionBlock:^(KIFTestStep *step, NSError **error)
{
UIAccessibilityElement *element = [[UIApplication sharedApplication] accessibilityElementWithLabel:label];
KIFTestCondition(element, error, #"View with label %# not found", label);
if(!element)
{
return KIFTestStepResultWait;
}
UIDatePicker *picker = (UIDatePicker*)[UIAccessibilityElement viewContainingAccessibilityElement:element];
KIFTestCondition([picker isKindOfClass:[UIDatePicker class]], error, #"Specified view is not a picker");
[picker setDate:date animated:YES];
// trigger the UIControlEventValueChanged in case of event listener
[picker sendActionsForControlEvents:UIControlEventValueChanged];
return KIFTestStepResultSuccess;
}];
}
I have a table view controller with (among others) a cell that is to represent a date. I followed this post "How do I make a modal date picker that only covers half the screen?" and I have it working with one big exception - I can't get the picker to disappear!
I tried registering for the event UIControlEventTouchUpOutside, but it seems that this is not generated by the picker (or at least in the mode that I am using it).
How can I recognize that the user has finished selecting a date?
Also, is there a way to disable the user from inputting directly into the UITextField? I want to force them to use the picker. I saw this post "Disable blinking cursor in UITextField?", but is there another way?
Reagards,
--John
Try this code.. here I am putting an datepicker to a uitextfield.. it will have a done button at the top right navigation bar.. so by clicking done I will user can dismiss the datepicker.. the another best method is by putting a toolbar above the datepicker having the done button.. Try this it will work.. when changing the datepicker you can populate the text field.. Hope this helps..
see this stackoverflow link https://stackoverflow.com/a/4824319/763747 this will have the datepicker with done button as toolbar above the keybord..
#pragma mark -
#pragma mark - TextField Delegate
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[textField resignFirstResponder];
return TRUE;
}
- (void) textFieldDidBeginEditing:(UITextField *)textField {
itsRightBarButton.title = #"Done";
itsRightBarButton.style = UIBarButtonItemStyleDone;
itsRightBarButton.target = self;
itsRightBarButton.action = #selector(doneAction:);
if ([textField isEqual:itsIncidentDateTextField])
{
itsDatePicker = [[[UIDatePicker alloc] init] autorelease];
itsDatePicker.datePickerMode = UIDatePickerModeDate;
[itsDatePicker addTarget:self action:#selector(incidentDateValueChanged:) forControlEvents:UIControlEventValueChanged];
//datePicker.tag = indexPath.row;
textField.inputView = itsDatePicker;
}
}
- (IBAction) incidentDateValueChanged:(id)sender{
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:#"MMM d, yyyy"];
itsIncidentDateTextField.text = [dateFormatter stringFromDate:[itsDatePicker date]];
[dateFormatter release];
}
I used a different method for disappearing the UIDatePicker.
create a XIB (nib) file and add to it a UIDatePicker then resize the view so it fits only the UIDatePicker, create a property (make it strong and nonatomic) in your ViewController (or whatever class your using, and synthesize of course).
#property (nonatomic, strong) UIView *myDatePickerView;
#synthesize myDatePickerView;
then create a loadDatePickerView method
- (void) loadDatePickerView
{
UINib *nib = [UINib nibWithNibName:kNIBname bundle:[NSBundle mainBundle]];
NSArray *views = [nib instantiateWithOwner:self options:nil];
self.myDatePickerView = [views firstObject];
[myDatePickerView setFrame:CGRectMake(0, 318, 320, 162)];
[self.view addSubview:myDatePickerView];
}
implement the UITextFieldDelegate, and in the textFieldDidBeginEditing method call the loadDatePickerView method,
[self loadDatePickerView];
to make function create a property which is a UIDatePicker instance
#property (nonatomic,strong)
UIDatePicker *myDatePicker;
(synthesize of course)
now create an IBAction like so:
-(IBAction)datePickerValueChanged:(UIDatePicker *)sender
{
myDatePicker = [[myDatePickerView subviews] lastObject];
//now you can do whatever you want with the DatePicker
}
now connect the IBAction to the picker in the XIB file, that way the XIB is now the UIDatePicker instance you created in the VC, if you want it to disappear you can add a UITapGestureRecognizer (in the ViewDidLoad) and the selector will be another IBAction which removes myDatePickerView from its' superView like this:
- (void)viewDidLoad
{
[super viewDidLoad];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(dropPicker:)];
[self.view addGestureRecognizer:tap];
[self datePickerValueChanged:myDatePicker];
}
-(IBAction)dropPicker:(id)sender
{
[myDatePickerView removeFromSuperview];
}