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

前端 未结 11 1776
有刺的猬
有刺的猬 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.getAllSubviews(from: self) as [T] }
        class func getAllSubviews(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.getAllSubviews(from: self) as [T] }
        class func getAllSubviews(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)
        }
    }
    

提交回复
热议问题