iPhone X, как обращаться с View Controller inputAccessoryView?

У меня есть приложение для обмена сообщениями, которое имеет типичный дизайн пользовательского интерфейса текстового поля в нижней части полноэкранного просмотра таблицы. Я устанавливаю это текстовое поле, чтобы быть контроллером представления inputAccessoryView и звонит ViewController.becomeFirstResponder() для того, чтобы получить поле для отображения в нижней части экрана.

Я понимаю, что Apple рекомендует этот способ создания структуры пользовательского интерфейса, и он отлично работает на "классических" устройствах, однако, когда я тестирую на симуляторе iPhone X, я замечаю, что при таком подходе текстовое поле не учитывает новые "безопасные области"., Текстовое поле отображается в самом низу экрана под индикатором главного экрана.

Я просмотрел документы HIG, но не нашел ничего полезного в отношении inputAccessoryView на контроллере представления.

Это сложно, потому что, используя этот подход, я фактически не контролирую ни одно из ограничений напрямую, я просто устанавливаю inputAccessoryView и позволяя контроллеру представления обрабатывать пользовательский интерфейс оттуда. Поэтому я не могу просто ограничить поле новыми безопасными районами.

Кто-нибудь нашел хорошую документацию по этому вопросу или знает альтернативный подход, который хорошо работает на iPhone X?

16 ответов

Решение

inputAccessoryView и безопасная зона на iPhone X

  • когда клавиатура не видна, inputAccessoryView закреплен в самом низу экрана. Обойти это невозможно, и я думаю, что это намеренное поведение.

  • layoutMarginsGuide (iOS 9+) и safeAreaLayoutGuide (iOS 11) свойства представления установлены как inputAccessoryView оба уважают безопасную зону, т.е. на iPhone X:

    • когда клавиатура не видна, bottomAnchor счета для области кнопки домой
    • когда клавиатура отображается, bottomAnchor находится в нижней части inputAccessoryView, так что он не оставляет бесполезного пространства над клавиатурой

Рабочий пример:

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

Смотрите результат здесь

Это общая проблема с inputAccessoryViews на iPhone X. InputAccessoryView игнорирует safeAreaLayoutGuides своего окна.

Чтобы исправить это, вы должны вручную добавить ограничение в ваш класс, когда представление перемещается в свое окно:

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

В Xib найдите правильное ограничение в нижней части вашего дизайна и установите для элемента значение Safe Area вместо Superview:

До:

Исправить:

После:

Я только что создал быстрый CocoaPod с именем https://cocoapods.org/pods/SafeAreaInputAccessoryViewWrapperView, чтобы исправить это. Он также динамически устанавливает высоту упакованного представления, используя ограничения автоматического размещения, поэтому вам не нужно вручную устанавливать фрейм. Поддерживает iOS 9+.

Вот как это использовать:

  1. Оберните любой UIView/UIButton/UILabel/ и т. Д., Используя SafeAreaInputAccessoryViewWrapperView(for:):

    SafeAreaInputAccessoryViewWrapperView(for: button)
    
  2. Храните ссылку на это где-то в вашем классе:

    let button = UIButton(type: .system)
    
    lazy var wrappedButton: SafeAreaInputAccessoryViewWrapperView = {
        return SafeAreaInputAccessoryViewWrapperView(for: button)
    }()
    
  3. Вернуть ссылку в inputAccessoryView:

    override var inputAccessoryView: UIView? {
        return wrappedButton
    }
    
  4. (Необязательно) Всегда показывать inputAccessoryView, даже когда клавиатура закрыта:

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

Удачи!

Просто добавьте одно расширение для 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
            }
        }
     }
}

дубликат: jsqmessageviewcontroller панель инструментов ios11

После долгих исследований и испытаний кажется, что это рабочее решение для попытки использовать UIToolbar с вводом текста inputAccessoryView. (Большинство существующих решений предназначены для использования фиксированного дополнительного представления вместо того, чтобы назначать его текстовому представлению (и скрывать его при закрытой клавиатуре).)

Код вдохновлен /questions/45395594/iphone-x-kak-obraschatsya-s-view-controller-inputaccessoryview/45395708#45395708. По сути, мы сначала создаем настраиваемое представление, которое имеет подвид панели инструментов:

class CustomInputAccessoryWithToolbarView: UIView {
    public var toolbar: UIToolbar!

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

        // https://stackru.com/a/58524360/2603230
        toolbar = UIToolbar(frame: frame)

        // Below is adopted from https://stackru.com/a/46510833/2603230
        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")
    }

    // https://stackru.com/a/46510833/2603230
    // 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 с проблемой UIBarButtonItem LayoutConstraint)

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 в симуляторе)

До:

После:

Кажется, это ошибка iOS, и для этого есть проблема rdar: inputAccessoryViews должен учитывать безопасную область, вставленную с внешней клавиатуры на iPhone X

Я думаю, это должно быть исправлено в обновлении iOS, когда появится iPhone X.

Пока iOS не будет автоматически управлять вставками, простой обходной путь будет заключаться в том, чтобы обернуть ваш аксессуар в виде контейнера и установить ограничение пространства снизу между вспомогательным видом и видом контейнера, чтобы соответствовать вставкам безопасной области окна.

Примечание. Конечно, этот обходной путь может удвоить расстояние между вспомогательными видами снизу, когда обновление iOS исправит нижний интервал для дополнительных видов.

Например

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

Из кода (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

Результат:

Я только что создал проект на Github с поддержкой iPhone X. Он уважает новое руководство по макету безопасной области. Использование:

autoresizingMask = [.flexibleHeight]

Скриншот:

Самый простой ответ (всего одна строка кода)

Просто используйте пользовательское представление, которое наследует от UIToolbar вместо UIView Как ваш inputAccessoryView,

Также не забудьте установить маску автоматического изменения размера этого пользовательского представления в UIViewAutoresizingFlexibleHeight,

Вот и все. Отблагодаришь позже.

Создайте иерархию представлений, в которой у вас естьcontainer viewиcontent view. Представление контейнера может иметь цвет фона или фоновое представление, охватывающее все его границы, и оно размещает свое представление содержимого на основеsafeAreaInsets. Если вы используете авторазметку, это так же просто, как установить нижнийAnchor представления контента равным его суперпредставлению.safeAreaLayoutGuide.

В случае, если у вас уже есть пользовательский вид, загруженный через файл пера.

Добавьте вспомогательный конструктор так:

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

и переопределить intrinsicContentSize :

override var intrinsicContentSize: CGSize {
    return .zero
}

в nib установить первое нижнее ограничение (видов, которые должны оставаться выше безопасной области), чтобы safeArea а второй superview с нижним priority так что это может быть удовлетворено на старых iOS.

Решение, которое работает для меня без обходных путей:

я использую UIInputViewController для обеспечения входного вспомогательного вида путем переопределения inputAccessoryViewController собственность вместо inputAccessoryView в "главном" виде контроллера.

UIInputViewController"s inputView установлен на мой пользовательский вид ввода (подкласс UIInputView).

Что на самом деле помогло мне - это установить allowsSelfSizing собственность моего UIInputView в true, Ограничения внутри входного представления используют безопасную область и устанавливаются таким образом, чтобы определять общую высоту представления (аналогично автоматическому изменению размера ячеек табличного представления).

- Для тех, кто использует lib JSQMessagesViewController -

Я предлагаю фиксированный форк на основе последней JSQ develop ветвь коммит.

Это использует didMoveToWindow решение (от @jki я верю?). Не идеально, но стоит попробовать, ожидая ответа Apple о inputAccessoryViewбезопасное расположение направляющих вложения, или любое другое лучшее исправление.

Вы можете добавить это в ваш Podfile, заменив предыдущую строку JSQ:

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

Я просто добавить безопасную область для inputAccessoryView (флажок в Xcode). И измените нижнее ограничение пространства, равное основанию безопасной области вместо корневого представления inputAccessoryView.

скованность

И результат

Другие вопросы по тегам