web-dev-qa-db-fra.com

iPhone X comment gérer View Controller inputAccessoryView?

J'ai une application de messagerie qui a la conception typique de l'interface utilisateur d'un champ de texte au bas d'une vue de tableau plein écran. Je règle ce champ de texte sur la variable inputAccessoryView du contrôleur de vue et appelle ViewController.becomeFirstResponder() afin d'obtenir l'affichage du champ en bas de l'écran.

Je comprends que c’est le moyen recommandé par Apple de réaliser cette structure d’interface utilisateur et que cela fonctionne parfaitement sur les appareils "classiques". Cependant, lorsque je teste sur le simulateur iPhone X, je remarque que, avec cette approche, le champ de texte ne respecte pas les nouvelles "zones protégées" . Le champ de texte est rendu tout en bas de l'écran, sous l'indicateur de l'écran d'accueil.

J'ai parcouru les documents HIG mais je n'ai rien trouvé d'utile concernant la inputAccessoryView sur un contrôleur de vue. 

C'est difficile parce qu'en utilisant cette approche, je ne contrôle pas directement l'une des contraintes, je règle simplement la variable inputAccessoryView et laisse le contrôleur de vue gérer l'interface utilisateur à partir de là. Je ne peux donc pas simplement limiter le champ aux nouvelles zones de sécurité.

Quelqu'un at-il trouvé une bonne documentation à ce sujet ou connaissez-vous une autre approche qui fonctionne bien sur l'iPhone X?

 enter image description here

16
LOP_Luke

inputAccessoryView et zone de sécurité sur iPhone X

  • lorsque le clavier n'est pas visible, la variable inputAccessoryView est épinglée tout en bas de l'écran. Il n'y a aucun moyen de contourner cela et je pense que c'est le comportement voulu.

  • les propriétés layoutMarginsGuide (iOS 9+) et safeAreaLayoutGuide (iOS 11) de la vue définie sur inputAccessoryView respectent toutes les deux la zone sécurisée, c'est-à-dire sur l'iPhone X:

    • lorsque le clavier n'est pas visible, la variable bottomAnchor représente la zone du bouton d'accueil
    • lorsque le clavier est affiché, la bottomAnchor est au bas de la inputAccessoryView, de sorte qu'elle ne laisse pas d'espace inutile au-dessus du clavier

Exemple de travail:

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

Voir le résultat ici

28
Alexandre Bintz

Il s'agit d'un problème général avec inputAccessoryViews sur iPhone X . L'entrée inputAccessoryView ignore les éléments safeAreaLayoutGuides de sa fenêtre.

Pour résoudre ce problème, vous devez ajouter manuellement la contrainte dans votre classe lorsque la vue passe à sa fenêtre:

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 fait ici référence à inputAccessoryView.

J'ai écrit à ce sujet en détail ici: http://ahbou.org/post/165762292157/iphone-x-inputaccessoryview-fix

25
ahbou

Dans Xib, recherchez une contrainte correcte au bottom de votre conception et définissez l'élément sur Safe Area au lieu de Superview:

Avant:  enter image description here

Réparer:  enter image description here

Après:  enter image description here

4
Vlad

Je viens de créer un rapide CocoaPod appelé SafeAreaInputAccessoryViewWrapperView pour résoudre ce problème. Il définit également de manière dynamique la hauteur de la vue encapsulée à l'aide de contraintes autolayout, évitant ainsi de définir manuellement le cadre. Prend en charge iOS 9+.

Voici comment l'utiliser:

  1. Enveloppez n'importe quel UIView/UIButton/UILabel/etc en utilisant SafeAreaInputAccessoryViewWrapperView(for:):

    SafeAreaInputAccessoryViewWrapperView(for: button)
    
  2. Conservez une référence à cela quelque part dans votre classe:

    let button = UIButton(type: .system)
    
    lazy var wrappedButton: SafeAreaInputAccessoryViewWrapperView = {
        return SafeAreaInputAccessoryViewWrapperView(for: button)
    }()
    
  3. Renvoie la référence dans inputAccessoryView:

    override var inputAccessoryView: UIView? {
        return wrappedButton
    }
    
  4. (Facultatif) Affichez toujours la inputAccessoryView, même lorsque le clavier est fermé:

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

Bonne chance!

3
Jeff

Ajoutez juste une extension pour 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
            }
        }
     }
}

dupliquer: barre d'outils jsqmessageviewcontroller ios11

3
ERbittuu

On dirait qu’il s’agit d’un bogue iOS et qu’il a un problème rdar: inputAccessoryViews doit respecter l’encart de zone de sécurité avec clavier externe sur iPhone X

Je suppose que cela devrait être corrigé dans la mise à jour iOS lorsque l'iPhone X apparaîtra.

2
Marat Saytakov

Jusqu'à ce que les incrustations sécurisées soient automatiquement guidées par iOS, la solution simple consisterait à envelopper votre accessoire dans la vue conteneur et à définir la contrainte d'espace inférieur entre la vue accessoire et la vue conteneur pour correspondre aux incrustations de zone sécurisée de la fenêtre. 

Remarque: bien entendu, cette solution de contournement peut doubler l'espacement des vues d'accessoires par rapport au bas lorsque la mise à jour iOS corrige l'espacement inférieur des vues d'accessoires.

Par exemple.

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

De code (Swift 4). Idée - surveillance de layoutMarginsDidChange événement et ajustement de 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])
   }
}

Usage:

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

Résultat:

 enter image description here  enter image description here

1
Vlad

Dans le cas où vous avez déjà une vue personnalisée chargée via un fichier nib.

Ajoutez un constructeur de commodité comme ceci:

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

et remplace intrinsicContentSize:

override var intrinsicContentSize: CGSize {
    return .zero
}

Dans nib, définissez la première contrainte inférieure (des vues devant rester au-dessus de la zone sécurisée) sur safeArea et la seconde sur superview avec une valeur inférieure priority pour pouvoir la satisfaire sur un ancien iOS.

0
Jakub Truhlář