当UITextField成为第一响应者时,如何使UIScrollView自动滚动

时间:2012-10-24 19:05:18

标签: iphone objective-c ios uiscrollview uitextfield

我看到here周围的帖子表明如果子视图UIScrollViews成为第一个响应者,UITextField应自动滚动;但是,我无法弄清楚如何让它发挥作用。

我所拥有的UIViewController UIScrollView UIScrollView且{{1}}内有多个文本字段。

我知道如何在必要时手动执行此操作;但是,从我一直在阅读的内容来看,似乎可以让它自动滚动。请帮助。

11 个答案:

答案 0 :(得分:32)

我希望这个例子可以帮到你 您可以通过此代码滚动到任意点。

scrollView.contentOffset = CGPointMake(0,0);

因此,如果你有文本字段,它必须在视图上有一些x,y位置,所以你可以使用

CGPoint point = textfield.frame.origin ;
scrollView.contentOffset = point 

这应该可以解决问题,

但如果您不知道何时调用此代码,那么您应该学习 UITextFieldDelegate 方法

在代码中实现此方法

- (void)textFieldDidBeginEditing:(UITextField *)textField {
// Place Scroll Code here
}

我希望你知道如何使用委托方法。

答案 1 :(得分:11)

我知道这个问题已经得到了解答,但我想我会分享我在@Adeel和@Basil中使用的代码组合,因为它似乎在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];
}

我还使用了动画方法,它可以实现更平滑的过渡。

答案 2 :(得分:6)

您无需手动操作。这是默认行为。有两种可能性,为什么你没有看到行为

  1. 最可能的原因是键盘覆盖了UITextField。请参阅下面的解决方案
  2. 另一种可能性是,在您要自动滚动的UIScrollViewUITextField之间的视图层次结构中的某处有另一个UIScrollView。这种可能性较小,但仍可能导致问题。
  3. 对于#1,您希望实现类似于Apple对Moving Content That Is Located Under the Keyboard的建议。请注意,Apple提供的代码不考虑轮换。有关其代码的改进,请查看使用该窗口正确翻译键盘框架的keyboardDidShow方法的this blog post's实现。

答案 3 :(得分:4)

- (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];
}

答案 4 :(得分:3)

您可以将此功能用于UITextField的autoScroll

on UITextFieldDelegate

- (void)textFieldDidBeginEditing:(UITextField *)textField {

[self autoScrolTextField:textField onScrollView:self.scrollView];
}




- (void) autoScrolTextField: (UITextField *) textField onScrollView: (UIScrollView *) scrollView { 
 float slidePoint = 0.0f;
float keyBoard_Y_Origin = self.view.bounds.size.height - 216.0f;
float textFieldButtomPoint = textField.superview.frame.origin.y + (textField.frame.origin.y + textField.frame.size.height);

if (keyBoard_Y_Origin < textFieldButtomPoint - scrollView.contentOffset.y) {
    slidePoint = textFieldButtomPoint - keyBoard_Y_Origin + 10.0f;
    CGPoint point = CGPointMake(0.0f, slidePoint);
    scrollView.contentOffset = point;
}

修改

我现在正在使用IQKeyboardManager 感谢开发者,你需要尝试这个。

答案 5 :(得分:2)

以下是@ Supertecnoboff回答的Swift 4更新。它对我很有用。

func textFieldDidBeginEditing(_ textField: UITextField) {
    scroll.setContentOffset(CGPoint(x: 0, y: (textField.superview?.frame.origin.y)!), animated: true)
}

func textFieldDidEndEditing(_ textField: UITextField) {
    scroll.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}

确保扩展UITextFieldDelegate并将textfields的委托设置为self。

答案 6 :(得分:1)

如果您有多个文本字段说Textfield1Textfield2Textfield3,并且您希望在textfield2成为第一响应者时沿y轴滚动滚动视图:

if([Textfield2 isFirstResponder])
{
    scrollView.contentOffset = CGPointMake(0,yourY);
} 

答案 7 :(得分:1)

解决方案

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

完整样本

不要忘记在此处粘贴解决方案代码

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

答案 8 :(得分:0)

正如Michael McGuire在上面的第2点中提到的那样,当滚动视图在文本字段和滚动视图之间包含另一个滚动视图时,系统的默认行为不当。我发现,当仅在文本字段的 next 下有一个滚动视图时,也会发生不当行为(两者都嵌入在滚动视图中,需要进行调整,以便在文本字段出现时可以看到文本字段想开始编辑。这是在iOS 12.1上。

但是我的解决方案与上述不同。在子类的顶层滚动视图中,因此我可以添加属性和覆盖方法,因此我覆盖scrollRectToVisible:animated:。除非有一个属性集告诉它调整传入的[super scrollRectToVisible:animated:](即文本字段的框架),否则它仅调用其rect。当该属性为非nil时,它是对所讨论的UITextField的引用,并且对rect进行了调整,以使滚动视图比系统认为的更远。因此,我将其放入UIScrollView的子类头文件中:

@property (nullable) UITextField *textFieldToBringIntoView;

(在实现中使用适当的@synthesize textFieldToBringIntoView;。然后,我将此替代方法添加到实现中:

- (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];
}

在即将开始编辑的UITextField的委托方法中,只需将textFieldToBringIntoView设置为有问题的textField

- (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);
}

似乎可行。而且,如果Apple修复了他们的错误,看来它仍然可以工作(手指交叉)。

答案 9 :(得分:0)

基于Vasily Bodnarchuk的回答,我创建了一个具有简单协议的要点,您可以实现该协议,它将为您完成所有工作。 您需要做的就是调用registerAsTextDisplacer()

我在项目中创建了一个BaseViewController并实现了它

https://gist.github.com/CameronPorter95/cb68767f5f8052fdc70293c167e9430e

答案 10 :(得分:0)

我看到的其他解决方案是,让您将偏移量设置为textField的原点,但这会使滚动视图超出其范围。 我对偏移量做了此调整,以不超出底部偏移量或顶部偏移量。

keyboardHeightConstraint设置在页面底部。 当键盘显示时,将其约束的常数更新为负的键盘高度。 然后滚动到responderField,如下所示。

@IBOutlet var keyboardHeightConstraint: NSLayoutConstraint?
var responderField: String?

@objc func keyboardNotification(notification: NSNotification) {
     guard let keyboardValue = notification.userInfo [UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
     let keyboardHeight = keyboardValue.cgRectValue.height

     keyboardHeightConstraint?.constant = -keyboardHeight
     scroll(field: responderField!)
}

func textFieldDidBeginEditing(_ textField: UITextField) {
     responderField = textField
}

现在,我们要确保滚动的幅度不会大于底部偏移量,也不小于顶部偏移量。 同时,我们要计算字段的maxY值的偏移量。 为此,我们从scrollView.bounds.size.height值中减去maxY

let targetOffset = field.frame.maxY - scrollView.bounds.size.height

我发现滚动键盘高度的额外距离更好,但是如果要在字段下方滚动,则可以忽略。

let targetOffset = keyboardHeight + field.frame.maxY - scrollView.bounds.size.height

如果标签栏可见,请记住添加scrollView.contentInset.bottom

func scroll(field: UITextField) {
        guard let keyboardConstraintsConstant = keyboardHeightConstraint?.constant else { return }
        let keyboardHeight = -keyboardConstraintsConstant
        
        view.layoutIfNeeded()
        let bottomOffset = scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.bottom
        let topOffset = -scrollView.safeAreaInsets.top
        let targetOffset = keyboardHeight + field.frame.maxY + scrollView.contentInset.bottom - scrollView.bounds.size.height
        let adjustedOffset = targetOffset > bottomOffset ? bottomOffset : (targetOffset < topOffset ? topOffset : targetOffset)
        scrollView.setContentOffset(CGPoint(x: 0, y: adjustedOffset), animated: true)
}