I have a search bar text field and a table view (for google auto complete) that I would like to translate up when the keyboard comes into view. I am successfully doing this, however, I am getting warnings/errors about my constraints. I am using auto layout via storyboard on this view and tried to disable/enable the constraints prior to/after showing/hiding the keyboard, but I am still getting these errors. Am I not disabling auto layout correctly? I followed what was given in this SO response.
override func viewDidLoad() {
...
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name:UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name:UIKeyboardWillHideNotification, object: nil)
...
}
func keyboardWillShow(sender: NSNotification) {
self.pixieLabel.hidden = true
self.searchBar.setTranslatesAutoresizingMaskIntoConstraints(true)
self.startingTableView.setTranslatesAutoresizingMaskIntoConstraints(true)
self.searchBar.frame.origin.y -= 150
self.startingTableView.frame.origin.y -= 150
}
func keyboardWillHide(sender: NSNotification) {
self.pixieLabel.hidden = false
self.searchBar.setTranslatesAutoresizingMaskIntoConstraints(false)
self.startingTableView.setTranslatesAutoresizingMaskIntoConstraints(false)
self.searchBar.frame.origin.y += 150
self.startingTableView.frame.origin.y += 150
}
Solution Code
func keyboardWillShow(sender: NSNotification) {
self.pixieLabel.hidden = true
self.seachBarTopConstraint.constant -= 150
self.searchBar.layoutIfNeeded()
}
func keyboardWillHide(sender: NSNotification) {
self.pixieLabel.hidden = false
self.seachBarTopConstraint.constant += 150
self.searchBar.layoutIfNeeded()
}
Instead of adjusting theframe values, I think you should be creating #IBOutlet references to constraints in Interface Builder and then changing the constant value of those constraints when you want to animate them, followed by a call to layoutIfNeeded. As I understand it, manually changing the values of a view's frame and auto layout don't mix.
Also, I wouldn't mess around with setTranslatesAutoresizingMaskIntoConstraints unless you are adding your constraints programmatically, in which case you're most likely just setting it to false.
Related
So I have a UIView which I want to move up and down as the textfield within it is editing and dismissing (keyboard appears and hides). Here is my keyboard observers, and the UIView's default constraint, all inside the viewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIWindow.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIWindow.keyboardWillHideNotification, object: nil)
let radiusViewConstraint = NSLayoutConstraint(item: searchRadiusView!, attribute: .bottom, relatedBy: .equal, toItem: super.view, attribute: .bottom, multiplier: 1.0, constant: -30.0)
Here are the keyboard functions:
#objc func keyboardWillShow(notification: NSNotification) {
print("keyboardWillShow")
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
self.searchRadiusView.center.y += (-1 * keyboardSize.height)
view.constraints[18].constant += (-1 * keyboardSize.height)
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
})
}
}
#objc func keyboardWillHide(notification: NSNotification){
print("keyboardWillHide")
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
self.searchRadiusView.center.y += 1 * keyboardSize.height
view.constraints[18].constant -= (-1 * keyboardSize.height)
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
})
}
}
constraints[18] is the constraint I created in my viewDidLoad. When I first tap the textfield, the UIView moves up the correct amount. Then, when I type my first character, it moves up that same amount again, and is now near the top of the screen. When I dismiss it, it moves back down to the height it was just previously at (just above the height of the keyboard, which is now dismissed). The next time I edit text, it moves up again, but not the full keyboard.height amount for some reason. When I dismiss, it goes down the FULL keyboard height. This then repeats until the UIView falls off the bottom of the screen. This movement is so strange and I have no idea what the problem is. All I wanted is for it to move up and down with the keyboard. Any ideas how to fix this? thanks
You are doing too many things wrong for me to list, so I have just fixed your project and made a pull request. Merge the pull request into your repo and you will see that it now works fine.
Just for the record, here are some of the main things you were doing wrong:
You added a bottom constraint, in code, to the blue view. But you already had a bottom constraint on the blue view. Thus you now have two of them, and any change in one of them will cause a conflict. The Xcode console was telling you very clearly that this was happening, but you ignored what it told you.
You were changing the constraint constant but also changing the blue view center. That probably caused no harm but it was pointless. You cannot govern a view's position by its center if you are governing it with constraints; they are opposites.
In your show and hide methods you examined keyboardFrameBeginUserInfoKey. That's wrong. You want to examine keyboardFrameEndUserInfoKey. The question is not where the keyboard is now but where it will be when it finishes moving.
The animation is wrong. There is no need for a UIView animation; you are already in an animation block. Just call layoutIfNeeded and the animation will happen together with the movement of the keyboard.
Your entire way of speaking of and accessing constraints is wrong. You use an incorrect expression super.view (you probably meant self.view). But even more important, you attempt to access the desired constraint by saying self.constraints[2]. That sort of thing is fragile in the extreme. The correct approach is to keep a reference to the actual constraint (an instance property). In this situation, since the constraint already exists (in the storyboard), that reference can be an outlet.
So, with all that said, here's my rewrite of your code; this is the complete code needed:
class ViewController: UIViewController {
#IBOutlet weak var sampleTextField: UITextField!
#IBOutlet weak var bottomConstraint: NSLayoutConstraint!
var originalConstant: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIWindow.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIWindow.keyboardWillHideNotification, object: nil)
self.originalConstant = bottomConstraint.constant
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
sampleTextField.endEditing(true)
}
}
extension ViewController {
#objc func keyboardWillShow(notification: NSNotification) {
print("keyboardWillShow")
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
self.bottomConstraint.constant += keyboardSize.height + 5
self.view.layoutIfNeeded()
}
}
#objc func keyboardWillHide(notification: NSNotification){
print("keyboardWillHide")
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
self.bottomConstraint.constant = self.originalConstant
self.view.layoutIfNeeded()
}
}
}
With all of that said, the code is still wrong, because you are not taking account of the very real possibility that you will get a keyboardWillShow notification when the keyboard is already showing. However, I leave that for your later investigation.
I would like to customize the scroll-offset when showing the keyboard. As you can see in the GIF, the Textfields are quite close to the keyboard and I would like to have a custom position. The "Name" textfield should have 50px more distance and the "Loan Title" textfield should just scroll to the bottom of my UIScrollView.
To be able to scroll past the keyboard I'm changing the UIScrollView insets. Strangely iOS automatically scrolls to the firstResponder textfield (see GIF).
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
}
#objc func keyboardWillShow(notification: NSNotification) {
// get the Keyboard size
let userInfo = notification.userInfo!
let keyboardEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
// update edge insets for scrollview
self.mainScrollView.scrollIndicatorInsets.bottom = keyboardEndFrame.height - self.view.layoutMargins.bottom
self.mainScrollView.contentInset.bottom = keyboardEndFrame.height - self.view.layoutMargins.bottom
}
I already tried to use the UITextfieldDelegate method: textFieldDidBeginEditing(_ textField: UITextField)
I also tried to use the Apple way described here: https://stackoverflow.com/a/28813720/7421005
None of these ways let me customize the automatic scroll position. In fact it kind of overrides every attempt. Does anyone know a way to workaround this?
You can prevent your view controller from automatically scrolling by setting automaticallyAdjustsScrollviewInsets to false as described here.
Implementing keyboard avoidance is also pretty straight forward. You can see how to do it here.
I don't believe there is any way to keep the automatic positioning and apply your own custom offset. You could experiment with making text field contained in another larger view and making that larger view the first responder, but that would be a hack at best.
I found a solution by myself. The problem was that the automatic scroll (animation) was interfering with my scrollRectToVisible call. Putting this in async fixed the problem.
It now looks similar to this:
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
}
#objc func keyboardWillShow(notification: NSNotification) {
// get the Keyboard size
let userInfo = notification.userInfo!
let keyboardEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
// update edge insets for scrollview
self.mainScrollView.scrollIndicatorInsets.bottom = keyboardEndFrame.height - self.view.layoutMargins.bottom
self.mainScrollView.contentInset.bottom = keyboardEndFrame.height - self.view.layoutMargins.bottom
var frame = CGRect.zero
if nameTextField.isFirstResponder {
frame = CGRect(x: nameTextField.frame.origin.x, y: nameTextField.frame.origin.y + 50, width: nameTextField.frame.size.width, height: nameTextField.frame.size.height)
}
if titleTextField.isFirstResponder {
frame = CGRect(x: titleTextField.frame.origin.x, y: titleTextField.frame.origin.y + titleShortcutsCollectionView.frame.height + 25, width: titleTextField.frame.size.width, height: titleTextField.frame.size.height)
}
DispatchQueue.main.async {
self.mainScrollView.scrollRectToVisible(frame, animated: true)
}
}
I've been struggling with a rather stupid issue for a few days now and I have not been able to find a solution.
I have a view controller with the following hierarchy: xcode screenshot
The user will have the possibility to add pictures below the textView (I haven't implemented this feature yet).
I set the "scrolling enabled" property of my textView to false so that its height automatically increases or decreases depending on the content typed by the user, which is working as expected.
I use the notification center in order to change the scrollView's contentInsets when the keyboard is shown like so:
func keyboardWillShow(notification: NSNotification) {
var userInfo = notification.userInfo!
var keyboardFrame: CGRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
keyboardFrame = self.view.convert(keyboardFrame, from: nil)
var contentInsets: UIEdgeInsets = self.scrollView.contentInset
contentInsets.bottom = keyboardFrame.size.height + 3*self.postContentTextView.font!.lineHeight
self.scrollView.contentInset = contentInsets
}
func keyboardWillHide(notification: NSNotification) {
let contentInsets = UIEdgeInsets.zero
self.scrollView.contentInset = contentInsets
}
with the following observers in viewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
I also set the scroll view content size in the viewDidLayoutSubviews function:
override func viewDidLayoutSubviews() {
self.scrollView.contentSize = self.postContentTextView.frame.size
}
Everything seemed to work fine until I realize that when the text is "too long" (or maybe it is something else), I am not able to select the last lines of the textView when I touch them. Actually, when I touch the screen the cursor is supposed to appear on the line I have just touched. This works normally for three quarters of the textView but it looks like the textView does not detect my finger for the last lines (the bigger the height is, the worse this issue is).
At the beginning, I thought I had a problem with my screen but I tested it on the iOS Simulator and the same problem occurred.
I have to admit that I am completely clueless about it. Does any of you have an idea of what the problem could be ?
Have you checked its constraints? The UITextView might be outside the bounds of its superviews. Set clipsToBounds to superview & test it. If it is out of the superview bounds, it will not detect touch
I've made it so a textfield is always on top of the keyboard (like in the messages app, whatsapp...) but my problem is that this poses the table above it up too because everything is in a scrollview. How can i make it so the table resizes so it fits in the space between the keyboard and top of the screen.
enter image description here
You can resize your UITableView when you change the frame.
For example:
tableView.frame = CGRectMake(0,0,100,200);
To setup this, you should check when the keyboard is present. With:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name:UIKeyboardWillShowNotification, object: nil);
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name:UIKeyboardWillHideNotification, object: nil);
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self);
}
func keyboardWillShow(notification: NSNotification) {
yourTableView.frame = newFrame // use CGRectMake()
}
Why you want to "resize" your TableView? if its on an scrollView, it dont need to be resized. Just use scrollRectToVisible on your scrollView to show your textBox.
Read here more about usage of UIScrollView:
http://www.appcoda.com/uiscrollview-introduction/
Or by Apple:
https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/UIScrollView_pg/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008179-CH1-SW1
I have a view controller with a UIScrollView pinned to all 4 sides. Then a UIView inside with all its 4 sides pinned to the scroll view and as well as equal width and equal height constraints added.
Inside this view, there are two container views. These two container views embed two separate UITableViewControllers. I'm getting no auto layout errors or warnings.
This is how it looks when it's run.
In the bottom table view, one cell(middle one of the first section) has a UITextField and the bottom cell has a UITextView. So obviously when the keyboard appears, these fields get obscured.
So what I wanted to do was to move the entire view that contains both container views when the keyboard appears. That's why I embedded it inside a scrollview. I use this code to monitor keyboard showing/hiding and set the scrollview's content inset accordingly.
class ViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name: UIKeyboardWillHideNotification, object: nil)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func keyboardWillShow(notification: NSNotification) {
adjustInsetForKeyboard(true, notification: notification)
}
func keyboardWillHide(notification: NSNotification) {
adjustInsetForKeyboard(false, notification: notification)
}
func adjustInsetForKeyboard(show: Bool, notification: NSNotification) {
let userInfo = notification.userInfo ?? [:]
let keybaordFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue()
let adjustmentHeight = (CGRectGetHeight(keybaordFrame)) * (show ? 1 : -1)
scrollView.contentInset.bottom += adjustmentHeight
}
}
But there are a couple of issues.
When the keyboard appears and although I change the scrollview's content inset, the entire view doesn't move. It does this weird thing. The bottom tableview goes under the top table view. It's easier to show so here is a video.
Tableviews overlapping issue
When I refocus on a textfield for more than 1 time, the scrollview goes off the screen!
Tableview going off the screen
Anyone got an idea why this is happening?
Dropbox link to demo project
A UITableViewController already automatically handles the adjustment of the content inset when the keyboard is shown. There is no documented way to disable this behaviour. You can override viewWillAppear(animated: Bool) in your StaticTableViewController and not call it's super method:
override func viewWillAppear(animated: Bool) {
}
It's probably where the UITableViewController registers for the keyboard events, as this disables the content inset adjustment. However, I can't tell you if there will be other adverse effects of not calling viewWillAppear of UITableViewController and this behaviour might change with future versions of iOS. So a safer way is to just not use UITableViewController and add a standard UITableView to a UIViewController and load your cells in there.
Also note that with your design the user could scroll all the way up and hide your lower content view behind the keyboard. Then the user can't scroll down as any scrolling only scrolls and bounces the upper tableview. So rethink your design or hide the keyboard as soon as the user scrolls
There are couple ways:
To observe UIKeyboadWillShowNotification and UIKeyboardWillHideNotification, get keyboard size data from it and adjust your scrollView contentInset bottom value properly.
func viewDidAppear() {
super.viewDidAppear()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "increaseContentInset:", name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "decreaseContentInset:", name: UIKeyboardWillHideNotification, object: nil)
}
func viewDidDisappear(){
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func increaseContentInset(notification: NSNotification) {
let endRect = notification.userInfo![UIKeyboardFrameEndUserInfoKey]
scrollView.contentInset = UIEdgeInsetsMake(0, 0, CGRectGetHeight(endRect), 0)
}
func decreaseContentInset(notification: NSNotification) {
scrollView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
}
Use the library for it. I strongly recommend you to use TPKeyboardAvoiding