iOS8 Weirdness Part2: Modal ViewController

As i promised we are continuing werid iOS8 bugs i encounter after update. Last one was funny but this one is a masterpiece. Imagine you have project where your user can type some values eg. add contact, some place or something. We want to help our user by giving him some hints while typing or some predefinied values he can choose from. It is a good idea to present this kind of view as a modal view controller…

Imagine my reaction to my stable iOS7 code when my QA guy come to me and said something like this:

“Why all our modal view controllers can close by themselves, i mean look if you dismiss popover on that modal view it will close.”

Sounds quiet funny right? How the hell modally showed UIViewController can close just like that without pressing anything. But after few hours of debbuging i was not so happy it actually happend to me! All the time i was dimissing UIPopoverController the modal view was dimissing with this popover! But there is a trick behing this you need to set your view stack like this:

UINavigationViewController that holds UIViewController and from here you can open modal UIViewController that contains some containers. In some big project im participating we have some reusable ViewControllers that doing specific things one of it is UIViewController with UITextField in it. If you press on that UITextField a popover will show to help you choose / find some value. So this is how our app looks / works:

Pretty simple, so the app opens some modal view and contains this funny component. Ofc this code works fine on iOS7, now if we run this on iOS8 we will see somethings like this:

So wherever user tap outside to dismiss UIPopoverController (i created it from code to avoid latest issues) modal controller also dimiss… It took me some cups of coffet to recognize what is wrong but before that let’s see some code that is responsible for this. I will not describe whole example code just the green UIViewController that is placed using container segue.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#import "MWLContainerViewController.h"
#import "MWLCitiesTableViewController.h"

@interface MWLContainerViewController ()
<UITextFieldDelegate,
UIPopoverControllerDelegate,
MWLCitiesTableViewControllerDelegate>

@property (nonatomic, weak) IBOutlet UITextField *typeCityTextField;
@property (nonatomic, strong) UIPopoverController *searchPopoverController;
@property (assign, nonatomic, getter = isKeyboardVisible) BOOL keyboardVisible;
@property (nonatomic, weak) MWLCitiesTableViewController *selectionController;

@end

@implementation MWLContainerViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.typeCityTextField.delegate = self;
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardDidShow:)
                                                 name:UIKeyboardDidShowNotification
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardDidHide:)
                                                 name:UIKeyboardDidHideNotification
                                               object:nil];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];

    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

-(void)keyboardDidShow:(NSNotification *)notification
{
    self.keyboardVisible = YES;

    if(self.typeCityTextField.editing) {

        [self showControllerIfNeeded:self.typeCityTextField];
    }
}

-(void)keyboardDidHide:(NSNotification *)notification
{
    self.keyboardVisible = NO;
}

- (void)showControllerIfNeeded:(UIView *)sender
{
    if(self.searchPopoverController.popoverVisible == NO && self.keyboardVisible == YES) {

        self.searchPopoverController = [self itemSelectionPopover];
        [self.searchPopoverController presentPopoverFromRect:sender.bounds
                                                      inView:sender
                                    permittedArrowDirections:UIPopoverArrowDirectionDown
                                                    animated:YES];
    }
}

- (UIPopoverController *)itemSelectionPopover
{
    MWLCitiesTableViewController *cityController = [self.storyboard instantiateViewControllerWithIdentifier:kMWLSegueControllerIdentifier];
    self.selectionController = cityController;
    self.selectionController.delegate = self;

    UIPopoverController *popoverController = [[UIPopoverController alloc] initWithContentViewController:cityController];

    popoverController.delegate = self;
    popoverController.popoverContentSize = CGSizeMake(300, 400);

    return popoverController;
}

#pragma mark - UITextFieldDelegate

- (void)textFieldDidBeginEditing:(UITextField *)textField
{
    self.typeCityTextField.clearsOnInsertion = YES;

    [self showControllerIfNeeded:textField];
}

- (void)textFieldDidEndEditing:(UITextField *)textField
{
    [self.searchPopoverController dismissPopoverAnimated:YES];
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField resignFirstResponder];

    return YES;
}

#pragma mark - UIPopoverControllerDelegate

- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController
{
    //delegate down
}

- (BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
    [self.typeCityTextField resignFirstResponder];

    return YES;
}

#pragma mark - MWLCitiesTableViewControllerDelegate

- (void)citiesTableView:(MWLCitiesTableViewController *)sender
          didSelectCity:(NSString *)cityName
{
    self.typeCityTextField.text = cityName;
    [self.typeCityTextField resignFirstResponder];
}

@end

Ok so we have view controller that hold UITextField and UIPopoverController and opens it when user focus on the textfield. Popover will close if:

a) User enter something and hide keyboard using hide key (bottom right corner)
b) User will press return button on keyboard
c) User will select some value from popover
d) User will tap outside to dimiss popover

Every seems to work fine except the last scenario where user tap outside to cancel and hide popover. So it is again connected with UIPopoverController delegate methods as option a, b, c closes popover from code using [self.searchPopoverController dismissPopoverAnimated:YES]; and as we all know popover delegate methods are called ONLY if you close popover by touching outside of it. Moving forward if you look closely you will recognize that when this code executes

1
2
3
4
5
6
- (BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
    [self.typeCityTextField resignFirstResponder];

    return YES;
}

we are acutally calling this

1
2
3
4
- (void)textFieldDidEndEditing:(UITextField *)textField
{
    [self.searchPopoverController dismissPopoverAnimated:YES];
}

before we leave function, so in iOS8 we are trying to close already closing popover, why this code works on iOS7? maybe there is difference in responder chain so that’s why we are passing dimiss command down in responder. If you not belive me try and set break point in viewDidDisappear :) Reassuming to fix this isse we need to change this line:

1
2
3
4
5
6
- (BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
    [self.typeCityTextField resignFirstResponder];

    return NO; //YES; 
}

So there is no more duplicated dimiss command moving around in our view. Acutally in this example we are alwasy sure that we call textFieldDidEndEditing no matter user do. But if you ever encounter some stupid , weird behaviours of your modal views you should first look at reponder chain logic.

That’s it, source code as always available on my github page. Next time another iOS8 issue that helps us develop “better” code.