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?
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+) andsafeAreaLayoutGuide
(iOS 11) properties of the view set asinputAccessoryView
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 theinputAccessoryView
, so that it leaves no useless space above the keyboard
- when the keyboard is not visible, the
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
}
}
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
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:
Wrap any UIView/UIButton/UILabel/etc using
SafeAreaInputAccessoryViewWrapperView(for:)
:SafeAreaInputAccessoryViewWrapperView(for: button)
Store a reference to this somewhere in your class:
let button = UIButton(type: .system) lazy var wrappedButton: SafeAreaInputAccessoryViewWrapperView = { return SafeAreaInputAccessoryViewWrapperView(for: button) }()
Return the reference in
inputAccessoryView
:override var inputAccessoryView: UIView? { return wrappedButton }
(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!
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
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.
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;
}
}
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:
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) tosafeArea
and the second one tosuperview
with lowerpriority
so it can be satisfied on older iOS.
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.
-- 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
I just created a project on Github with support for iPhone X. It respects the new safe area layout guide. Use:
autoresizingMask = [.flexibleHeight]
Screenshot:
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.
来源:https://stackoverflow.com/questions/46282987/iphone-x-how-to-handle-view-controller-inputaccessoryview