I'm trying to make a user interface similar to Messages where an input bar is pinned to the top of the keyboard, yet still appears at the bottom of the screen when the keyboard is dismissed. The approach I'd like to use involves setting the 'inputAccessoryView' property of my view controller with a view that contains a UITextView. This seems to work well on devices other than an iPhone X. Sometimes on the iPhone X, the keyboard when raised, shows the bottom row ('123', 'space' and 'return') of keys displayed at the very bottom of the screen below the safe area. The Globe and Microphone icons are drawn over the bottom row. I've included a screenshot showing this issue below. Pressing 'return' to add an extra line in the text view seems to reset the keyboard to its proper layout.
I'm wondering if there is a good workaround for this issue. The following code can be used to demonstrate the problem (replace the contents of ViewController.Swift of the project generated from the "Single View App" project template):
import UIKit
class ViewController: UIViewController {
private var textViewInputAccessoryView = UITextView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: 375, height: 44)))
override var canBecomeFirstResponder: Bool {
return true
}
override var inputAccessoryView: UIView? {
return textViewInputAccessoryView
}
override func viewDidLoad() {
super.viewDidLoad()
textViewInputAccessoryView.backgroundColor = UIColor.lightGray
}
}
Related
I'm adding a custom titleView inside a navigation bar using navigationItem.titleView for both Master/Detail VC. On changing the orientation of the device to landscape, titleView under MasterViewController works fine, but for DetailViewController titleView disappears. On changing the orientation back to portrait titleView appears back for DetailViewController. I have also attached a link for source code and video.
Is this an intended behavior or am I making a mistake from my side or is it an issue from Apple's side ?
//Custom Title View:
class TitleView: UIView {
override func sizeThatFits(_ size: CGSize) -> CGSize {
return CGSize(width: 50, height: 20)
}
}
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//Adding titleView for Master/Detail VC:
navigationItem.titleView = {
//Setting frame size here, did not make any difference
let view = TitleView(frame: .zero)
view.backgroundColor = .red
return view
}()
}
}
Full source code here: https://github.com/avitron01/SplitViewControllerIssue/tree/master/MathMonsters
Video highlighting the issue:
https://vimeo.com/336288580
I had the same issue. It seems an iOS bug. My workaround was to reassign the title view on every view layout. I used this piece of code in my DetailViewController:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let v = navigationItem.titleView {
navigationItem.titleView = nil
navigationItem.titleView = v
}
}
For those who stumble upon this, see also iOS 11 navigationItem.titleView Width Not Set. Basically, there's two suggested workarounds:
use a custom UIView that tells iOS to treat its intrinsicContentSize to be as big as possible with UIView.layoutFittingExpandedSize
use widthAnchor/heightAnchor constraints to set width and height of your view
I've come across this issue while working on a bigger project, but I've simplified it to demonstrate the problem.
I have a UIView in my View Controller that is square and centered:
Now, 'Proportional Height' is important here, because the problem does not occur if height or width are set to constant:
I want this view to appear as a nice circle on the phone, so I add an outlet from it to ViewController.swift and provide cornerRadius to view's layer:
class ViewController: UIViewController {
#IBOutlet weak var centerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
centerView.layer.cornerRadius = centerView.frame.width / 2
}
}
When in my Storyboard I set 'View as' to iPhone SE and launch the app on iPhone SE simulator, I get the expected result:
But if I do not change 'View as' in Storyboard and launch on iPhone 6 Plus simulator, this is what I get:
When I change Storyboard to corresponding screen, it's back to normal:
I was hoping it's just a Simulator bug, but same problem occurs when launching on physical devices. So potentially when the app is deployed, users may get messed up UI depending on which screen I chose for my Storyboard while developing?
UPDATE. I should note that adding layoutIfNeeded() to ViewController file resolves the issue:
override func viewDidLoad() {
super.viewDidLoad()
view.layoutIfNeeded()
centerView.layer.cornerRadius = centerView.frame.width / 2
}
But shouldn't layout happen without forcing when app launches no matter on which screen size?
Views can change size frequently. If you set the .cornerRadius in viewDidLoad() it won't be reflected if the view changes.
You will be much better off subclassing your UIView:
class RoundedView: UIView {
override func layoutSubviews() {
self.layer.cornerRadius = self.bounds.size.height / 2.0
}
}
and, you can easily make it "designable" so you see the "round view" when laying it out in your storyboard:
#IBDesignable
class RoundedView: UIView {
override func layoutSubviews() {
self.layer.cornerRadius = self.bounds.size.height / 2.0
}
}
I have UIViewController that has inputAccessoryView overrided with custom UITextView, lets call that view controller A. And I have another view controller that push A to navigation stack.
So, when I push A first time everything is ok - the UITextView appeared with text immediately. The strange thing starts with next push - the UITextView's text does not appear until view controller push transition animation end.
=>
The code of viewController A is there:
class NextViewController: UIViewController {
private var userInputView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.grayColor()
let textView = UITextView(frame: CGRect(origin: .zero,
size: CGSize(width: 200, height: 30)
), textContainer: nil)
textView.text = "asd"
userInputView = textView
}
override var inputAccessoryView: UIView? {
return userInputView
}
override func canBecomeFirstResponder() -> Bool {
return true
}
}
Tested in iOS 9.3 simulator.
After some coding I noticed that if inputAccessoryView is not deallocated (stored somewhere in global scope) the appearance during animation is defined by last UITextView state. But this solution is not good for me is ok actually. But is there any native-like solution, am I missing something? Because the desired behaviour is rather standard in my opinion.
So I will stick with the solution where the inputAccessoryView is stored until next appearance.
Ive been searching for awhile on this issue and I cant seem to find an answer. Im looking to use a Input accessory view on a view controller that is displayed on the iPad as a form sheet. I currently have the ALTextInputBar implemented that works well but my problem is that the accessory view is displayed full screen width. Id like to apply the input accessory to the displayed modal VC only. Like facebook has implemented in their iPad app below.
So my commentsVC code has the following methods
var messageInput = ALTextInputBar()
let keyboardObserver = ALKeyboardObservingView()
let leftButton = UIButton(frame: CGRectMake(0, 0, 44, 44))
let rightButton = UIButton(frame: CGRectMake(0, 0, 44, 44))
override var inputAccessoryView: UIView? {
get {
return messageInput
}
}
override func canBecomeFirstResponder() -> Bool {
return true
}
I then configure the textInputbar in its own method setting the left and right buttons etc. This all works perfect, Its just Id like to have the same approach as the image above.
One approach I have tried but it doesnt work too well is setting the tablefooterview of the commentsVc to equal messageInput. Although this is very buggy and not the right approach imo.
What I'm trying to do is to create something similar to the "find on page" search function in Safari on iPad.
I'm using a UIToolbar with some items in it and attached it to the keyboard by setting it as an inputAccessoryView on the UITextField. Works like a charm, but there is one thing I can't figure out. In Safari, when you search for something, the keyboard disappears but the tool bar remains on the bottom of the screen.
Does anyone have a clue on how to accomplish this? The only solution I can think of is to respond to a keyboard dismissed event and then pull out the UIToolBar and create a custom animation that moves it to the bottom of the screen. But this is hacky. I am looking for a more elegant solution. Something that can make me decide what to do with the input accessory view when the keyboard gets dismissed.
It's done like this:
Assign your UIToolbar to a property in your view controller:
#property (strong, nonatomic) UIToolbar *inputAccessoryToolbar;
In your top view controller, add these methods:
- (BOOL)canBecomeFirstResponder{
return YES;
}
- (UIView *)inputAccessoryView{
return self.inputAccessoryToolbar;
}
And then (optionally, as it usually shouldn't be necessary), whenever the keyboard gets hidden, just call:
[self becomeFirstResponder];
That way, your inputAccessoryToolbar will be both your view controller's and your text view's input accessory view.
I've ended up with UIToolBar that is not assigned as input accessory view, and slide up and down on UIKeyboardWillShowNotification / UIKeyboardWillHideNotification
Update to Swift 4, based on prior answers. If you add toolbar via storyboards you can do this
class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
#IBOutlet var toolbar: UIToolbar!
override var canBecomeFirstResponder: Bool {
get {
return true
}
}
override var inputAccessoryView: UIView {
get {
return self.toolbar
}
}
override func viewDidLoad() {
super.viewDidLoad()
textField.inputAccessoryView = toolbar
}
}
In this case, whenever text field resigns first responder, it defaults first responder to main view. Keep in mind, you might want to explicitly resign first responder, and set main view as first responder if there are multiple UI elements and first responder defaults to undesired view after resignation.
Adding to #arik's answer, here is the Swift version:
class ViewController: UIViewController {
#IBOutlet var textField: UITextField!
// Input Accessory View
private var inputAccessoryToolbar: UIToolBar?
override func canBecomeFirstResponder() -> Bool {
return true
}
override var inputAccessoryView: UIView? {
return inputAccessoryToolbar
}
override func viewDidLoad() {
super.viewDidLoad()
inputAccessoryToolbar = UIToolbar(frame: CGRectMake(0, 0, view.frame.size.width, 50))
textField.inputAccessoryView = inputAccessoryToolbar
}
// UITextFieldDelegate
func textFieldShouldReturn(textField: UITextField) -> Bool {
becomeFirstResponder()
return true
}
}
Thanks for the clean solution!
You may also need to work around the bug with the inputAccessoryView not respecting the safe area margins and thus not making room for home indicator thing on iPhone X: iPhone X how to handle View Controller inputAccessoryView?
I found the easiest solution when you have a UIToolbar from a xib and you are also using that UIToolbar as the inputAccessoryView of a text field is to embed the toolbar in a UIView when you return it from your overridden inputAccessoryView, and make the containing UIView taller by the safeAreaInsets.bottom. (Other solutions suggest constraining the bottom of the toolbar to the safe area in a subclass, but this leads to constraint conflicts and also means the area under the toolbar is the wrong colour.) However, you have to also bear in mind that the text field can have focus even when there is no keyboard on the screen (for instance if there is an external keyboard), so you need to change the inputAccessoryView of the text view to this toolbar-within-a-UIView in that case as well. In fact it will probably make things simpler to just always use the containing view and adjust the size of it appropriately. Anyway, here's my override of inputAccessoryView:
override var inputAccessoryView: UIView? {
if toolbarContainerView == nil {
let frame=CGRect(x: toolBar.frame.minX, y: toolBar.frame.minY, width: toolbar.frame.width, height: toolBar.frame.height+view.safeAreaInsets.bottom)
toolbarContainerView = UIView(frame: frame)
}
if (toolbar.superview != toolbarContainerView) {
//this is set to false when the toolbar is used above the keyboard without the container view
//we need to set it to true again or else the toolbar will appear at the very top of the window instead of the bottom if the keyboard has previously been shown.
toolbar.translatesAutoresizingMaskIntoConstraints=true
toolbarContainerView?.addSubview(toolbar)
}
return toolbarContainerView
}
It would probably be a good idea to override viewSafeAreaInsetsDidChange to adjust the size of toolbarContainerView in that case, too.