28

I have a messaging app that has the typical UI design of a text field at the bottom of a full screen table view. I am setting that text field to be the view controller's inputAccessoryView and calling ViewController.becomeFirstResponder() in order to get the field to show at the bottom of the screen.

I understand this is the Apple recommended way of accomplishing this UI structure and it works perfectly on "classic" devices however when I test on the iPhone X simulator I notice that using this approach, the text field does not respect the new "safe areas". The text field is rendered at the very bottom of the screen underneath the home screen indicator.

I have looked around the the HIG documents but haven't found anything useful regarding the inputAccessoryView on a view controller.

It's difficult because using this approach I'm not actually in control of any of the constraints directly, I'm just setting the inputAccessoryView and letting the view controller handle the UI from there. So I can't just constrain the field to the new safe areas.

Has anyone found good documentation on this or know of an alternate approach that works well on the iPhone X?

enter image description here

LOP_Luke
  • 3,150
  • 3
  • 22
  • 25
  • This looks like a bug in the API. You should file a bug report. As a workaround you could change the accessory view to have extra space at the bottom when running on iPhone X. – dasdom Sep 18 '17 at 15:32
  • is your textfield in a toolbar or uiview? I'm assuming you either assigned a toolbar or uiview containing your textfield and camera/send button to the inputAccessoryView. If so I would do what was suggested by dasdom and increase the height of that toolbar or uiview prior to assigning it to your inputAccessoryView for the iPhone X – alionthego Sep 22 '17 at 05:03
  • in viewWillAppear you can put print(inputAccessoryView?.safeAreaInsets) and will see all the safeAreaInsets are 0. Unfortunately this is read only and cannot be modified. – alionthego Sep 22 '17 at 07:00
  • Have you done your layout via code or storyboard? I've found a way to make it work via code. Just set te bottom to safeLayoutGuide.bottomAnchor. – Phillip Sep 22 '17 at 10:21
  • Looks like this thread will help you out: https://github.com/jessesquires/JSQMessagesViewController/issues/2179 – Tulleb Nov 07 '17 at 18:07
  • **A COMPLETE SOLUTION** I built using native Swift code, here is it: https://github.com/29satnam/InputAccessoryView – Codetard May 19 '18 at 08:16

16 Answers16

53

inputAccessoryView and safe area on iPhone X

  • when the keyboard is not visible, the inputAccessoryView is pinned on the very bottom of the screen. There is no way around that and I think this is intended behavior.

  • the layoutMarginsGuide (iOS 9+) and safeAreaLayoutGuide (iOS 11) properties of the view set as inputAccessoryView both respect the safe area, i.e on iPhone X :

    • when the keyboard is not visible, the bottomAnchor accounts for the home button area
    • when the keyboard is shown, the bottomAnchor is at the bottom of the inputAccessoryView, so that it leaves no useless space above the keyboard

Working example :

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

Alexandre Bintz
  • 664
  • 5
  • 3
  • 1
    The key to making this work is adding a SUBVIEW to the input accessory view and then constraining the bottom of that view to the safe area. – LOP_Luke Nov 01 '17 at 15:00
  • 4
    Nice one. The `intrinsicContentSize ` was the magic for me. Didn't even have to change the `translatesAutoresizingMask...` properties. – Sergiu Todirascu Jun 10 '18 at 11:23
  • Well, after hundreds of failures, the only things that work for me are setting the `intrinsicContentSize` to `CGSizeMake(UIViewNoIntrinsicMetric, 0.0f);` or simply inherit from `UIToolbar` instead of plain `UIView`. I don't know which one is the proper way but the popular `JSQMessagesViewController` uses the second approach, which simply inherits from the `UIToolbar`. – Edward Anthony Jul 04 '19 at 22:07
  • Is there any document from Apple refer this `this is needed so that the inputAccesoryView is properly sized from the auto layout constraints`? – congnd Aug 24 '20 at 04:22
  • The best answer, thanks a lot. What took me a while to realize is that UIKit creates an Auto Layout height constraint on `inputAccessoryView` based on its initial `frame`. But overriding `CustomView.intrinsicContentSize` disables this behavior, allowing us to set our own constraints instead. – Greg de J Oct 27 '22 at 13:35
48

This is a general issue with inputAccessoryViews on iPhone X. The inputAccessoryView ignores the safeAreaLayoutGuides of its window.

To fix it we have to manually add the constraint in your class when the view moves to its window:

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 here is referring to the inputAccessoryView.

I wrote about it in detail here: http://ahbou.org/post/165762292157/iphone-x-inputaccessoryview-fix

ahbou
  • 4,710
  • 23
  • 36
  • 4
    This is _the best_ answer and should be accepted instead of more complicated one. – iostriz Jan 18 '18 at 15:33
  • 2
    Works out of the box and should be the accepted answer. Thanks @ahbou – latenitecoder Mar 26 '18 at 14:54
  • 2
    This answer is mostly really good, and is the simplest way to get you going, but for some reason, it causes an issue on iPhone 6 and iPhone 6 Plus devices, where if you try to present a view controller onto the screen that has this inputAccessoryView, the screen will black out for a moment before presenting the view controller. This occurs with UIAlertControllers, UIActivityViewControllers, and any other UIViewController subclasses. – Soroush Khanlou May 04 '18 at 02:55
  • @SoroushKhanlou Are you using a UIToolBar? I've seen that happen when using it. I switched to using a plain UIView instead because iOS 11 broke UIToolBar inputAccessoryViews. If you're using it you will need to call layoutIfNeeded() right after adding the subviews. – ahbou May 04 '18 at 09:41
  • @ahbou nope, just a plain UIView – Soroush Khanlou May 05 '18 at 16:19
  • crashing in uiViewController class `if #available(iOS 11.0, *) { let window = UIApplication.shared.keyWindow if let window = window { pickerinputView.bottomAnchor.constraintLessThanOrEqualToSystemSpacingBelow(window.safeAreaLayoutGuide.bottomAnchor, multiplier: 1.0).isActive = true } }` – sanjeev sharma Jul 05 '18 at 09:21
  • You should put this code in your **UIView** subclass – ahbou Jul 05 '18 at 13:55
  • Amazing Answer. Thanks for the help! – hackerman58888 Jan 16 '19 at 18:18
  • 6
    Somehow, I get a "Unable to simultaneously satisfy constraints." error :/ Do you have it as well or is it my error? It still works though :D – unixb0y Mar 20 '19 at 22:37
  • Works like a charm. – tounaobun Jun 14 '19 at 06:20
  • Did you find a way to fix the "Unable to simultaneously satisfy constraints." warning @unixb0y? – hyouuu Sep 20 '19 at 09:14
  • @hyouuu no, I use a custom view now :) – unixb0y Sep 20 '19 at 12:46
  • what if I have both a custom input view, and an inputaccessoryview on top? – agirault Feb 07 '20 at 17:00
  • If you are working with landscape orientation handling in the same way leadingAnchor and trailingAnchor don't work. For some reason, after several rotations, safe area becomes wrong - width is too small, while height is correct in portrait mode. – Timur Suleimanov May 20 '21 at 16:08
  • doesn't work if you have `inputView` and `inputAccessoryView` simultaneously – Gargo Feb 22 '23 at 11:58
7

In Xib, find a right constraint at the bottom of your design, and set item to Safe Area instead of Superview:

Before: enter image description here

Fix: enter image description here

After: enter image description here

Vlad
  • 6,402
  • 1
  • 60
  • 74
  • @Akshay Sample code on this page below. See my answer with „green background“ screenshots of iPhone X. – Vlad Mar 02 '18 at 20:13
3

I just created a quick CocoaPod called SafeAreaInputAccessoryViewWrapperView to fix this. It also dynamically sets the wrapped view's height using autolayout constraints so you don't have to manually set the frame. Supports iOS 9+.

Here's how to use it:

  1. Wrap any UIView/UIButton/UILabel/etc using SafeAreaInputAccessoryViewWrapperView(for:):

    SafeAreaInputAccessoryViewWrapperView(for: button)
    
  2. Store a reference to this somewhere in your class:

    let button = UIButton(type: .system)
    
    lazy var wrappedButton: SafeAreaInputAccessoryViewWrapperView = {
        return SafeAreaInputAccessoryViewWrapperView(for: button)
    }()
    
  3. Return the reference in inputAccessoryView:

    override var inputAccessoryView: UIView? {
        return wrappedButton
    }
    
  4. (Optional) Always show the inputAccessoryView, even when the keyboard is closed:

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

Good luck!

Jeff
  • 349
  • 2
  • 12
3

Just add one extension for 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
            }
        }
     }
}

duplicate : jsqmessageviewcontroller ios11 toolbar

ERbittuu
  • 968
  • 8
  • 19
2

Seems it's an iOS bug, and there is a rdar issue for it: inputAccessoryViews should respect safe area inset with external keyboard on iPhone X

I guess this should be fixed in iOS update when iPhone X will come up.

Marat Saytakov
  • 393
  • 1
  • 8
2

Until safe are insets are guided by iOS automatically, simple workaround would be to wrap your accessory in container view and set bottom space constraint between accesory view and container view to match safe area insets of window.

Note: Of course this workaround can double your accessory view spacing from bottom when iOS update fixes bottom spacing for accessory views.

E.g.

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

After much research and trials, this seems to be a working solution for trying to use UIToolbar with a text input's inputAccessoryView. (Most of the existing solutions are for using fixed accessory view instead of assigning it to a text view (and hiding it when the keyboard is closed).)

The code is inspired by https://stackoverflow.com/a/46510833/2603230. Basically, we first create a custom view that has a toolbar subview:

class CustomInputAccessoryWithToolbarView: UIView {
    public var toolbar: UIToolbar!

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

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

        // Below is adopted from https://stackoverflow.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://stackoverflow.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
    }
}

Then you can set it as an inputAccessoryView for a text input normally: (You should specify the frame size to avoid the warnings seen in 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

When you want to interact with the toolbar (e.g., set items on the toolbar), you can simply refer to the toolbar variable:

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

Demo: (with hardware keyboard / Command+K in simulator)

Before: the toolbar obstructs the home indicator

After: the toolbar does not obstruct the home indicator

He Yifei 何一非
  • 2,592
  • 4
  • 38
  • 69
1

From code (Swift 4). Idea - monitoring layoutMarginsDidChange event and adjusting 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

Result:

enter image description here enter image description here

Vlad
  • 6,402
  • 1
  • 60
  • 74
1

I just created a project on Github with support for iPhone X. It respects the new safe area layout guide. Use:

autoresizingMask = [.flexibleHeight]

Screenshot:

screenshot

shim
  • 9,289
  • 12
  • 69
  • 108
  • Can you explain how to use this code, as opposed to just sharing the link? – Max von Hippel Dec 26 '18 at 05:17
  • Create a custom view which does not follows safearea layout guide and add `autoresizingMask = [.flexibleHeight]` in its init method, add another view in the subview which follows the safearea layout. Add others view in it normally. Check out the project for textview resizing – Raghav Ahuja Dec 26 '18 at 08:29
1

Have a view hierarchy where you have a container view and a content view. The container view can have a background color or a background view that encompasses its entire bounds, and it lays out its content view based on safeAreaInsets. If you’re using autolayout, this is as simple as setting the content view’s bottomAnchor to be equal to its superview’s safeAreaLayoutGuide.

danronmoon
  • 3,814
  • 5
  • 34
  • 56
Shockki
  • 61
  • 2
0

In the case you already have a custom view loaded via nib file.

Add a convenience constructor like this:

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

and override 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.

Jakub Truhlář
  • 20,070
  • 9
  • 74
  • 84
  • While your answer isn't super precise, overriding the intrinsicContentSize can be the key for people who are overriding the InputView (not the InputAccessoryView) for the iPhone X, for example, for showing a photo selector instead of the keyboard. – Claus Jørgensen Feb 07 '19 at 21:11
0

The simplest answer (with just one line of code)

Simply use a custom view that inherits from UIToolbar instead of UIView as your inputAccessoryView.

Also don't forget to set that custom view's autoresizing mask to UIViewAutoresizingFlexibleHeight.

That's it. Thank me later.

Edward Anthony
  • 3,354
  • 3
  • 25
  • 40
0

Solution that worked for me without workarounds:

I'm using UIInputViewController for providing input accessory view by overriding inputAccessoryViewController property instead of inputAccessoryView in the "main" view controller.

UIInputViewController's inputView is set to my custom input view (subclass of UIInputView).

What actually did the trick for me is setting allowsSelfSizing property of my UIInputView to true. The constraints inside input view use safe area and are set up in a way that defines total view's height (similar to autoresizing table view cells).

Yuriy Pavlyshak
  • 327
  • 2
  • 6
-1

-- For those who are using the JSQMessagesViewController lib --

I am proposing a fixed fork based on the JSQ latest develop branch commit.

It is using the didMoveToWindow solution (from @jki I believe?). Not ideal but worth to try while waiting for Apple's answer about inputAccessoryView's safe area layout guide attachment, or any other better fix.

You can add this to your Podfile, replacing the previous JSQ line:

pod 'JSQMessagesViewController', :git => 'https://github.com/Tulleb/JSQMessagesViewController.git', :branch => 'develop', :inhibit_warnings => true
Tulleb
  • 8,919
  • 8
  • 27
  • 55
-3

I'm just add safe area to inputAccessoryView (checkbox at Xcode). And change bottom space constraint equal to bottom of safe area instead of inputAccessoryView root view bottom.

Constraint

And result