16

In iOS 8 and lower show a UIActionSheet when keyboard is presented will present the action sheet over the keyboard. With iOS 9 this is no longer the case.

In my app we have a chat functionality and want the show a action over the keyboard. We used to use UIActionSheet which worked fine until iOS 8. In iOS 9 the action sheet is present behind the keyboard. I've tried both UIActionSheet and UIAlertController.

What we want is a action sheet like in messages.app Action sheet in messages app.

I've tried placing the action sheet in it own window and overriding canBecomeFirstResponder which just made the keyboard disappear.

rckoenes
  • 69,092
  • 8
  • 134
  • 166
  • Are you sure about `UIAlertController`? I had same problem in my app with `UIActionSheet` in iOS9 but when I switched to `UIAlertController` problem gone. – John Tracid Oct 13 '15 at 19:12
  • Yes I'm sure. But it does seem to only happen when you are using a it when the keyboards is presente as part of an views `inputAccessoryView`. – rckoenes Oct 14 '15 at 07:55
  • Possible duplicate of [KEEP keyboard ON when UIAlertcontroller is presented in Swift?](http://stackoverflow.com/questions/28564710/keep-keyboard-on-when-uialertcontroller-is-presented-in-swift) – gblazex Feb 23 '16 at 21:10

4 Answers4

30

I have implemented exactly this in our app. The trick is to have the alert controller appear on a different window. This is how the UIActionSheet implementation does it, and works great on iOS 8, but on 9, Apple has moved the keyboard implementation to a window which has a very high window level (10000000). The fix is to give your alert window an even higher window level (as a custom double value, not using the provided constants).

When using a custom window which will have transparency, make sure to read my answer here, regarding background color, to prevent window becoming black during rotation transitions.

_alertWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
_alertWindow.rootViewController = [UIViewController new];
_alertWindow.windowLevel = 10000001;
_alertWindow.hidden = NO;
_alertWindow.tintColor = [[UIWindow valueForKey:@"keyWindow"] tintColor];

__weak __typeof(self) weakSelf = self;

UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Test" message:nil preferredStyle:UIAlertControllerStyleActionSheet];
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
    weakSelf.alertWindow.hidden = YES;
    weakSelf.alertWindow = nil;
}]];
[alert addAction:[UIAlertAction actionWithTitle:@"Test" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
    weakSelf.alertWindow.hidden = YES;
    weakSelf.alertWindow = nil;
}]];

[_alertWindow.rootViewController presentViewController:alert animated:YES completion:nil];

enter image description here

Community
  • 1
  • 1
Léo Natan
  • 56,823
  • 9
  • 150
  • 195
  • I had tried everything, but the this high window level. – rckoenes Sep 21 '15 at 09:28
  • @rckoenes It's really annoying. If you run an SDK8 app, it has a low window level so `UIActionSheet` works well. But if the app uses SDK9, it goes to the sky. It's a conspiracy to get you to use `UIAlertController`. `;-)` – Léo Natan Sep 21 '15 at 09:29
  • I'm even using `UIAlertController` but even that did not work and was showing nicely behind the keyboard :S – rckoenes Sep 21 '15 at 09:31
  • @rckoenes One more thing. If you support rotation, make sure to give the window's root controller some color (different than `clearColor`) with an alpha of 0, or on rotation, it will appear black. #uiwindowhacks – Léo Natan Sep 21 '15 at 09:32
  • Thanks for the heads up. – rckoenes Sep 21 '15 at 09:33
  • More hacks. If you want to implement something similar to iOS' messages app's action sheet, with an image picker, you can set a `contentViewController` to alert controllers. – Léo Natan Sep 21 '15 at 09:34
  • 2
    thank you, such a simple solution and good for a while at least, until that value changes again. ;) – drew.. Sep 30 '15 at 16:57
  • @Leo How'd you discover this value? May the value be discovered at runtime? – Travis Sep 30 '15 at 20:57
  • 1
    @Travis Unfortunately, not without using private API. Apple split the keyboard into two windows, and the high keyboard window is not accessible without either using the debugger or using private API. – Léo Natan Oct 01 '15 at 04:26
  • @LeoNatan Don't you think it's enough to grab the current top window and present the VC inside that? `UIApplication.sharedApplication().windows` is ordered by windowLevel. See this answer and my explanation: http://stackoverflow.com/questions/28564710/keep-keyboard-on-when-uialertcontroller-is-presented-in-swift – gblazex Feb 23 '16 at 21:08
  • @galambalazs No, because the keyboard is drawn in a window of its own, which is a system window that does not appear under `windows`. – Léo Natan Feb 23 '16 at 22:00
  • @LeoNatan The code is working iOS 8 & 9. Did you try it? `` does appear in the list on iOS 9. – gblazex Feb 23 '16 at 22:04
  • @galambalazs Interesting. I remember looking for that window and not seeing it there in the past. I figured they did it on purpose to better protect the keyboard, with a possible full XPC keyboard implementation in iOS 10 (hence the "Remote" in the name). – Léo Natan Feb 24 '16 at 17:55
  • @galambalazs Still, would not present any controller on a system controller, especially one that is managed by the keyboard. – Léo Natan Feb 24 '16 at 18:00
  • @galambalazs Also changed your edit to be more correct. Using the main window is not always correct, especially if the application is managing more than one windows. Taking the key window's tint color is more accurate. – Léo Natan Feb 24 '16 at 18:02
  • Isn't it `[UIApplication sharedApplication].keyWindow` that you meant? – gblazex Mar 05 '16 at 14:08
  • From experience, `+[UIWindow keyWindow]` is more accurate at some edge cases. I just use that. – Léo Natan Mar 05 '16 at 15:50
  • Can any one confirm whether this still works for iOS 9.3.5 and iOS 10? I found the maximum of windowLevel you can set is 10000000. – Joe Smith Sep 26 '16 at 22:56
  • @sabalaba Please do not modify my answers without first understanding why it is written the way it is written. – Léo Natan Mar 10 '17 at 08:33
  • @LeoNatan 1) `[UIWindow valueForKey:@"keyWindow"]` is relying on a class method that is only declared in the runtime headers, and is private API usage for getting the tint color---`[UIApplication sharedApplication].keyWindow` works just fine without using a private API. 2) What's your reason for accessing the alertWindow variable later? 3) A minor point, but assuming you do have a reason for accessing your alertWindow later, you have inconsistent accessor usage, you use an ivar outside the block and then in the block you use setters (either synthesized with a property or otherwise). – sabalaba Mar 12 '17 at 05:01
  • @Jerome What happens on 10.3? – Léo Natan Sep 14 '17 at 08:53
  • @Leo Natan, UIAlertController will under keyboard still. – Jerome Sep 14 '17 at 09:12
  • Sounds like incorrect setting for the window level. What is the keyboard window level in 10.3? – Léo Natan Sep 14 '17 at 09:12
8

The answer supplied by Leo is broken as of iOS 11, because Apple now prevents you from setting a windowLevel above 10000000. A fix is to implement a custom UIWindow and override the windowLevel receiver:

@interface TopWindow : UIWindow @end

@implementation TopWindow
- (UIWindowLevel) windowLevel {
    return 20000000.000;
}
@end

// usage:
UIWindow* w = [[TopWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
w.rootViewController = [UIViewController new];
w.hidden = NO;

[w.rootViewController presentViewController:yourActionSheetController animated:YES completion:nil];

This approach should be backwards compatible, but haven't tested all known versions. Happy hacking!

Emiel Mols
  • 436
  • 4
  • 12
5

Based on Leo Natan's answer, I've created a Swift extension for presenting an alert sheet over the keyboard.

In my brief testing, the alertWindow is deallocated after the alert is dismissed, I believe because there's no strong reference to it outside of the alert. This means there's no need to hide or deallocate it in your UIAlertActions.

extension UIAlertController {

    func presentOverKeyboard(animated: Bool, completion: (() -> Void)?) {

        let alertWindow = UIWindow(frame: UIScreen.mainScreen().bounds)

        // If you need a white/hidden/other status bar, use an appropriate VC.
        // You may not need a custom class, and you can just use UIViewController()
        alertWindow.rootViewController = whiteStatusBarVC()

        alertWindow.windowLevel = 10000001
        alertWindow.hidden = false

        // Set to a tint if you'd like
        alertWindow.tintColor = UIColor.greenColor()

        alertWindow.rootViewController?.presentViewController(self, animated: animated, completion: completion)
    }
}

private class whiteStatusBarVC: UIViewController {
    private override func preferredStatusBarStyle() -> UIStatusBarStyle {
        return .LightContent
    }
}
Kyle Bashour
  • 1,327
  • 2
  • 13
  • 20
  • That's one way to do. I'm using a lot of UIAlertController throughout the app. So, I don't want to deallocate in each handler. Thanks. – Dinesh Raja Jul 27 '16 at 13:02
-2

use UIAlertController instead of UIActionSheet

charlie
  • 11
  • 1
    That does not work in iOS9, it will still be behind the keyboard. – rckoenes Oct 02 '15 at 08:07
  • 1
    While this might be a valuable hint to solve the problem, an answer really needs to demonstrate the solution. Please [edit] to provide example code to show what you mean. Alternatively, consider writing this as a comment instead. – Toby Speight Nov 08 '16 at 13:56