35

I have UI tests which testing login functionality (and uses it to test other stuff), but sometimes when focus is changed from one field to another - the keyboard hides, and although the cursor is blinking in the field, I getting error on field.typeText - no focused fields to fill.

Somehow I realized, that clicking on a Hardware -> Keyboard -> toggle software keyboard makes keyboard to persist on the screen, so test is works well. But I need to make it working on any testing device, on any developer machine, so I want to set this option programmatically without annoying "if test fails, go to … and set … by hand" in readme of the project.

Is it possible?

extempl
  • 2,987
  • 1
  • 26
  • 38

7 Answers7

57

Tested in Xcode 10.3 & Xcode 11. The snippet below needs to be located in the app target (not the test bundle) — for instance, in AppDelegate.swift. It will disable any hardware keyboards from automatically connecting by setting any UIKeyboardInputMode's automaticHardwareLayout properties to nil.

Does not depend on the settings of the Simulator.

#if targetEnvironment(simulator)
// Disable hardware keyboards.
let setHardwareLayout = NSSelectorFromString("setHardwareLayout:")
UITextInputMode.activeInputModes
    // Filter `UIKeyboardInputMode`s.
    .filter({ $0.responds(to: setHardwareLayout) })
    .forEach { $0.perform(setHardwareLayout, with: nil) }
#endif

Or Objective-C:

#if TARGET_IPHONE_SIMULATOR
SEL setHardwareLayout = NSSelectorFromString(@"setHardwareLayout:");
for (UITextInputMode *inputMode in [UITextInputMode activeInputModes]) {
    if ([inputMode respondsToSelector:setHardwareLayout]) {
        // Note: `performSelector:withObject:` will complain, so we have to use some dark magic.
        ((void (*)(id, SEL, id))[inputMode methodForSelector:setHardwareLayout])(inputMode, setHardwareLayout, NULL);
    }
}
#endif
Chris Zielinski
  • 1,231
  • 1
  • 9
  • 11
  • Do you know how to do it in **AppDelegate.m**? – swenedo Sep 06 '19 at 08:14
  • I don’t understand. Do you want the objective-c equivalent? – Chris Zielinski Sep 06 '19 at 16:33
  • Yes please :-) Our app is a React Native app where AppDelegate is in objective-c. – swenedo Sep 08 '19 at 08:52
  • 4
    This seems to be the only solution that is working. – nr5 Feb 26 '20 at 19:07
  • 1
    Can be added to AppDelegate:didFinishLaunchingWithOptions to great effect. Will fix tests, but won't even annoy you by altering the simulator setting. Top solution! – Arru Apr 16 '20 at 13:34
  • Looks nice/working as expected in *unit* (vs UI) tests (so in this case it's part of the tests, not the host app): I'm doing some snapshot tests. I wonder though how to revert it properly, for the sake of less side effects in the tests... – Grigory Entin Jun 16 '20 at 22:05
  • Don't remove the ```, with: nil)``` from the swift code, it results in runtime exceptions. – otaviokz Nov 10 '20 at 23:39
  • 1
    For those using fastlane snapshot this is the working solution that worked for me. Thank you @ChrisZielinski – James DeCarlo Feb 19 '22 at 18:23
22

The simulator's .plist file changed to add support for multiple simulators. The ConnectHardwareKeyboard boolean is now nested underneath the device's UDID. Luckily this UDID is also stored in the plist file. You can add this code using 'run script' under your UITest target's build phases.

Xcode 9 answer:

#grab the UDID from the plist
UDID=$(defaults read com.apple.iphonesimulator CurrentDeviceUDID)

#overwrite the existing value with false
#OR if the plist doesn't have that value add it in
/usr/libexec/PlistBuddy -c "Set :DevicePreferences:$UDID:ConnectHardwareKeyboard 
false" ~/Library/Preferences/com.apple.iphonesimulator.plist 
|| 
/usr/libexec/PlistBuddy -c  "Add :DevicePreferences:$UDID:ConnectHardwareKeyboard
bool false" ~/Library/Preferences/com.apple.iphonesimulator.plist

Or you can use this other code to affect all simulators:

/usr/libexec/PlistBuddy -c "Print :DevicePreferences" ~/Library/Preferences/com.apple.iphonesimulator.plist | perl -lne 'print $1 if /^    (\S*) =/' | while read -r a; do /usr/libexec/PlistBuddy -c "Set :DevicePreferences:$a:ConnectHardwareKeyboard
false" ~/Library/Preferences/com.apple.iphonesimulator.plist || /usr/libexec/PlistBuddy -c  "Add :DevicePreferences:$a:ConnectHardwareKeyboard
bool false" ~/Library/Preferences/com.apple.iphonesimulator.plist; done
Everton Cunha
  • 1,017
  • 8
  • 10
Brooks DuBois
  • 705
  • 6
  • 16
11

Prior to Xcode 9, you can work around this by disabling the hardware keyboard in Simulator.app which will cause the software keyboard to always be present. Eg:

defaults write com.apple.iphonesimulator ConnectHardwareKeyboard -bool NO
Jeremy Huddleston Sequoia
  • 22,938
  • 5
  • 78
  • 86
  • 2
    Looks like after enabling it by hand in simulator it stays enabled for UI tests too. Sad. – extempl Jun 26 '16 at 11:40
  • Yeah, but you can at least automate disabling it before running your UI tests and then re-enabling it after the UI tests. Please make sure you file a radar at http://bugreport.apple.com about this bug. – Jeremy Huddleston Sequoia Jun 27 '16 at 16:13
  • This doesn't work anymore since this property now belongs to each simulator udid, see my answer. – Everton Cunha Apr 13 '18 at 19:54
  • I used this solution (from a different source) for a while, but it was randomly failing if the tests were being run through the console (for instance on the CI server). I switched to the solution where keyboard is disabled on AppDelegate and it seems to work now. The solution is: https://stackoverflow.com/a/57618331/705982 – kubilay Oct 30 '20 at 15:56
  • also does not work for latest xcode... – ch271828n Jul 04 '22 at 23:02
6

Following Brooks great answer, this will work for all simulators:

/usr/libexec/PlistBuddy -c "Print :DevicePreferences" ~/Library/Preferences/com.apple.iphonesimulator.plist | perl -lne 'print $1 if /^    (\S*) =/' | while read -r a; do /usr/libexec/PlistBuddy -c "Set :DevicePreferences:$a:ConnectHardwareKeyboard
false" ~/Library/Preferences/com.apple.iphonesimulator.plist || /usr/libexec/PlistBuddy -c  "Add :DevicePreferences:$a:ConnectHardwareKeyboard
bool false" ~/Library/Preferences/com.apple.iphonesimulator.plist; done
Everton Cunha
  • 1,017
  • 8
  • 10
  • 1
    I can't mark two answers as correct, and your answer is rather enhancement of Brooks` answer, so I'd recommend you to edit his answer and add variant for all udids. – extempl Apr 14 '18 at 03:03
  • 2
    Not working on Xcode 9.4, returns `Unrecognized Type: false` – Pedro Paulo Amorim Aug 01 '18 at 11:19
  • 4
    Tested and working on Xcode 9.4.1. These are not multiple lines, it's one long line. Also try not to paste rich text. Sometimes doesn't work on the first run. You can also try with this file https://github.com/evertoncunha/cocoaheads-2018-07/blob/master/fastlane/enable_simulators_keyboards.sh – Everton Cunha Aug 02 '18 at 14:37
  • 3
    Thank you for the script @EvertonCunha. Unfortunately for me when I run it, I can see that the plist gets edited but the simulator still doesn't show the keyboard. I even checked your gist where I saw that you included a command to kill the simulator but that didn't work for me either. Any clues? – Raphael Oliveira Oct 02 '18 at 07:13
  • 1
    @RaphaelOliveira this is still happening to me. Seems like only 'toggle software keyboard' via the menu in the Simulator will persist it. Would love to know how to do this via the command line. – mikey Jun 19 '19 at 07:50
  • It seems that the recent Simulator versions are very bugged, even custom menu shortcuts aren't working anymore :( – Everton Cunha Jul 10 '19 at 13:00
2

Tested with Xcode 10.1 I tried different approaches but none of them solve how to get the simulator UDID

#Find the UDID of the booted simulator
#And use PlistBuddy to change the flag to true or false
#Set the variable useHardwareKeyboard with the desired result
#Relaunch the simulator
useHardwareKeyboard=false
export UDID=$(xcrun simctl list devices | grep "(Booted)" | grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})")
/usr/libexec/PlistBuddy -c "Set :DevicePreferences:$UDID:ConnectHardwareKeyboard ${useHardwareKeyboard}" ~/Library/Preferences/com.apple.iphonesimulator.plist
xcrun simctl shutdown $UDID
xcrun simctl boot $UDID

You can also verify these changes.

Find your simulator UDID: (More here: https://nshipster.com/simctl/)

xcrun simctl list devices | grep "(Booted)" | grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})"

Run one of these commands and find your changes based on the UDID from above:

defaults read com.apple.iphonesimulator
#OR
open ~/Library/Preferences/com.apple.iphonesimulator.plist
2

inspired by real_kappa_guy s answer above, I managed to do it without restarting the simulator by checking if the keyboard is enabled and then running apple script to send the keyboard shortcut Cmd-shift-k (need to be added to pre-test action):

if test `/usr/libexec/PlistBuddy -c "Print DevicePreferences:${TARGET_DEVICE_IDENTIFIER}:ConnectHardwareKeyboard" ~/Library/Preferences/com.apple.iphonesimulator.plist` == true; then 
osascript <<EOD
  tell application "Simulator" to activate
  tell application "System Events"
      keystroke "K" using {command down, shift down}
  end tell
EOD
fi
user2378197
  • 760
  • 7
  • 11
1

For Xcode 10.2, none of these solutions work for me, the plist is changed correctly but keyboard is till hidden. I have to use oascript to press Ctrl + Shift + K on Simulator if the keyboard is not displayed. It's not beauty but it's the only workaround that works.

tell application "Simulator" to activate
tell application "System Events"
    keystroke "K" using {command down, shift down}
end tell
shim
  • 9,289
  • 12
  • 69
  • 108
real_kappa_guy
  • 313
  • 1
  • 4
  • 12
  • 1
    Nice workaround. I faced similar problem and just found that after setting the defaults ConnectHardwareKeyboard NO, we have to restart the simulator may be using "killall Simulator" shell command. – Hasaan Ali Jul 30 '19 at 09:55
  • 3
    I thought of using an AppleScript like yours, but the problem is that cmd-shift-K is a toggle, so if the simulator already has the hardware keyboard configuration you want, it will reverse it. – Guillaume Laurent Sep 13 '19 at 14:33
  • @GuillaumeLaurent in my case, I only call the script if the hardware keyboard is not displayed. how to determine it is totally depended on what use are using for your UI tests – real_kappa_guy Sep 17 '19 at 10:42
  • 1
    thanks for this. The only problem with this is that it will re-enable the keyboard if it's disabled, so if you do a check before hand it works! (see my answer below) – user2378197 Nov 23 '20 at 11:52