46

Previous to iOS 9, the most reliable method of determining whether an external keyboard is connected was to listen for UIKeyboardWillShowNotification and make a text field the first responder, as discussed in this question. The notification would fire when using the virtual keyboard, but would not fire when using an external keyboard.

However this behavior has now changed with iOS 9. UIKeyboardWillShowNotification also fires when an external keyboard is connected, since the new keyboard toolbar is now shown.

It is still possible to detect the keyboard height and make a judgement whether it is the smaller toolbar or the larger virtual keyboard that is being shown. However this method is not reliable since the keyboard height has changed between the various beta and can't be counted on to stay the same over time.

Is there a more reliable method that can be used with iOS 9?

Community
  • 1
  • 1
Sarah Elan
  • 2,465
  • 1
  • 23
  • 45
  • Just a question. Why do you need to know if a external keyboard is connected? – agy Aug 25 '15 at 19:48
  • 5
    @agy In order to enable functionality that is only supposed to be enabled when the user is using an external keyboard. – Sarah Elan Aug 25 '15 at 20:22
  • How about this one? https://github.com/danielamitay/DAKeyboardControl It works with iOS9, but I don't know you can detect if an external keyboard is connected. – pixyzehn Sep 01 '15 at 15:54
  • I would like to know if a keyboard is attached so I can show keyboard shortcuts (not the ones you get with the command key) — I want to allow typing 1, 2, 3. There would not be a text field. – David Dunham Nov 12 '16 at 02:23

11 Answers11

59

After going back to the original question, I've found a solution that works.

It seems that when the regular virtual keyboard is displayed the keyboard frame is within the dimensions of the screen. However when a physical keyboard is connected and the keyboard toolbar is displayed, the keyboard frame is located offscreen. We can check if the keyboard frame is offscreen to determine if the keyboard toolbar is showing.

Objective-C

- (void) keyboardWillShow:(NSNotification *)notification {
    NSDictionary* userInfo = [notification userInfo];
    CGRect keyboardFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
    CGRect keyboard = [self.view convertRect:keyboardFrame fromView:self.view.window];
    CGFloat height = self.view.frame.size.height;

    if ((keyboard.origin.y + keyboard.size.height) > height) {
        self.hasKeyboard = YES;
    }
}

Swift

@objc func keyboardWillShow(_ notification: NSNotification) {
    guard let userInfo = notification.userInfo else {return}
    let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
    let keyboard = self.view.convert(keyboardScreenEndFrame, from: self.view.window)
    let height = self.view.frame.size.height
    if (keyboard.origin.y + keyboard.size.height) > height {
        self.hasKeyboard = true
    }
}
Sarah Elan
  • 2,465
  • 1
  • 23
  • 45
  • Thank you very much! saved so much time for me during a release. :) – Ninja9 Oct 05 '15 at 22:10
  • 4
    Thanks! To calculate keyboard's toolbar height on iOS9: `CGFloat toolbarHeight = height - keyboard.origin.y` – SoftDesigner Oct 20 '15 at 20:33
  • hasKeyboard should be YES when a physical keyboard is attached, correct? – trever Mar 14 '16 at 23:56
  • 2
    For some reason, this works fine the first time, but then if I dismiss and pull the keyboard back up, it does not calculate properly and says there is a keyboard attached when there is not. – trever Mar 29 '16 at 16:40
  • 1
    @trever yes, it's because you have to reset the hasKeyboard property in a else clause of the if – Pierre Mardon Jun 15 '17 at 15:42
  • `fromView:self.view.window` . WINDOW? Really? – Alexander Volkov Dec 03 '17 at 05:19
  • Do you know how to detect the key down if bluetooth keyboard is connected to iOS devices? – newszer Jan 05 '18 at 07:11
  • @SarahElan, how about if you want to check without tapping into a textfield? is the private API the only way? – ɯɐɹʞ Mar 06 '18 at 22:00
  • 1
    @ɯɐɹʞ you can programmatically create a UITextView and set it as first responder, then resign as first responder and remove it. keyboardWillShow will fire. – Sarah Elan Mar 07 '18 at 14:26
  • 4
    I tested this on iOS 11 iPads and the `keyboard.origin.y + keyboard.size.height` would result with a value equal to `self.view.frame.size.height`; however, I found `keyboard.size.height` equals 55 for all cases – ɯɐɹʞ Mar 07 '18 at 22:31
  • 3
    I tested iOS 12.1 new iPads Pro and the toolbarHeight is 69, so it isn't all cases 55 height anymore. – thacilima Nov 21 '18 at 13:39
  • Don't convert the frame from the window -- just keep it in window bounds, and compare against UIScreen.bounds. Your view might not take up the whole screen. – beebcon Aug 02 '19 at 14:15
  • 1
    More better would be: `self.hasKeyboard = (keyboard.origin.y + keyboard.size.height) > height` To make sure it gets set back to false when the external keyboard is removed – chrysb Jul 13 '21 at 18:28
29

iOS 14 SDK finally brings public API for that: GCKeyboard. To check if external keyboard is connected:

let isKeyboardConnected = GCKeyboard.coalesced != nil

Notes:

  • import GameController
  • you might need to enclose it in if #available(iOS 14.0, *)
mmackh
  • 3,550
  • 3
  • 35
  • 51
kambala
  • 2,341
  • 19
  • 20
  • I can confirm that this works, thank you! However, I'd also need to check if the keyboard is "active". A folio keyboard is registered as soon as it's snapped into place, but I have to find a way to see if it's folded out and being used. – Daniel Saidi May 13 '21 at 09:43
  • I have the same issue. A folded folio keyboard causes the above to return true. – Nikolozi Oct 15 '21 at 11:52
  • I mean, it is true in the sense that it's connected to the device, but that's not helping us here, right? :) – Daniel Saidi Sep 27 '22 at 06:38
  • @DanielSaidi not sure why? – kambala Sep 27 '22 at 07:40
  • 2
    For the Smart Keyboard Folio, connecting the keyboard to the device makes the above return true, even if the keyboard is folded back. Grammarly, however, can somehow also detect when the keyboard is being activated by docking the device to its magnetic dock. – Daniel Saidi Sep 28 '22 at 08:19
  • GameController framework might provide some property for that, I haven't checked that – kambala Sep 28 '22 at 08:23
  • Doesn't work for my Lightning connected keyboard. – David H Dec 29 '22 at 18:29
6

This code supports iOS 8 and iOS 9, inputAccessoryView, has double-protected constant to be ready for new changes in future versions of iOS and to support new devices:

#define gThresholdForHardwareKeyboardToolbar 160.f // it's minimum height of the software keyboard on non-retina iPhone in landscape mode

- (bool)isHardwareKeyboardUsed:(NSNotification*)keyboardNotification {
    NSDictionary* info = [keyboardNotification userInfo];
    CGRect keyboardEndFrame;
    [[info valueForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardEndFrame];
    float height = [[UIScreen mainScreen] bounds].size.height - keyboardEndFrame.origin.y;
    return height < gThresholdForHardwareKeyboardToolbar;
}

Note, a hardware keyboard may present but not used.

Dmitry
  • 14,306
  • 23
  • 105
  • 189
2

I am using a variation on Sarah Elan's answer. I was having issues with her approach in certain views. I never quite got to the bottom of what caused the problem. But here is another way to determine if it is an ios9 external keyboard 'undo' bar that you have, rather than the full sized keyboard.

It is probably not very forward compatible since if they change the size of the undo bar, this brakes. But, it got the job done. I welcome criticism as there must be a better way...

//... somewhere ...
#define HARDWARE_KEYBOARD_SIZE_IOS9 55 
//

+ (BOOL) isExternalKeyboard:(NSNotification*)keyboardNotification {

  NSDictionary* info = [keyboardNotification userInfo];
  CGRect keyboardEndFrame;
  [[info valueForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardEndFrame];
  CGRect keyboardBeginFrame;
  [[info valueForKey:UIKeyboardFrameBeginUserInfoKey] getValue:&keyboardBeginFrame];

  CGFloat diff = keyboardEndFrame.origin.y - keyboardBeginFrame.origin.y;
  return fabs(diff) == HARDWARE_KEYBOARD_SIZE_IOS9;
}
Mike
  • 894
  • 12
  • 19
  • 1
    This was the path I originally went down, and it does get the job done nicely. The problem came when the size changed from 49 to 55 between iOS9 beta releases. It just didn't seem reliable any more. – Sarah Elan Sep 21 '15 at 13:11
  • You don't have to hardcode keyboard's toolbar size. Use this line: `CGFloat toolbarHeight = UIScreen.mainScreen().bounds.height - keyboardEndFrame.origin.y` – SoftDesigner Oct 21 '15 at 18:34
  • SoftDesigner, this value doesn't help. – Dmitry Feb 13 '16 at 21:04
  • 1
    The keyboard size is different when device in portrait mode or landscape mode and the spell checking is on or off. Morover size is different when someone use external developed software keyboard. – feca Mar 11 '16 at 06:44
1

Private API solution: (have to grab the private header file - use RuntimeViewer).

Works nicely for enterprise apps, where you don't have AppStore restrictions.

#import "UIKit/UIKeyboardImpl.h"

+ (BOOL)isHardwareKeyboardMode
{
   UIKeyboardImpl *kbi = [UIKeyboardImpl sharedInstance];
   BOOL externalKeyboard = kbi.inHardwareKeyboardMode;
   NSLog(@"Using external keyboard? %@", externalKeyboard?@"YES":@"NO");
   return externalKeyboard;
}
Simon Tillson
  • 3,609
  • 1
  • 15
  • 23
1

In iOS 15 when a hardware Keyboard is present and you are supporting earlier versions of iOS that do not support an explicit indication that an external keyboard IS present, the Keyboard height and width obtained via:

CGRect kbEndFrame = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];

both have a value of 0

Therefore you can detect external hardware via:

if(kbEndFrame.size.height == 0)
    // External/virtual KB exists
else
    // Virtual on screen KB exists
Cliff Ribaudo
  • 8,932
  • 2
  • 55
  • 78
0

You could try checking for peripherals that are advertising services using Core Bluetooth

CBCentralManager *centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil]; 
[centralManager scanForPeripheralsWithServices:nil options:nil];

And you should implement the delegate:

- (void)centralManager:(CBCentralManager * _Nonnull)central
 didDiscoverPeripheral:(CBPeripheral * _Nonnull)peripheral
     advertisementData:(NSDictionary<NSString *,
                                id> * _Nonnull)advertisementData
                  RSSI:(NSNumber * _Nonnull)RSSI{

}
agy
  • 2,804
  • 2
  • 15
  • 22
  • I want to detect whether any external keyboard, of any kind, is connected. I'm not looking for a specific accessory. – Sarah Elan Aug 25 '15 at 21:09
  • @SarahElan this could lead you to your solution. – agy Aug 25 '15 at 21:13
  • 4
    Can you expand on this? It needs a lot more information to be a useful answer. – Sarah Elan Aug 27 '15 at 14:12
  • @SarahElan I'm thinking one could couple this with the answer above for iOS 14, where you can use the `GCKeyboard` framework to determine if it was a keyboard that got plugged in. I need this for a project now so going to try that out soon. – David H Dec 20 '22 at 19:33
0

You can subscribe notification when the external device is connected:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceConnected:) name:EAAccessoryDidConnectNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceDisconnected:) name:EAAccessoryDidDisconnectNotification object:nil];
[[EAAccessoryManager sharedAccessoryManager] registerForLocalNotifications];

Or just retrieve the list of attached devices:

EAAccessoryManager* accessoryManager = [EAAccessoryManager sharedAccessoryManager];

if (accessoryManager)
{
    NSArray* connectedAccessories = [accessoryManager connectedAccessories];
    NSLog(@"ConnectedAccessories = %@", connectedAccessories);
}
itsji10dra
  • 4,603
  • 3
  • 39
  • 59
0

If you make the toolbar irrelevant then the keyboard doesn't show up. Do this by blanking out its left and right groups (at least on iOS 12.4):

textField.inputAssistantItem.leadingBarButtonGroups = []
textField.inputAssistantItem.trailingBarButtonGroups = []

...and in case it helps here is a swifty way to observe:

// Watch for a soft keyboard to show up
let observer = NotificationCenter.default.addObserver(forName: UIWindow.keyboardWillShowNotification, object: nil, queue: nil) { notification in
    print("no external keyboard")
}

// Stop observing shortly after, since the keyboard should have shown by now
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
    NotificationCenter.default.removeObserver(observer)
}
garafajon
  • 1,278
  • 13
  • 15
0
  1. None of the answers here worked for me. I have iPad Air with iOS 13.x.

I was able to do it, I reckon, by checking the height of the keyboard. That's it! Notice when an external keyboard is connected, the onscreen keyboard is about 50-60px in height. See the working demo here: https://youtu.be/GKi-g0HOQUc

So in your event keyboardWillShow, just get the keyboard height and see if it's around 50-60, if so, then we can assume that there's an external keyboard connected.

@objc func keyboardWillShow(_ notification: NSNotification) {
        guard let userInfo = notification.userInfo else {return}
        let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
        let keyboard = self.view.convert(keyboardScreenEndFrame, from: self.view.window)

        // If with keyboard, the onscreen keyboard height is 55.
        // otherwise, the onscreen keyboard has >= 408px in height.
        // The 66 digit came from a Stackoverflow comment that the keyboard height is sometimes around that number.
        if keyboard.size.height <= 66 {
            hasExternalKeyboard = true
        } else {
            hasExternalKeyboard = false
        }
    }
Glenn Posadas
  • 12,555
  • 6
  • 54
  • 95
0

This solution works for me on iOS 9 - iOS 16 (iPhones only, doesn't work on newest iPads)

class ViewController: UIViewController {
    // declare a proprty where you will store keyboard notifications
    private var cachedKeyboardNotification: Notification?
    
    // in this iexample tere is only one text fiels which can trigger the keyboard
    var textField: UItextField!

    override func viewDidLoad() {
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardNotifications), name: UIWindow.keyboardDidShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardNotifications), name: UIWindow.keyboardDidChangeFrameNotification, object: nil)
    }

   // store the keyboard notifioaction
   @objc func keyboardNotifications(_ notification: Notification) {
        cachedKeyboardNotification = notification
   }

    func isKeyboardExternal() -> Bool {
        
        guard let notification = cachedKeyboardNotification else {
            return false
        }
        guard let kbEndFrame = (notification.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else {
            return false
        }
        let offset = kbEndFrame.maxY - UIScreen.main.bounds.height
        let kbVisibleHeight = kbEndFrame.height - offset
        let accessoryViewHeight = textField.inputAccessoryView?.frame.height ?? 0
    
        return kbVisibleHeight - accessoryViewHeight == 0
    }

....

}