Get the current first responder without using a private API

前端 未结 28 1827
夕颜
夕颜 2020-11-22 05:03

I submitted my app a little over a week ago and got the dreaded rejection email today. It tells me that my app cannot be accepted because I\'m using a non-public API; specif

相关标签:
28条回答
  • 2020-11-22 05:39

    For a Swift 3 & 4 version of nevyn's answer:

    UIApplication.shared.sendAction(#selector(UIView.resignFirstResponder), to: nil, from: nil, for: nil)
    
    0 讨论(0)
  • 2020-11-22 05:41

    I have a slightly different approach than most. Rather than iterate through the collection of views looking for the one that has isFirstResponder set, I too send a message to nil, but I store the receiver of the message so I can return it and do whatever I wish with it.

    import UIKit
    
    private var _foundFirstResponder: UIResponder? = nil
    
    extension UIResponder {
    
        static var first:UIResponder? {
    
            // Sending an action to 'nil' implicitly sends it to the first responder
            // where we simply capture it and place it in the _foundFirstResponder variable.
            // As such, the variable will contain the current first responder (if any) immediately after this line executes
            UIApplication.shared.sendAction(#selector(UIResponder.storeFirstResponder(_:)), to: nil, from: nil, for: nil)
    
            // The following 'defer' statement runs *after* this getter returns,
            // thus releasing any strong reference held by the variable immediately thereafter
            defer {
                _foundFirstResponder = nil
            }
    
            // Return the found first-responder (if any) back to the caller
            return _foundFirstResponder
        }
    
        @objc func storeFirstResponder(_ sender: AnyObject) {
    
            // Capture the recipient of this message (self), which is the first responder
            _foundFirstResponder = self
        }
    }
    

    With the above, I can resign the first responder by simply doing this...

    UIResponder.first?.resignFirstResponder()
    

    But since my API actually hands back whatever the first responder is, I can do whatever I want with it.

    Here's an example that checks if the current first responder is a UITextField with a helpMessage property set, and if so, shows it in a help bubble right next to the control. We call this from a 'Quick Help' button on our screen.

    func showQuickHelp(){
    
        if let textField   = UIResponder?.first as? UITextField,
           let helpMessage = textField.helpMessage {
        
            textField.showHelpBubble(with:helpMessage)
        }
    }
    

    The support for the above is defined in an extension on UITextField like so...

    extension UITextField {
        var helpMessage:String? { ... }
        func showHelpBubble(with message:String) { ... }
    }
    

    Now to support this feature, all we have to do is decide which text fields have help messages and the UI takes care of the rest for us.

    0 讨论(0)
  • 2020-11-22 05:42

    Peter Steinberger just tweeted about the private notification UIWindowFirstResponderDidChangeNotification, which you can observe if you want to watch the firstResponder change.

    0 讨论(0)
  • 2020-11-22 05:43

    If you just need to kill the keyboard when the user taps on a background area why not add a gesture recognizer and use it to send the [[self view] endEditing:YES] message?

    you can add the Tap gesture recogniser in the xib or storyboard file and connect it to an action,

    looks something like this then finished

    - (IBAction)displayGestureForTapRecognizer:(UITapGestureRecognizer *)recognizer{
         [[self view] endEditing:YES];
    }
    
    0 讨论(0)
  • 2020-11-22 05:44

    Here is a Extension implemented in Swift based on Jakob Egger's most excellent answer:

    import UIKit
    
    extension UIResponder {
        // Swift 1.2 finally supports static vars!. If you use 1.1 see: 
        // http://stackoverflow.com/a/24924535/385979
        private weak static var _currentFirstResponder: UIResponder? = nil
        
        public class func currentFirstResponder() -> UIResponder? {
            UIResponder._currentFirstResponder = nil
            UIApplication.sharedApplication().sendAction("findFirstResponder:", to: nil, from: nil, forEvent: nil)
            return UIResponder._currentFirstResponder
        }
        
        internal func findFirstResponder(sender: AnyObject) {
            UIResponder._currentFirstResponder = self
        }
    }
    

    Swift 4

    import UIKit    
    
    extension UIResponder {
        private weak static var _currentFirstResponder: UIResponder? = nil
        
        public static var current: UIResponder? {
            UIResponder._currentFirstResponder = nil
            UIApplication.shared.sendAction(#selector(findFirstResponder(sender:)), to: nil, from: nil, for: nil)
            return UIResponder._currentFirstResponder
        }
        
        @objc internal func findFirstResponder(sender: AnyObject) {
            UIResponder._currentFirstResponder = self
        }
    }
    
    0 讨论(0)
  • 2020-11-22 05:45

    Here's a solution which reports the correct first responder (many other solutions won't report a UIViewController as the first responder, for example), doesn't require looping over the view hierarchy, and doesn't use private APIs.

    It leverages Apple's method sendAction:to:from:forEvent:, which already knows how to access the first responder.

    We just need to tweak it in 2 ways:

    • Extend UIResponder so it can execute our own code on the first responder.
    • Subclass UIEvent in order to return the first responder.

    Here is the code:

    @interface ABCFirstResponderEvent : UIEvent
    @property (nonatomic, strong) UIResponder *firstResponder;
    @end
    
    @implementation ABCFirstResponderEvent
    @end
    
    @implementation UIResponder (ABCFirstResponder)
    - (void)abc_findFirstResponder:(id)sender event:(ABCFirstResponderEvent *)event {
        event.firstResponder = self;
    }
    @end
    
    @implementation ViewController
    
    + (UIResponder *)firstResponder {
        ABCFirstResponderEvent *event = [ABCFirstResponderEvent new];
        [[UIApplication sharedApplication] sendAction:@selector(abc_findFirstResponder:event:) to:nil from:nil forEvent:event];
        return event.firstResponder;
    }
    
    @end
    
    0 讨论(0)
提交回复
热议问题