keyboardWillShow events make additional changes - ios

I have a chat view and I transition the textView up with the keyboard by changing the height of the view. However, when I change the keyboard type to emoji, and also back to regular keyboard, the UIKeyboardWillShowNotification fires again and move the view an additional step up (ie an additional height of the keyboard).
How can I keep track of this and make sure I only subtract the height of a keyboard if it is not already subtracted, or only subtract the additional height of the emoji keyboard ?
func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
self.view.frame.size.height = self.view.frame.size.height - keyboardSize.height
self.view.layoutIfNeeded()
}
}
I change keyboard to emoji
I change keyboard from emoji and back to normal

Instead of using self.view.frame.height, use screen height.
func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
self.view.frame.size.height = UIScreen.mainScreen().bounds.size.height - keyboardSize.height
self.view.layoutIfNeeded()
}
}
An even more simpler way is to import this third party utility in your code
https://github.com/hackiftekhar/IQKeyboardManager
It automatically handles everything you want.

Related

Customize automatic UIScrollView scrolling with UITextField

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

IPhone X - Adjusting view based on keyboard

I'm currently trying to adjust the origin.y of a view based on when the keyboard shows/hides. I am able to adjust it accordingly however, on the iPhone X the contents of my adjusted view appears behind the status bar?
Below is the code for adjusting the view;
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
if keyboardShown == false{
self.containerView.frame.origin.y -= keyboardSize.height
keyboardShown = true
print("Keyboard UP")
}
}
}
#objc func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
if keyboardShown == true{
self.containerView.frame.origin.y += keyboardSize.height
keyboardShown = false
print("Keyboard DOWN")
}
}
They operated as intended minus the fact that the contents appears behind the statusbar (and is visible - overlapping with the time and signalbars etc.)
Below are screenshots to give a visual representation:
I am using Safe Area in the Interface builder, and have constrained my UIElements in accordance to the Safe Area. However, the container view (everything beside the custom navigation bar) seem to move beyond the Safe Area when adjusting the view.frame.origin.y.
Is there a way to take into consideration the Safe Area programatically when manipulating views?

How to darken/tint background when user clicks UITextView and keyboard appears?

Currently, I am able to make my keyboard appear/hide when I click inside/outside my UITextView. I'd like to make this nicer by tinting the background darker when keyboard appears, and then have the background return to normal when keyboard goes away.
I was told this behavior might be called "Focus" or "Modal Shadow Overlay" but have been unable to find a tutorial or images that match my goal. Here is a screenshot that shows exactly what I want to accomplish: when writing a caption for a new Instagram post, the background tints darker.
How do I implement this behavior in Swift?
What's this behavior called in iOS programming?
Thank you. [:
First of all to answer your two questions :
There are many ways to implement the overlay.
You can add a UIView as a subview, giving it constraints as vertical spacing from textView and aligning the bottom of the self.view and change its alpha when keyboard is presented/dismissed.
You can add a MaskView to self.view of a viewcontroller. The problem would be that when a mask is applied it turns all the other area black(Of course you can change color) and only the part that you set in the mask frame will be the color(black with 0.5 alpha) that you suggested.
You can call it adding an overlay(there isn't something else other than adding a mask from Apple's documentation that I've come across)
Now coming to the approach I've been using for a long time.
I add a UIView called overlay variable to my ViewController, Currently setting its frame to CGRect.zero
var overlay: UIView = UIView(frame: CGRect.zero)
After that, I add the Notification Observers for keyboardWillShow and keyboardWillHide in viewDidLoad.
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
And add the corresponding selectors to handle the Notifications
#objc func keyboardWillShow(notification: NSNotification)
#objc func keyboardWillHide(notification: NSNotification)
In keyboardWillShow, I get the keyboard frame to get the keyboard height
After that, I calculate the height of the overlay by getting the height of the screen and subtracting the navigation bar height, height of the textView and any margin added to the top of textView
Then I initialize my overlay variable by giving it the Y Position of from just below the textView. Initially, I set it's color to be UIColor.clear Add it as a subView to self.view and then changing it's color to black with 0.5 alpha with a 0.5 duration animation.
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
let overlayHeight = UIScreen.main.bounds.height - heightOfNavigationBar - keyboardSize.height - textView.frame.size.height - topConstraintofTextView(if any)
let overlayFrame = CGRect(x: 0, y: textView.frame.size.height + textView.frame.origin.y, width: UIScreen.main.bounds.width, height: overlayHeight)
self.overlay = UIView(frame: overlayFrame)
self.overlay.backgroundColor = .clear
self.view.addSubview(overlay)
UIView.animate(withDuration: 0.5, animations: {
self.overlay.backgroundColor = UIColor.black.withAlphaComponent(0.5)
})
}
}
After that, In keyboardWillHide, I change the alpha of overlay to be 0 with a little animation and as soon as it ends I remove the Overlay from superView.
#objc func keyboardWillHide(notification: NSNotification) {
UIView.animate(withDuration: 0.5, animations: {
self.overlay.backgroundColor = UIColor.black.withAlphaComponent(0)
}, completion: { (completed) in
self.overlay.removeFromSuperview()
})
self.overlay.removeFromSuperview()
}
And I do self.view.endEditing(true) in touchesBegan of viewController to dismiss the keyboard but that's upto you how you want to dismiss it.
Here's how it looks
Hope it helps!

How can I keep UITableView in view when keyboard appears?

I'm trying to create a page in an app that's your standard style messaging screen. I'm having trouble getting everything to position correctly when the keyboard slides into view. I'll post screenshots (sadly not inline), but here is my structure:
VIEWCONTROLLER
|-View
|-Scroll View
|-Content View
|-TextField
|-TableView (messages)
Everything is showing up as I would like it to when first loaded: If there aren't enough messages to fill the screen, the messages start at the top followed by a gap, and the text field is pinned to the bottom. Nothing scrolls. If there are a lot of messages, I am successfully scrolling the table to the last row and the textfield is pinned to the bottom of the screen still.
When the textfield is activated however, and there aren't a lot of messages, the gap between the table and the textfield remains and the messages are pushed out of view to the top.
I am trying to get the gap to shrink so the messages stay. This is standard in other messaging apps, but I cannot figure out how to do it
Initial view
Textfield activated, keyboard appears
Scrolling to display messages hides the textfield
UI Layout and constraints
Lastly, here is the code I have for keyboardWillShow. You'll notice some comments of things I have tried unsuccessfully.
func keyboardWillShow(notification:NSNotification) {
var userInfo = notification.userInfo!
let keyboardFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size
let contentInsets: UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardFrame!.height, 0.0)
self.scrollView.contentInset = contentInsets
self.scrollView.scrollIndicatorInsets = contentInsets
// scrollViewBottomConstraint.constant = keyboardFrame!.height - bottomLayoutGuide.length
// contentViewHeightConstraint.constant = -keyboardFrame!.height
// self.notificationReplyTable.frame.size.height -= keyboardFrame!.height
var aRect: CGRect = self.view.frame
aRect.size.height -= keyboardFrame!.height
if let activeField = self.activeField {
if(!aRect.contains(activeField.frame.origin)) {
self.scrollView.scrollRectToVisible(activeField.frame, animated: true)
}
}
}
I feel like the piece I'm missing is pretty small, but just don't know enough Swift 3 to nail this. Thank you for your help!
Edit: the problem is similar to this question with no accepted answer.
A way to this is to set up vertical autolayout constraints like this (but you will need a reference to the actual bottomMargin constraint to be able to modify it) :
"V:|[scrollView][textField]-(bottomMargin)-|"
The first time you arrive on the screen, bottomMargin is set to 0.
Then when keyboardWillShow is called, get the keyboard frame (cf How to get height of Keyboard?)
func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
}
}
And animate the constraint bottomMargin to get the height of the keyboard (the duration is 0.3 after some tests, but you can adjust it) :
bottomConstraint.constant = keyboardHeight
UIView.animate(withDuration: 0.3, delay: 0, options: nil, animations: {
self.view.layoutIfNeeded()
}
That means that every time the keyboard will appear, an animation will move up the text field, hence the scroll view height will be smaller and everything will fit in the screen.
!! Don't forget to test it on landscape mode if you support it, and on iPad too!!
Finally, handle the case when the keyboard will disappear in the keyboardWillHide and set bottomMargin back to 0 :
func keyboardWillHide(_ notification: Notification) {
bottomConstraint.constant = 0
UIView.animate(withDuration: 0.3, delay: 0, options: nil, animations: {
self.view.layoutIfNeeded()
}
}

Prevent UITextView from auto-scrolling when when being resized through auto-layout

My view has a TextView with 0 as the number of lines, populated with a long text that the user can edit.
When the keyboard shows, to resize the TextView so that it fits in the visible portion of the screen, the height constraint of an empty view at the bottom of the screen is equaled to the keyboard height. The bottom of the textView is constrained to the top of the empty view, so the text view gets resized.
When this happens, the textView automatically scrolls down in the text. I would like the text to avoid this scrolling, so that the first line always stays visible. I have tried a few ways, such as:
Disabling scrolling between keyboardWillShow and keyboardDidShow, not working
Scrolling back to zero, but we can see the text scrolling down and then scrolling back up (textView.scrollRangeToVisible(NSRange(location:0, length:0)))
Here's a drawing to make it much clearer (can't embed it yet, sorry):
UITextView Drawing
Relevant code:
func keyboardWillShow(_ notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
let duration: TimeInterval = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
emptyViewHeightConstraint.constant = keyboardSize.height
UIView.animate(withDuration: duration) { self.view.layoutIfNeeded() }
}
}
func keyboardWillHide(_ notification: NSNotification) {
let duration: TimeInterval = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
emptyViewHeightConstraint.constant = 0
UIView.animate(withDuration: duration) { self.view.layoutIfNeeded() }
}
PS: The TextView is actually inside a StackView, and this StackView has its bottom constrained to the top of the empty view.

Resources