How to make a UIScrollView auto scroll when a UITextField becomes a first responder

前端 未结 11 1778
有刺的猬
有刺的猬 2021-01-31 03:24

I\'ve seen posts around here that suggest that UIScrollViews should automatically scroll if a subview UITextField becomes the first responder; however,

相关标签:
11条回答
  • 2021-01-31 03:44

    Solution

    extension UIScrollView {
        func scrollVerticallyToFirstResponderSubview(keyboardFrameHight: CGFloat) {
            guard let firstResponderSubview = findFirstResponderSubview() else { return }
            scrollVertically(toFirstResponder: firstResponderSubview,
                             keyboardFrameHight: keyboardFrameHight, animated: true)
        }
    
        private func scrollVertically(toFirstResponder view: UIView,
                                      keyboardFrameHight: CGFloat, animated: Bool) {
            let scrollViewVisibleRectHeight = frame.height - keyboardFrameHight
            let maxY = contentSize.height - scrollViewVisibleRectHeight
            if contentOffset.y >= maxY { return }
            var point = view.convert(view.bounds.origin, to: self)
            point.x = 0
            point.y -= scrollViewVisibleRectHeight/2
            if point.y > maxY {
                point.y = maxY
            } else if point.y < 0 {
                point.y = 0
            }
            setContentOffset(point, animated: true)
        }
    }
    
    extension UIView {
        func findFirstResponderSubview() -> UIView? { getAllSubviews().first { $0.isFirstResponder } }
        func getAllSubviews<T: UIView>() -> [T] { UIView.getAllSubviews(from: self) as [T] }
        class func getAllSubviews<T: UIView>(from parenView: UIView) -> [T] {
            parenView.subviews.flatMap { subView -> [T] in
                var result = getAllSubviews(from: subView) as [T]
                if let view = subView as? T { result.append(view) }
                return result
            }
        }
    }
    

    Full Sample

    Do not forget to paste the Solution code here

    import UIKit
    
    class ViewController: UIViewController {
    
        private weak var scrollView: UIScrollView!
        private lazy var keyboard = KeyboardNotifications(notifications: [.willHide, .willShow], delegate: self)
        
        override func viewDidLoad() {
            super.viewDidLoad()
            let scrollView = UIScrollView()
            view.addSubview(scrollView)
            scrollView.translatesAutoresizingMaskIntoConstraints = false
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
            scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
            scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
            scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
            scrollView.contentSize = CGSize(width: view.frame.width, height: 1000)
            scrollView.isScrollEnabled = true
            scrollView.indicatorStyle = .default
            scrollView.backgroundColor = .yellow
            scrollView.keyboardDismissMode = .interactive
            self.scrollView = scrollView
            
            addTextField(y: 20)
            addTextField(y: 300)
            addTextField(y: 600)
            addTextField(y: 950)
        }
        
        private func addTextField(y: CGFloat) {
            let textField = UITextField()
            textField.borderStyle = .line
            scrollView.addSubview(textField)
            textField.translatesAutoresizingMaskIntoConstraints = false
            textField.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: y).isActive = true
            textField.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 44).isActive = true
            textField.widthAnchor.constraint(equalToConstant: 120).isActive = true
            textField.heightAnchor.constraint(equalToConstant: 44).isActive = true
        }
        
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            keyboard.isEnabled = true
        }
        
        override func viewWillDisappear(_ animated: Bool) {
            super.viewWillDisappear(animated)
            keyboard.isEnabled = false
        }
    }
    
    extension ViewController: KeyboardNotificationsDelegate {
        func keyboardWillShow(notification: NSNotification) {
            guard   let userInfo = notification.userInfo as? [String: Any],
                    let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
            scrollView.contentInset.bottom = keyboardFrame.height
            scrollView.scrollVerticallyToFirstResponderSubview(keyboardFrameHight: keyboardFrame.height)
        }
    
        func keyboardWillHide(notification: NSNotification) {
            scrollView.contentInset.bottom = 0
        }
    }
    
    /// Solution
    
    extension UIScrollView {
        func scrollVerticallyToFirstResponderSubview(keyboardFrameHight: CGFloat) {
            guard let firstResponderSubview = findFirstResponderSubview() else { return }
            scrollVertically(toFirstResponder: firstResponderSubview,
                             keyboardFrameHight: keyboardFrameHight, animated: true)
        }
    
        private func scrollVertically(toFirstResponder view: UIView,
                                      keyboardFrameHight: CGFloat, animated: Bool) {
            let scrollViewVisibleRectHeight = frame.height - keyboardFrameHight
            let maxY = contentSize.height - scrollViewVisibleRectHeight
            if contentOffset.y >= maxY { return }
            var point = view.convert(view.bounds.origin, to: self)
            point.x = 0
            point.y -= scrollViewVisibleRectHeight/2
            if point.y > maxY {
                point.y = maxY
            } else if point.y < 0 {
                point.y = 0
            }
            setContentOffset(point, animated: true)
        }
    }
    
    extension UIView {
        func findFirstResponderSubview() -> UIView? { getAllSubviews().first { $0.isFirstResponder } }
        func getAllSubviews<T: UIView>() -> [T] { UIView.getAllSubviews(from: self) as [T] }
        class func getAllSubviews<T: UIView>(from parenView: UIView) -> [T] {
            parenView.subviews.flatMap { subView -> [T] in
                var result = getAllSubviews(from: subView) as [T]
                if let view = subView as? T { result.append(view) }
                return result
            }
        }
    }
    
    // https://stackoverflow.com/a/42600092/4488252
    
    import Foundation
    
    protocol KeyboardNotificationsDelegate: class {
        func keyboardWillShow(notification: NSNotification)
        func keyboardWillHide(notification: NSNotification)
        func keyboardDidShow(notification: NSNotification)
        func keyboardDidHide(notification: NSNotification)
    }
    
    extension KeyboardNotificationsDelegate {
        func keyboardWillShow(notification: NSNotification) {}
        func keyboardWillHide(notification: NSNotification) {}
        func keyboardDidShow(notification: NSNotification) {}
        func keyboardDidHide(notification: NSNotification) {}
    }
    
    class KeyboardNotifications {
    
        fileprivate var _isEnabled: Bool
        fileprivate var notifications: [NotificationType]
        fileprivate weak var delegate: KeyboardNotificationsDelegate?
        fileprivate(set) lazy var isKeyboardShown: Bool = false
    
        init(notifications: [NotificationType], delegate: KeyboardNotificationsDelegate) {
            _isEnabled = false
            self.notifications = notifications
            self.delegate = delegate
        }
    
        deinit { if isEnabled { isEnabled = false } }
    }
    
    // MARK: - enums
    
    extension KeyboardNotifications {
    
        enum NotificationType {
            case willShow, willHide, didShow, didHide
    
            var selector: Selector {
                switch self {
                    case .willShow: return #selector(keyboardWillShow(notification:))
                    case .willHide: return #selector(keyboardWillHide(notification:))
                    case .didShow: return #selector(keyboardDidShow(notification:))
                    case .didHide: return #selector(keyboardDidHide(notification:))
                }
            }
    
            var notificationName: NSNotification.Name {
                switch self {
                    case .willShow: return UIResponder.keyboardWillShowNotification
                    case .willHide: return UIResponder.keyboardWillHideNotification
                    case .didShow: return UIResponder.keyboardDidShowNotification
                    case .didHide: return UIResponder.keyboardDidHideNotification
                }
            }
        }
    }
    
    // MARK: - isEnabled
    
    extension KeyboardNotifications {
    
        private func addObserver(type: NotificationType) {
            NotificationCenter.default.addObserver(self, selector: type.selector, name: type.notificationName, object: nil)
        }
    
        var isEnabled: Bool {
            set {
                if newValue {
                    for notificaton in notifications { addObserver(type: notificaton) }
                } else {
                    NotificationCenter.default.removeObserver(self)
                }
                _isEnabled = newValue
            }
    
            get { _isEnabled }
        }
    
    }
    
    // MARK: - Notification functions
    
    extension KeyboardNotifications {
    
        @objc func keyboardWillShow(notification: NSNotification) {
            delegate?.keyboardWillShow(notification: notification)
            isKeyboardShown = true
        }
    
        @objc func keyboardWillHide(notification: NSNotification) {
            delegate?.keyboardWillHide(notification: notification)
            isKeyboardShown = false
        }
    
        @objc func keyboardDidShow(notification: NSNotification) {
            isKeyboardShown = true
            delegate?.keyboardDidShow(notification: notification)
        }
    
        @objc func keyboardDidHide(notification: NSNotification) {
            isKeyboardShown = false
            delegate?.keyboardDidHide(notification: notification)
        }
    }
    
    0 讨论(0)
  • 2021-01-31 03:49

    There is nothing you have to do manually. It is the default behavior. There are two possibilities as to why you are not seeing the behavior

    1. The most likely reason is that the keyboard is covering your UITextField. See below for solution
    2. The other possibility is that you have another UIScrollView somewhere in the view hierarchy between the UITextField and the UIScrollView that you want to auto scroll. This is less likely but can still cause problems.

    For #1, you want to implement something similar to Apple's recommendations for Moving Content That Is Located Under the Keyboard. Note that the code provided by Apple does not account for rotation. For improvements on their code, check out this blog post's implementation of the keyboardDidShow method that properly translates the keyboard's frame using the window.

    0 讨论(0)
  • 2021-01-31 03:49
    - (void)textFieldDidBeginEditing:(UITextField *)textField {
        CGRect rect = [textField bounds];
        rect = [textField convertRect:rect toView:self.scrollView];
        rect.origin.x = 0 ;
        rect.origin.y -= 60 ;
        rect.size.height = 400;
    
        [self.scrollView scrollRectToVisible:rect animated:YES];
    }
    
    0 讨论(0)
  • 2021-01-31 03:53

    I know this question has already been answered, but I thought I would share the code combination that I used from @Adeel and @Basil answer, as it seems to work perfectly for me on iOS 9.

    -(void)textFieldDidBeginEditing:(UITextField *)textField {
    
        // Scroll to the text field so that it is
        // not hidden by the keyboard during editing.
        [scroll setContentOffset:CGPointMake(0, (textField.superview.frame.origin.y + (textField.frame.origin.y))) animated:YES];
    }
    
    -(void)textFieldDidEndEditing:(UITextField *)textField {
    
        // Remove any content offset from the scroll
        // view otherwise the scroll view will look odd.
        [scroll setContentOffset:CGPointMake(0, 0) animated:YES];
    }
    

    I also used the animated method, it makes for a much smoother transition.

    0 讨论(0)
  • 2021-01-31 03:58

    As Michael McGuire mentioned in his point #2 above, the system's default behavior misbehaves when the scroll view contains another scroll view between the text field and the scroll view. I've found that the misbehavior also occurs when there's a scroll view merely next to the text field (both embedded in the scroll view that needs to be adjusted to bring the text field into view when the text field wants to start editing. This is on iOS 12.1.

    But my solution is different from the above. In my top-level scroll view, which is sub-classed so I can add properties and override methods, I override scrollRectToVisible:animated:. It simply calls its [super scrollRectToVisible:animated:] unless there's a property set that tells it to adjust the rect passed in, which is the frame of the text field. When the property is non-nil, it is a reference to the UITextField in question, and the rect is adjusted so that the scroll view goes further than the system thought it would. So I put this in the UIScrollView's sub-classed header file:

    @property (nullable) UITextField *textFieldToBringIntoView;
    

    (with appropriate @synthesize textFieldToBringIntoView; in the implementation. Then I added this override method to the implementation:

    - (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)how
    {
        if (textFieldToBringIntoView) {
           // Do whatever mucking with `rect`'s origin needed to make it visible
           // based on context or its spatial relationship with the other
           // view that the system is getting confused by.
    
           textFieldToBringIntoView = nil;        // Go back to normal
           }
        [super scrollRectToVisible:rect animated:how];
    }
    

    In the delegate method for the UITextField for when it's about to begin editing, just set textFieldToBringIntoView to the textField in question:

    - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField
    {
        // Ensure it scrolls into view so that keyboard doesn't obscure it
        // The system is about to call |scrollRectIntoView:| for the scrolling
        // superview, but the system doesn't get things right in certain cases.
    
        UIScrollView *parent = (UIScrollView *)textField.superview;
        // (or figure out the parent UIScrollView some other way)
    
        // Tell the override to do something special just once
        // based on this text field's position in its parent's scroll view.
        parent.textFieldToBringIntoView = textField;
        // The override function will set this back to nil
    
        return(YES);
    }
    

    It seems to work. And if Apple fixes their bug, it seems like it might still work (fingers crossed).

    0 讨论(0)
提交回复
热议问题