Is there a way in Swift to force the iOS keyboard to not have the top part? By saying the top part I mean the autocomplete and the input field switcher tools that appear at the top.
Some of my views have embedded webViews that run local js and I want the keyboard for the inputs in webView to not have that top part of the keyboard. If it`s not possible to disable these for webView specifically, any other method should be fine as well.
Please take a look at this screenshot to see exactly what part of the keyboard I am talking about.
You can try running the below JS in the webview every time you load the web page
var textFields = document.getElementsByTagName('input');
if (textFields) {
var i;
for( i = 0; i < textFields.length; i++) {
var txtField = textFields[i];
if(txtField) {
txtField.setAttribute('autocomplete','off');
txtField.setAttribute('autocorrect','off');
txtField.setAttribute('autocapitalize','off');
txtField.setAttribute('spellcheck','false');
}
}
}
Additionally write this code to hide the done button accessory view from keyboard
class CustomWebView: WKWebView {
var accessoryView: UIView?
override var inputAccessoryView: UIView? {
return accessoryView
}
}
And use CustomWebView in place of WKWebView wherever this functionality is needed.
Let me know if you need any more help.
Happy Coding :)
Related
Portions of my app present users with forms via a webview. I've had a few users ask why after entering data into a field when they continue to scroll down the form/webview the keyboard remained visible on the screen. I cannot seem to find any textarea or textfield delegate method which would dismiss the keyboard.
I am apprehensive to start adding gesture recognizers because I don't want to override the touches on the webview.
Native behavior:
The Keyboard shows automatically on focus on an UITextView, to hide it again, you need to call yourself on the UIView [self.view endEditing:YES];
On the WKWebView the Keyboard also shows automatically on focus on an HTMLInputElement for example and hides on blur.
But the WKWebView does not blur the focus of the HTMLInputElement by tapping somewhere outside (e.g. on the body), so the Keyboard still stays in the view.
If you want to hide the Keyboard after some scrolling you could just go with a little JS (inject it just into the WKWebView):
var SKIP_BLURRING = ['INPUT', 'TEXTAREA'];
document.body.addEventListener('touchend', function (event) {
var activeElement = document.activeElement;
var tapped = event.target;
var shouldSkipElement = SKIP_BLURRING.indexOf(tapped.tagName) > -1;
var isSameElement = activeElement === tapped;
if (shouldSkipElement || isSameElement) {
return;
}
activeElement.blur();
}, false);
I want to have the same kind of behaviour as iMessage has for its input. I don't know in what end I should start, so I'll describe what I want to do and you can (I hope) give me suggestions on how to do this. I code in Swift so I'd like it to be in Swift if you provide any code.
What I want
I want to have a button on my screen (not an UITextView or UITextField) which upon press shows the keyboard, and where the keyboard has a UIToolBar with an UITextView in it. When I type in the UITextView the ToolBar/TextView expands up until a certain point then it starts to scroll.
How on earth do I do this? I've been trying for an hour but I can't seem to trigger the keyboard unless I have a UITextView or UITextField to set as becomeFirstResponder(). Furthermore I don't understand how I'm supposed to attach a UITextView to the UIToolBar once I get the keyboard up. I have added the UIToolBar, but not the UITextField.
Cheers
This should do it for you: https://github.com/AlexLittlejohn/ALTextInputBar
It grows like you mentioned, and it's also at the bottom of screen, like an input accessory view.
In your pods file copy paste
pod 'ALTextInputBar'
Run your podfile
Configure it like this
import ALTextInputBar
class ViewController: UIViewController {
let textInputBar = ALTextInputBar()
// The magic sauce
// This is how we attach the input bar to the keyboard
override var inputAccessoryView: UIView? {
get {
return textInputBar
}
}
// Another ingredient in the magic sauce
override func canBecomeFirstResponder() -> Bool {
return true
}
}
I just started with UI testing in Xcode 7 and hit this problem:
I need to enter text into a textfield and then click a button. Unfortunately this button is hidden behind the keyboard which appeared while entering text into the textfield. Xcode is trying to scroll to make it visible but my view isn't scrollable so it fails.
My current solution is this:
let textField = app.textFields["placeholder"]
textField.tap()
textField.typeText("my text")
app.childrenMatchingType(.Window).elementBoundByIndex(0).tap() // hide keyboard
app.buttons["hidden button"].tap()
I can do this because my ViewController is intercepting touches:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
view.endEditing(false)
super.touchesBegan(touches, withEvent: event)
}
I am not really happy about my solution, is there any other way how to hide the keyboard during UI testing?
If you have set up your text fields to resign FirstResponder (either via textField.resignFirstResponder() or self.view.endEditing(true)) in the textFieldShouldReturn() delegate method, then
textField.typeText("\n")
will do it.
Swift 5 helper function
func dismissKeyboardIfPresent() {
if app.keyboards.element(boundBy: 0).exists {
if UIDevice.current.userInterfaceIdiom == .pad {
app.keyboards.buttons["Hide keyboard"].tap()
} else {
app.toolbars.buttons["Done"].tap()
}
}
}
Based on a question to Joe's blog, I have an issue in which after a few runs on simulator the keyboards fails to hide using this piece of code:
XCUIApplication().keyboard.buttons["Hide keyboard"]
So, I changed it to: (thanks Joe)
XCUIApplication().keyboard.buttons["Hide keyboard"]
let firstKey = XCUIApplication().keys.elementBoundByIndex(0)
if firstKey.exists {
app.typeText("\n")
}
What I try to do here is detecting if the keyboard stills open after tap the hide button, if it is up, I type a "\n", which in my case closes the keyboard too.
This also happens to be tricky, because sometimes the simulator lost the focus of the keyboard typing and this might make the test fail, but in my experience the failure rate is lower than the other approaches I've taken.
I hope this can help.
I always use this to programmatically hide the keyboard in Swift UITesting:
XCUIApplication().keyboards.buttons["Hide keyboard"].tap()
XCUIApplication().toolbars.buttons["Done"].tap()
With Swift 4.2, you can accomplish this now with the following snippet:
let app = XCUIApplication()
if app.keys.element(boundBy: 0).exists {
app.typeText("\n")
}
The answer to your question lies not in your test code but in your app code. If a user cannot enter text using the on-screen software keyboard and then tap on the button, you should either make the test dismiss the keyboard (as a user would have to, in order to tap on the button) or make the view scrollable.
Just make sure that the keyboard is turned off in the simulator before running the tests.
Hardware->Keyboard->Connect Hardware Keyboard.
Then enter your text using the paste board
textField.tap()
UIPasteboard.generalPasteboard().string = "Some text"
textField.doubleTap()
app.menuItems["paste"].tap()
I prefer to search for multiple elements that are possibly visible to tap, or continue, or whatever you want to call it. And choose the right one.
class ElementTapHelper {
///Possible elements to search for.
var elements:[XCUIElement] = []
///Possible keyboard element.
var keyboardElement:XCUIElement?
init(elements:[XCUIElement], keyboardElement:XCUIElement? = nil) {
self.elements = elements
self.keyboardElement = keyboardElement
}
func tap() {
let keyboard = XCUIApplication().keyboards.firstMatch
if let key = keyboardElement, keyboard.exists {
let frame = keyboard.frame
if frame != CGRect.zero {
key.forceTap()
return
}
}
for el in elements {
if el.exists && el.isHittable {
el.forceTap()
return
}
}
}
}
extension XCUIElement {
///If the element isn't hittable, try and use coordinate instead.
func forceTap() {
if self.isHittable {
self.tap()
return
}
//if element isn't reporting hittable, grab it's coordinate and tap it.
coordinate(withNormalizedOffset: CGVector(dx:0, dy:0)).tap()
}
}
It works well for me. This is how I would usually use it:
let next1 = XCUIApplication().buttons["Next"]
let keyboardNext = XCUIApplication().keyboards.firstMatch.buttons["Next"]
ElementTapHelper(elements: [next1], keyboardElement: keyboardNext).tap()
Nice thing about this is you can provide multiple elements that could be tapped, and it searches for keyboard element first.
Another benefit of this is if you are testing on real devices the keyboard opens by default. So why not just press the keyboard button?
I only use this helper when there are multiple buttons that do the same thing, and some may be hidden etc.
If you are using IQKeyboardManager you can easily do this:
app.toolbars.buttons["Done"].tap()
This way you capture the "Done" button in the keyboard toolbar and hide the keyboard. It also works for different localizations.
My application is used with a barcode scanner connected via Bluetooth. When the scanner is connected you can double tap a button on the scanner to dismiss/show the on screen keyboard. 90% of the time the user will want the keyboard to be hidden as they will be scanning a barcode to input data. There are a few exceptions that I know of ahead of time that the keyboard will need to be enabled, I would like to save them the effort of pressing the scanner button to bring up the keyboard and instead force the keyboard to show up.
The scanner does not use resignfirstresponder to dismiss the keyboard, this is evident because the cursor is still visible and scanning a barcode will input data into the current text field.
Does anyone know how to dismiss/show the on screen keyboard without using resignfirstresponder?
For reference I am using this scanner http://ww1.socketmobile.com/products/bluetooth-scanners/how-to-buy/details.aspx?sku=CX2864-1336
To end editing completely in the view you can use the following
[self.view endEditing:YES];
This will remove the keyboard for you in the view.
For anyone still struggling with this, you can achieve this in Swift by making the inputView of the textfield equals UIView()
That is:
yourtextfield.inputview = UIView()
I ran into this today and have found a solution. The trick is to use a secondary text field that is off-screen or hidden with a custom empty inputView set and make that field become the first responder. That field captures text from the hardware scanner while the software keyboard hides.
However I got this working using a very similar approach and instead making the view controller itself the first responder as the scanning input view.
Example:
class SearchViewController: UIViewController, UIKeyInput {
let searchField = UITextField()
let softwareKeyboardHider = UIView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(searchField)
inputAssistantItem.leadingBarButtonGroups = []
inputAssistantItem.trailingBarButtonGroups = []
}
override var canBecomeFirstResponder: Bool {
return true
}
override var inputView: UIView? {
return softwareKeyboardHider
}
var hasText: Bool {
return searchField.hasText
}
func insertText(_ text: String) {
searchField.insertText(text)
}
func deleteBackward() {
searchField.deleteBackward()
}
}
Now, when you want to hide the software keyboard make SearchViewController the first responder.
To show the software keyboard make SearchViewController.searchField the first responder.
I'm just starting out with UI Automation for my iOS app and am already having trouble. I'm unable to attach screen shots so I'll do my best to describe my scenario.
I'm building for iOS 6.0 and am using a storyboard. The app launches to a screen with a navigation controller. The root view controller contains a main view that has 1 UIView subview that takes up the bottom 60% of the screen and a segmented control that sits above that subview. I was able to configure accessibility for the main view (label "mainview"). I am then able to locate this element in my test no problem. However, I am now having trouble finding the segmented controller. So I decided to log out the length of "elements()" and "segementedControls()" from my "mainview" element and the length of each array is 0. So somehow when the test is running my app it's saying there are no sub-elements on my main view.
Another thing to note is that I could not find any accessibility section in the identity inspector of the storyboard editor for the segmented control. However I temporarily added a button to my main view and configured that with an accessibility label, just to test if the elements() or buttons() calls would subsequently show an element for the main view when running my test, but these arrays were still returning as empty, even with the button.
Here's my script:
var target = UIATarget.localTarget();
var app = target.frontMostApp();
function selectListView() {
var testName = "selectListView";
UIALogger.logStart(testName);
var view = app.mainWindow().elements()["mainview"];
if (!view.isValid()) {
UIALogger.logFail("Could not locate main view");
}
UIALogger.logMessage("Number of elements for sub element: " + view.elements().length);
var segmentedControl = view.segmentedControls()[0];
if (!segmentedControl.isValid()) {
UIALogger.logFail("Could not locate segmented control on physician collection parent view");
}
var listButton = segmentedControl.buttons()[1];
if (!listButton.isValid()) {
UIALogger.logFail("Could not locate list button on segemented controller on physician collection parent view");
}
UIALogger.logMessage("Tapping List button on physician collection view's segmented control");
listButton.tap();
UIALogger.logPass(testName);
}
selectListView();
Any help greatly appreciated.
EDIT: I added this to my script to search the entire view hierarchy from the main window, set an accessibility label value for my segmented control in initWithCoder (since I don't seem able to set one in the storyboard editor for a segmented control, as I stated earlier) and still could not find the element - it's as though it's just not in the view hierarchy, though it's on the screen and functions just fine:
function traverse(root, name) {
if (root.name() == name) {
return root;
}
var elements = root.elements();
for (var i = 0; i < elements.length; i++) {
var e = elements[i];
var result = traverse(e, name);
if(result != null) {
return result;
}
}
return null;
}
function selectListView() {
var testName = "selectListView";
var segmentedControl = traverse(UIATarget.localTarget().frontMostApp().mainWindow(), "mysegementedcontrol");
if (segmentedControl == null) {
UIALogger.logMessage("Still could not find it");
}
....
}
EDIT: Added call to app.logElementTree() and still no segmented control in sight ("PhysicianCollectionParentView" is my "mainview" - you can see, no sub-elements there):
EDIT: Here are some screen shots. The first shows my "master" view controller. The next shows that in addition to the segmented control there is also a UIView subview. The 3rd shows the basic entry point for the app in my storyboard.
Here is the class extension for my "master" view controller here, showing the outlets for the segmented control and the other UIView subview:
#interface PhysicianCollectionMasterViewController ()
#property (strong, nonatomic) IBOutlet UISegmentedControl *viewSelectionControl;
#property (strong, nonatomic) IBOutlet UIView *physicianCollectionView;
#end
EDIT: Here's something very interesting - I decided to go with a brand new script created within instruments and take advantage of the record feature. When I clicked on my segmented control, here's the JavaScript it created to show me how it had accessed one of the buttons on my segmented control:
var target = UIATarget.localTarget();
target.frontMostApp().mainWindow().elements()["PhysicianCollectionParentView"].tapWithOptions({tapOffset:{x:0.45, y:0.04}});
So, I guess worst-case I could go with something like this, but it just makes no sense to me that UI Automation just does not think that the control exists. So strange. There must be something I'm missing but my setup is so basic I can't imagine what it could be.
When you set accessibilityLabel for an element and flag it isAccessibilityElement = YES; the subviews of that element are hidden. For automation, you should use accessibilityIdentifier instead of accessibilityLabel and set isAccessibilityElement = NO;
In your objective C code after physicianCollectionView is rendered, remove the label and accessibility flag and do this instead:
physicianCollectionView.accessibilityIdentifier = #"PhysicianCollectionParentView";
physicianCollectionView.isAccessibilityElement = NO;
For last elements in element tree, which do not have sub views, set isAccessibilityElement = YES;
If you haven't tried it already, you might try adding a delay at the beginning of your script:
UIATarget.localTarget().delay(3);
I think that it is possible that your app isn't done rendering/animating before the logElementTree() that you have posted above. We add delays at the beginning and between each application transition in our automated testing scripts to ensure that the screen has finished rendering.
EDIT: After messing around with your setup in a test app, I believe that your issue is that you are enabling Accessibility on the UIView that contains your segmented control. With accessibility disabled on the UIView, I am able to get the UISegmentedControl to show in the element tree, but once I enable it, the UIView then begins displaying as a UIAElement with no children. My suggestion is to disable accessibility for the containing UIView, and only use accessibility for base controls (like buttons, table view cells, or your segmented button control).