iPhone X如何处理View Controller inputAccessoryView?

iPhone X how to handle View Controller inputAccessoryView?

我有一个消息传递应用程序,它在全屏 table 视图的底部具有典型的 UI 文本字段设计。我将该文本字段设置为视图控制器的 inputAccessoryView 并调用 ViewController.becomeFirstResponder() 以使该字段显示在屏幕底部。

我知道这是 Apple 推荐的实现此 UI 结构的方法,它在 "classic" 设备上完美运行,但是当我在 iPhone X 模拟器上测试时,我注意到使用这种方法,文本字段不尊重新 "safe areas"。文本字段呈现在主屏幕指示器下方屏幕的最底部。

我查看了 HIG 文档,但没有发现任何关于视图控制器上的 inputAccessoryView 的有用信息。

这很困难,因为使用这种方法我实际上并不能直接控制任何约束,我只是设置 inputAccessoryView 并让视图控制器从那里处理 UI .所以我不能只将字段限制在新的安全区域。

有没有人找到这方面的好文档或知道在 iPhone X 上运行良好的替代方法?

我只是将安全区域添加到 inputAccessoryView(Xcode 处的复选框)。并更改底部 space 约束等于安全区域底部而不是 inputAccessoryView 根视图底部。

Constraint

And result

这似乎是一个 iOS 错误,并且有一个 rdar 问题:inputAccessoryViews should respect safe area inset with external keyboard on iPhone X

我想这应该在 iOS 更新中修复,当 iPhone X 出现时。

这是 iPhone X 上 inputAccessoryViews 的一个普遍问题。 inputAccessoryView 忽略其 window.

的 safeAreaLayoutGuides

要修复它,我们必须在视图移动到其 window 时在您的 class 中手动添加约束:

override func didMoveToWindow() {
    super.didMoveToWindow()
    if #available(iOS 11.0, *) {
        if let window = self.window {
            self.bottomAnchor.constraintLessThanOrEqualToSystemSpacingBelow(window.safeAreaLayoutGuide.bottomAnchor, multiplier: 1.0).isActive = true
        }
    }
}

PS: 这里的self指的是inputAccessoryView。

我在这里写的很详细:http://ahbou.org/post/165762292157/iphone-x-inputaccessoryview-fix

直到安全插图由 iOS 自动引导,简单的解决方法是将您的附件包装在容器视图中并在附件视图和容器视图之间设置底部 space 约束以匹配安全区域插图window。

注意:当然,当 iOS 更新修复了附件视图的底部间距时,此变通方法可以将附件视图距底部的间距加倍。

例如

- (void) didMoveToWindow {
    [super didMoveToWindow];
    if (@available(iOS 11.0, *)) {
        self.bottomSpaceConstraint.constant = self.window.safeAreaInsets.bottom;
    }
}

inputAccessoryView 和 iPhone X

上的安全区域
  • 当键盘不可见时,inputAccessoryView 固定在屏幕的最底部。没有办法解决这个问题,我认为这是预期的行为。

  • 视图的 layoutMarginsGuide (iOS 9+) 和 safeAreaLayoutGuide (iOS 11) 属性设置为 inputAccessoryView都尊重安全区域,即 iPhone X :

    • 当键盘不可见时,bottomAnchor占主页按钮区域
    • 当显示键盘时,bottomAnchor位于inputAccessoryView的底部,因此在键盘
    • 上方没有留下无用的space

工作示例:

import UIKit

class ViewController: UIViewController {

    override var canBecomeFirstResponder: Bool { return true }

    var _inputAccessoryView: UIView!

    override var inputAccessoryView: UIView? {

        if _inputAccessoryView == nil {

            _inputAccessoryView = CustomView()
            _inputAccessoryView.backgroundColor = UIColor.groupTableViewBackground

            let textField = UITextField()
            textField.borderStyle = .roundedRect

            _inputAccessoryView.addSubview(textField)

            _inputAccessoryView.autoresizingMask = .flexibleHeight

            textField.translatesAutoresizingMaskIntoConstraints = false

            textField.leadingAnchor.constraint(
                equalTo: _inputAccessoryView.leadingAnchor,
                constant: 8
            ).isActive = true

            textField.trailingAnchor.constraint(
                equalTo: _inputAccessoryView.trailingAnchor,
                constant: -8
            ).isActive = true

            textField.topAnchor.constraint(
                equalTo: _inputAccessoryView.topAnchor,
                constant: 8
            ).isActive = true

            // this is the important part :

            textField.bottomAnchor.constraint(
                equalTo: _inputAccessoryView.layoutMarginsGuide.bottomAnchor,
                constant: -8
            ).isActive = true
        }

        return _inputAccessoryView
    }

    override func loadView() {

        let tableView = UITableView()
        tableView.keyboardDismissMode = .interactive

        view = tableView
    }
}

class CustomView: UIView {

    // this is needed so that the inputAccesoryView is properly sized from the auto layout constraints
    // actual value is not important

    override var intrinsicContentSize: CGSize {
        return CGSize.zero
    }
}

See the result here

我刚刚创建了一个名为 SafeAreaInputAccessoryViewWrapperView 的快速 CocoaPod 来解决这个问题。它还使用自动布局约束动态设置包装视图的高度,因此您不必手动设置框架。支持 iOS 9+.

使用方法如下:

  1. 使用 SafeAreaInputAccessoryViewWrapperView(for:):

    包装任何 UIView/UIButton/UILabel/etc
    SafeAreaInputAccessoryViewWrapperView(for: button)
    
  2. 在你的某处存储对此的引用 class:

    let button = UIButton(type: .system)
    
    lazy var wrappedButton: SafeAreaInputAccessoryViewWrapperView = {
        return SafeAreaInputAccessoryViewWrapperView(for: button)
    }()
    
  3. Return inputAccessoryView中的参考:

    override var inputAccessoryView: UIView? {
        return wrappedButton
    }
    
  4. (可选)始终显示 inputAccessoryView,即使键盘关闭时也是如此:

    override var canBecomeFirstResponder: Bool {
        return true
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        becomeFirstResponder()
    }
    

祝你好运!

在Xib中,在你设计的底部找到一个右约束,并将item设置为Safe Area而不是Superview:

之前

修复

之后

-- 对于那些使用 JSQMessagesViewController 库的人 --

我提议基于 JSQ 最新 develop 分支提交的固定分支。

它正在使用 didMoveToWindow 解决方案(我相信来自@jki?)。不理想,但在等待 Apple 关于 inputAccessoryView 的安全区域布局指南附件或任何其他更好的修复的答案时值得尝试。

您可以将其添加到您的 Podfile 中,替换之前的 JSQ 行:

pod 'JSQMessagesViewController', :git => 'https://github.com/Tulleb/JSQMessagesViewController.git', :branch => 'develop', :inhibit_warnings => true

来自代码 (Swift 4)。想法 - 监控 layoutMarginsDidChange 事件并调整 intrinsicContentSize.

public final class AutoSuggestionView: UIView {

   private lazy var tableView = UITableView(frame: CGRect(), style: .plain)
   private var bottomConstraint: NSLayoutConstraint?
   var streetSuggestions = [String]() {
      didSet {
         if streetSuggestions != oldValue {
            updateUI()
         }
      }
   }
   var handleSelected: ((String) -> Void)?

   public override func initializeView() {
      addSubview(tableView)
      setupUI()
      setupLayout()
      // ...
      updateUI()
   }

   public override var intrinsicContentSize: CGSize {
      let size = super.intrinsicContentSize
      let numRowsToShow = 3
      let suggestionsHeight = tableView.rowHeight * CGFloat(min(numRowsToShow, tableView.numberOfRows(inSection: 0)))
      //! Explicitly used constraint instead of layoutMargins
      return CGSize(width: size.width,
                    height: suggestionsHeight + (bottomConstraint?.constant ?? 0))
   }

   public override func layoutMarginsDidChange() {
      super.layoutMarginsDidChange()
      bottomConstraint?.constant = layoutMargins.bottom
      invalidateIntrinsicContentSize()
   }
}

extension AutoSuggestionView {

   private func updateUI() {
      backgroundColor = streetSuggestions.isEmpty ? .clear : .white
      invalidateIntrinsicContentSize()
      tableView.reloadData()
   }

   private func setupLayout() {

      let constraint0 = trailingAnchor.constraint(equalTo: tableView.trailingAnchor)
      let constraint1 = tableView.leadingAnchor.constraint(equalTo: leadingAnchor)
      let constraint2 = tableView.topAnchor.constraint(equalTo: topAnchor)
      //! Used bottomAnchor instead of layoutMarginGuide.bottomAnchor
      let constraint3 = bottomAnchor.constraint(equalTo: tableView.bottomAnchor)
      bottomConstraint = constraint3
      NSLayoutConstraint.activate([constraint0, constraint1, constraint2, constraint3])
   }
}

用法:

let autoSuggestionView = AutoSuggestionView()
// ...
textField.inputAccessoryView = autoSuggestionView

结果:

如果您已经通过 nib 文件加载了自定义视图。

像这样添加一个方便的构造函数:

convenience init() {
    self.init(frame: .zero)
    autoresizingMask = .flexibleHeight
}

并覆盖 intrinsicContentSize:

override var intrinsicContentSize: CGSize {
    return .zero
}

In the nib set the first bottom constraint (of views that should stay above the safe area) to safeArea and the second one to superview with lower priority so it can be satisfied on older iOS.

只需为 JSQMessagesInputToolbar 添加一个扩展

extension JSQMessagesInputToolbar {
    override open func didMoveToWindow() {
        super.didMoveToWindow()
        if #available(iOS 11.0, *) {
            if self.window?.safeAreaLayoutGuide != nil {
            self.bottomAnchor.constraintLessThanOrEqualToSystemSpacingBelow((self.window?.safeAreaLayoutGuide.bottomAnchor)!,
                                                                            multiplier: 1.0).isActive = true
            }
        }
     }
}

重复:

我刚刚创建了一个支持 iPhone X 的 project on Github。它符合新的安全区域布局指南。使用:

autoresizingMask = [.flexibleHeight]

截图:

最简单的答案(一行代码)

只需使用继承自 UIToolbar 而不是 UIView 的自定义视图作为您的 inputAccessoryView

另外不要忘记将该自定义视图的自动调整大小掩码设置为 UIViewAutoresizingFlexibleHeight

就是这样。稍后谢谢我。

对我有效但没有变通办法的解决方案:

我正在使用 UIInputViewController 通过覆盖 inputAccessoryViewController 属性 而不是 "main" 视图控制器中的 inputAccessoryView 来提供输入附件视图。

UIInputViewControllerinputView 设置为我的自定义输入视图(UIInputView 的子类)。

真正对我有用的是将我的 UIInputViewallowsSelfSizing 属性 设置为 true。输入视图中的约束使用安全区域,并以定义总视图高度的方式设置(类似于自动调整 table 视图单元格)。

经过大量研究和试验,这似乎是 尝试将 UIToolbar 与文本输入 inputAccessoryView 一起使用的有效解决方案。 (现有的大多数解决方案都是使用固定的附件视图,而不是将其分配给文本视图(并在键盘关闭时将其隐藏)。)

代码的灵感来自 。基本上,我们首先创建一个具有工具栏子视图的自定义视图:

class CustomInputAccessoryWithToolbarView: UIView {
    public var toolbar: UIToolbar!

    override init(frame: CGRect) {
        super.init(frame: frame)

        // 
        toolbar = UIToolbar(frame: frame)

        // Below is adopted from 
        self.addSubview(toolbar)

        self.autoresizingMask = .flexibleHeight

        toolbar.translatesAutoresizingMaskIntoConstraints = false
        toolbar.leadingAnchor.constraint(
            equalTo: self.leadingAnchor,
            constant: 0
        ).isActive = true
        toolbar.trailingAnchor.constraint(
            equalTo: self.trailingAnchor,
            constant: 0
        ).isActive = true
        toolbar.topAnchor.constraint(
            equalTo: self.topAnchor,
            constant: 0
        ).isActive = true
        // This is the important part:
        if #available(iOS 11.0, *) {
            toolbar.bottomAnchor.constraint(
                equalTo: self.safeAreaLayoutGuide.bottomAnchor,
                constant: 0
            ).isActive = true
        } else {
            toolbar.bottomAnchor.constraint(
                equalTo: self.layoutMarginsGuide.bottomAnchor,
                constant: 0
            ).isActive = true
        }
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // 
    // This is needed so that the inputAccesoryView is properly sized from the auto layout constraints.
    // Actual value is not important.
    override var intrinsicContentSize: CGSize {
        return CGSize.zero
    }
}

然后您可以将其设置为 inputAccessoryView 用于正常的文本输入:(您应该指定帧大小以避免在 UIToolbar with UIBarButtonItem LayoutConstraint issue 中看到的警告)

let myAccessoryView = CustomInputAccessoryWithToolbarView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 44))
textView.inputAccessoryView = myAccessoryView

当你想与工具栏交互时(例如,在工具栏上设置项目),你可以简单地引用toolbar变量:

myAccessoryView.toolbar.setItems(myToolbarItems, animated: true)

演示:(在模拟器中使用硬件键盘/Command+K)

之前:

之后: