【问题标题】:How to make a UIScrollView auto scroll when a UITextField becomes a first responder当 UITextField 成为第一响应者时如何使 UIScrollView 自动滚动
【发布时间】:2012-10-14 21:27:08
【问题描述】:

我看到here 周围的帖子表明,如果子视图UITextField 成为第一响应者,UIScrollViews 应该自动滚动;但是,我不知道如何让它工作。

我有一个UIViewController,它有一个UIScrollView,在UIScrollView 中有多个文本字段。

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

【问题讨论】:

  • 我相信他们所说的自动滚动是默认行为。例如,如果您在键盘所在的视图底部附近选择了一个文本字段(但还没有),那么该文本字段会自动滚动,因此当键盘向上滑动时,它就在键盘上方可见。我相信如果您以编程方式选择当前不在屏幕上的文本字段,例如 [textview1 becomeFirstResponder];

标签: iphone objective-c ios uiscrollview uitextfield


【解决方案1】:

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

scrollView.contentOffset = CGPointMake(0,0);

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

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

这应该可以解决问题,

但是如果你不知道什么时候调用这段代码,那么你应该学习UITextFieldDelegate方法

在你的代码中实现这个方法

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

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

【讨论】:

  • 问题是如何实现自动滚动,而不是如何手动实现。
  • 我会使用 [self.scrollView setContentOffset:point animated:YES] 来使过渡更流畅:)
  • @ChrisBorg iOS 7 及更高版本在执行此操作时似乎并没有真正滚动。所以这并没有真正达到预期的效果。
  • 执行[scroll setContentOffset:CGPointMake(0, point.y) animated:YES]; 以使滚动视图为偏移设置动画。
  • @AdeelPervaiz 不过还有一个问题。如果您将滚动视图设置为仅滚动一小段距离怎么办?那么这将不起作用,因为它无法正确滚动?比如[scroll setContentSize:CGSizeMake(320, 690)]; 那么如果point.y大于690,那么就不行了???
【解决方案2】:

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

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

【讨论】:

  • 你为什么要打扰textField1/2/3..等?如果您使用的是textFieldDidBeginEditing:(UITextField *)textField 等委托方法之一,那么您只需使用textField
【解决方案3】:

UITextField的autoScroll可以使用这个功能

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 感谢这个开发者,你需要试试这个。

【讨论】:

  • 使用 216.0 作为键盘高度的常数是个坏主意
  • @George 同意,尤其是我们现在如何在 iOS 中使用自定义键盘。
【解决方案4】:

您无需手动执行任何操作。这是默认行为。关于您没有看到该行为的原因有两种可能性

  1. 最可能的原因是键盘覆盖了您的UITextField。解决方案见下文
  2. 另一种可能性是您在要自动滚动的UITextFieldUIScrollView 之间的视图层次结构中的某个位置有另一个UIScrollView。这不太可能,但仍会导致问题。

对于 #1,您希望实现类似于 Apple 对Moving Content That Is Located Under the Keyboard 的建议。请注意,Apple 提供的代码不考虑轮换。要改进他们的代码,请查看 keyboardDidShow 方法的 this blog post's 实现,该方法使用窗口正确转换键盘的框架。

【讨论】:

  • 如果确定滚动视图内容大小的约束不明确,也可能发生这种情况。由于我的滚动视图只垂直滚动,我为滚动视图的contentLayoutGuide 设置的唯一约束是topAnchorbottomAnchor。为前导和尾随锚添加约束使内容大小完全明确,并为我解决了这个问题。
【解决方案5】:
- (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];
}

【讨论】:

    【解决方案6】:

    我知道这个问题已经得到解答,但我想我会分享我从 @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];
    }
    

    我也使用了动画方法,它使过渡更加平滑。

    【讨论】:

    • 非常适合我。谢谢!我浪费了大量时间试图根据出现的键盘来获得解决方案,然后搜索哪个 UITextView 触发了它。此解决方案避免了该搜索。
    • 我发现通过这个解决方案,如果UITextField 在滚动视图中并且您滚动,它会将 contentOffset 重置为默认值,基本上将文本字段隐藏在键盘后面。跨度>
    【解决方案7】:

    这是@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 并将文本字段的委托设置为 self。

    【讨论】:

    • 确认中。在我的项目中使用 Swift 5 效果很好。
    【解决方案8】:

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

    但我的解决方案与上述不同。在我的顶级滚动视图中,它是子类的,因此我可以添加属性和覆盖方法,我覆盖scrollRectToVisible:animated:。它只是简单地调用它的[super scrollRectToVisible:animated:],除非有一个属性集告诉它调整传入的rect,它是文本字段的框架。当属性为非零时,它是对有问题的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】:

      解决方案

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

      【讨论】:

      • 这不会是低效的表现吗?既然是遍历所有子视图来寻找第一响应者?
      【解决方案10】:

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

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

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

      【讨论】:

        【解决方案11】:

        我看到的其他解决方案,让您将偏移量设置为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 值的偏移量。 为此,我们从maxY 值中减去scrollView.bounds.size.height

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

        【讨论】:

          猜你喜欢
          • 2011-06-02
          • 2011-06-25
          • 2018-02-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多