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

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!

Related

Why can't I get the height between the nav bar and the top of the keyboard correctly?

I have a weird problem. I'm trying to calculate the exact height between the bottom of the navigation bar and the top of the keyboard no matter which iOS device I'm running the app on. Here is the method where I'm doing this calculation:
#objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
let navigationBarHeight: CGFloat = self.navigationController!.navigationBar.frame.height
let viewableArea = screenSize.height - keyboardRectangle.height - reportPostInstructionContainerViewHeight - characterCountContainerViewHeight - reportPostInstructionContainerViewHeight + 4
//iPhone 12 and above is "- 20"; iPhone 8 needs to be "+ 4"; iPhone 12 mini is "- 24"
print("**** navigationBarHeight: \(navigationBarHeight)")
print("**** keyboardHeight: \(keyboardHeight)")
print("**** screenSize.height: \(screenSize.height)")
print("**** total screen height - keyboard height: \(screenSize.height - keyboardRectangle.height)")
print("**** viewableArea: \(viewableArea)")
textViewHeight = viewableArea
print("**** textViewHeight: \(textViewHeight)")
UIView.animate(withDuration: 0.01, animations: { () -> Void in
self.textView.anchor(
top: self.horizontalDividerView.bottomAnchor,
leading: self.view.leadingAnchor,
bottom: nil,
trailing: self.view.trailingAnchor,
identifier: "ReportPostPFAVC.textView.directionalAnchors",
size: .init(width: 0, height: self.textViewHeight)
)
self.textView.layoutIfNeeded()
})
}
}
The line where "viewableArea" is being calculated seems to be the issue. For example, if I'm running the app on an iPhone 8, I need to add 4 to this calculation in order to size the text view properly.
Here is an image for reference:
I'm trying to get the bar with the "Report" button to sit perfectly on top of the keyboard. But, if I test on different devices sometimes I need to subtract 20 or 24 instead of adding 4.
I don't really understand where this gap is coming from.
Can anyone advise?
I will try to approach this from a different angle as I am not sure where exactly your issue is and what exactly was your logic from the code alone that you provided.
What you need to achieve is to find the coordinates of two frames in the same coordinate system. The two frames being; the keyboard frame and the navigation bar frame. And the "same coordinate system" is best defined by one of your views such as the view of your view controller.
There are convert methods on UIView which are designed to convert frames to/from different coordinate systems such as views.
So in your case all you need to do is
let targetView = self.view!
let convertedNavigationBarFrame = targetView.convert(self.navigationController!.navigationBar.bounds, from: self.navigationController!.navigationBar)
let convertedKeyboardFrame = targetView.convert(keyboardFrame.cgRectValue, from: nil)
In this example I used self.view as my coordinate system in which I want the two frames. This means the coordinates will be within a view controller. To get a height between two frames (which is your question) I could use absolutely any view that is in same window hierarchy and I should be getting the same result.
Then in this example I convert bounds of navigation bar from navigation bar, to target view. I found this to be best approach when dealing with UIView frames.
Last I convert keyboard frame to target view. The keyboard frame has a screen coordinate system which leads to using from: nil.
Getting the vertical distance between them is then a simple subtraction of two vertical coordinates
convertedKeyboardFrame.minY - convertedNavigationBarFrame.maxY
To have a full example I cerated a new project. In storyboard:
I added a navigation controller
I set the navigation controller to be "initial".
I set the old auto-generated view controller to be the root view controller of the navigation controller.
I added a text field which will trigger the
appearance of keyboard.
Then applied the following code:
The example code:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
self.navigationController?.navigationBar.backgroundColor = .green
}
private lazy var checkView: UIView = {
let checkView = UIView(frame: .zero)
checkView.backgroundColor = UIColor.black
checkView.layer.borderColor = UIColor.red.cgColor
checkView.layer.borderWidth = 5
self.view.addSubview(checkView)
return checkView
}()
#objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let targetView = self.view!
let convertedNavigationBarFrame = targetView.convert(self.navigationController!.navigationBar.bounds, from: self.navigationController!.navigationBar)
let convertedKeyboardFrame = targetView.convert(keyboardFrame.cgRectValue, from: nil)
checkView.frame = CGRect(x: 30.0, y: convertedNavigationBarFrame.maxY, width: 100.0, height: convertedKeyboardFrame.minY - convertedNavigationBarFrame.maxY)
}
}
}
The checkView appears between navigation bar and keyboard to show that the computation is correct. The view should fill the space between the two items and border is used to show that this view does not stretch below keyboard or above navigation bar.

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

Black color appears when moving origin to top when keyboard pops

I am moving the origin of the self.view by the -minY of the text field when the keyboard shows to make the text field show at top of the screen. But if the text field is near the bottom, the frame bottom gets black color. I tapped on the first text field in the attached image which moves it to top. Is there a way to move the view without displaying black color?
The view hierarchy is
+ UIViewController
+ UIView
+ UIScrollView
+ UIView
- UITextField
- UITextField
- UITextField
self.view.frame.origin.y = -(tagsTextField.frame.minY)
I fixed it by setting scrollview content offset on textfield tap.
scrollView.setContentOffset(CGPoint(x: 0, y: tagsTextField.frame.minY), animated: true)
And resetting on keyboard dismiss.
scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true)
You can move frame up when keyboard frame hides your textfield by the height of keyboard:
override func keyboardWillBeShown(note: Notification) {
let userInfo = note.userInfo
let keyboardFrame = userInfo?[UIKeyboardFrameEndUserInfoKey] as! CGRect
if keyboardFrame.intersects(activeTextField.frame) {
UIView.animate(withDuration: 0.2) {
self.view.frame.origin.y = -keyboardFrame.height
}
}
}
override func keyboardWillBeHidden(note: Notification) {
UIView.animate(withDuration: 0.2) {
self.view.frame.origin.y = 0
}
}
You can set the background color of the embedding view (perhaps your navigation controller) the same color as your view background color.
What I usually do in form screen is I put my input fields in a UITableView. Then, instead of moving the view up to make the current input field visible, I scroll the UITableView.
So, the background view/color of the UITableView does not move.

UITextView - Scrolling to selected place

I have a Notes view controller with a large UITextView in it. When the keyboard is active, I've made sure the contentInset is adjusted so that the user can see what's being typed. This works well.
However, if the textView already has a large amount of text in it and the keyboard isn't active yet, when the user taps on text in the lower portion of the textView, the textView doesn't automatically scroll up to show their cursor. As soon as they start typing, the textView scrolls to the appropriate position, but I'd like the textView to scroll to the position of the cursor as soon as they tap in the textView.
Here's my code:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWasShown), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillBeHidden), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
func keyboardWasShown(_ notification: Notification) {
let mainViewY = self.view.frame.origin.y
let textViewY = self.textView.frame.origin.y
let oneLineHeight = self.textView.font.lineHeight
let delta = (textViewY - mainViewY) - oneLineHeight
let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue
let keyboardHeight = (keyboardSize?.height)!
self.textView.contentInset = UIEdgeInsetsMake(0, 0, keyboardHeight + delta, 0)
self.textView.scrollIndicatorInsets = textView.contentInset
}
func keyboardWillBeHidden(_ notification: Notification) {
self.textView.contentInset = UIEdgeInsets.zero
self.textView.scrollIndicatorInsets = UIEdgeInsets.zero
}
I searched for people asking this question but couldn't find anyone experiencing my particular issue.
How can I ensure that when the user taps the textView to begin typing in a portion of the textView that would be covered by the keyboard, the textView scrolls to show their cursor even before they actually type?
You can achieve this by calling scrollRangeToVisible (docs here) using the text view's selectedRange. That method scrolls the text view to any range of text, and the selectedRange should be at the position of the cursor.
I was having a similar issue, but I tried out your sample code.
In viewDidLoad, I commented out the second observer (self.keyboardWillBeHidden).
When I ran the simulator and selected any part of a large block of text, the textView did automatically scroll to the correct position.

how can I move my container up when user displays a screen keyboard in swift?

I have a UIViewController with two containers embedded and one textfield. So far when user taps the textfield the whole screen moves up so the keyboard can fit without covering the lower part of the containers.
This is how it looks in my story board:
My code looks as follows:
override func viewDidLoad() {
super.viewDidLoad()
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "dismissKeyboard")
// view.addGestureRecognizer(tap)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name: UIKeyboardWillChangeFrameNotification, object: nil)
}
func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
if(isKeyboardShown == false){
realKeyboardSize = CGRect(x: keyboardSize.origin.x, y: keyboardSize.origin.y, width: keyboardSize.width, height: keyboardSize.height)
isKeyboardShown = true
self.view.frame.origin.y -= realKeyboardSize!.height
}else{
isKeyboardShown = false
self.view.frame.origin.y += realKeyboardSize!.height
}
}
}
Is it possible to move the lower container up instead of the whole screen?
I imagine it working like this:
topContainer stays untouched, lowerContainer moves up so that half of it is hidden behind topContainer and the keyboard is visible. When user hides the keyboard everything comes back to normal.
If you are using autolayout, you can achieve this by changing bottom constraint runtime
Here is your hierarchy and related constraints..
For how it works and how to setup things check this link
and Here is the demo, if you don't understand
take outlet of container's bottom constraint and increase it's constant to keyboard size or take outlet of top constraint and decrease it's constant to keyboard size . hope this will help :)

Resources