I have an UIAlertController with a textfield in it, like this:
let alertController = UIAlertController(title: "Title", message: "Hello, World!", preferredStyle: .Alert)
let someAction = UIAlertAction(title: "Action", style: .Default) { (_) in }
let cancelAction = UIAlertAction(title: "Cancel", style: .Cancel) {(_) in }
alertController.addAction(someAction)
alertController.addAction(cancelAction)
alertController.addTextFieldWithConfigurationHandler { textfield in
textfield.text = "Text"
}
self.presentViewController(alertController, animated: true, completion: nil)
When the controller is presented, the textfield has focus and the keyboard comes up. Is it possible to change that behavior so that the textfield only becomes first responder when the user taps on it? I don't want the keyboard to be presented at the same time the alert controller is presented.
Here's one slightly hacky solution that involves associated objects and method swizzling.
The idea is to give UITextField
a closure that gives outside classes a say in whether a text field can become the first responder or not. This closure passes back boolean value indicating whether the text field is trying to become the first responder due to user interaction or due to canBecomeFirstResponder
having been programmatically called on the text field. It also passes back the value that canBecomeFirstResponder
would return normally in the defaultValue
parameter -- this isn't needed in this case, but as a general solution it could be useful to have.
First, we add the UITextField
extension that will handle the swizzling and associated object stuff:
public extension UITextField {
// Private struct to hold our associated object key
private struct AssociatedKeys {
static var HandlerKey = "xxx_canBecomeFirstResponder"
}
// Typealias for the type of our associated object closure
public typealias CanBecomeFirstResponderHandler = (fromUserInteraction: Bool,
defaultValue: Bool) -> Bool
// We need this private class to wrap the closure in an object
// because objc_setAssociatedObject takes an 'AnyObject', but
// closures are not 'AnyObject's -- they are instead 'Any's
private class AnyValueWrapper {
var value: Any?
}
// Define the closure as a computed property and use associated objects to
// store/retrieve it.
public var canBecomeFirstResponderHandler: CanBecomeFirstResponderHandler? {
get {
// Get the AnyValueWrapper object
let wrapper = objc_getAssociatedObject(self,
&AssociatedKeys.HandlerKey)
// ...then get the closure from its `value` property
return (wrapper as? AnyValueWrapper)?.value
as? CanBecomeFirstResponderHandler
}
set {
// If the new value is not nil:
if let newValue = newValue {
// Create a new AnyValueWrapper and set its `value` property to
// the new closure
let wrapper = AnyValueWrapper()
wrapper.value = newValue
// Set this wrapper object as an associated object
objc_setAssociatedObject(
self,
&AssociatedKeys.HandlerKey,
wrapper,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
return
}
// If the new value is nil, remove any existing associated object for
// the closure
objc_setAssociatedObject(
self,
&AssociatedKeys.HandlerKey,
nil,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
// Set up the method swizzling when the `UITextField` class is initialized
public override class func initialize() {
struct Static {
static var token: dispatch_once_t = 0
}
// Make sure we are not in a subclass when this method is called
if self !== UITextField.self {
return
}
// Swizzle the canBecomeFirstResponder method.
dispatch_once(&Static.token) {
let originalSelector =
#selector(UITextField.canBecomeFirstResponder)
let swizzledSelector =
#selector(UITextField.xxx_canBecomeFirstResponder)
let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
let didAddMethod =
class_addMethod(self,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(self, swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod))
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
// MARK: - Method Swizzling
// Our swizzled method that replaces the canBecomeFirstResponder
// method of `UITextField`
func xxx_canBecomeFirstResponder() -> Bool {
// Get the default value of canBecomeFirstResponder
let defaultValue = xxx_canBecomeFirstResponder()
// If we have a closure in our associated object:
if let canBecomeFirstResponder = canBecomeFirstResponderHandler {
// Determine if the user interacted with the text field and set
// a flag if so. We do this by checking all gesture recognizers
// of the text field to see if any of them have begun, changed, or
// ended at the time of calling `canBecomeFirstResponder`.
// It's reasonable to assume that if `canBecomeFirstResponder` is
// called when any of these conditions are true, then the text field
// must be trying to become the first responder due to a user
// interaction.
var isFromUserInteraction = false
if let gestureRecognizers = gestureRecognizers {
for gestureRecognizer in gestureRecognizers {
if (gestureRecognizer.state == .Began ||
gestureRecognizer.state == .Changed ||
gestureRecognizer.state == .Ended)
{
isFromUserInteraction = true
break
}
}
}
// Call our closure and pass in the two boolean values,
// then return the result
return canBecomeFirstResponder(
fromUserInteraction: isFromUserInteraction,
defaultValue: defaultValue
)
}
// If we don't have a closure in our associated object,
// just return the original value
return defaultValue
}
}
Then, you can use this closure in your alert controller text field configuration handler like so:
alertController.addTextFieldWithConfigurationHandler { textfield in
textfield.text = "Text"
// Set the closure on the text field. You can use the passed in flags if you
// want or you can simply return fromUserInteraction to only allow user
// interaction to let the text field become the first responder, as is done
// here:
textfield.canBecomeFirstResponderHandler = {
fromUserInteraction, defaultValue in
return fromUserInteraction
}
}
Adding this:
alertController.view.endEditing(true)
right before presenting the alert controller will resign first responder from all text fields and prevent the keyboard from appearing while presenting.