Search code examples
ioscocoa-touch

The textfield in an UIAlertController should not be the first responder


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.


Solution

  • 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
        }
    }
    

    Old solution that no longer works:

    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.