so I've set up keyboard observers using KeyboardDidShow so that I can shift the view up only when the keyboard is shown. However, KeyboardDidShow runs at the launch of every view and also at random times. I've tried monitoring the keyboard frames and only shifting the view if the frame changes, but every so often the view is still shifted even without the keyboard being shown. Usually, it happens whenever the view is first launched, so I tried adding a delay but it's not very dependable.
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardDidShow(_:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
}
#objc func keyboardDidShow(_ notification: Notification) {
let userInfo = notification.userInfo!
let beginFrameValue = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)!
let beginFrame = beginFrameValue.cgRectValue
let endFrameValue = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)!
let endFrame = endFrameValue.cgRectValue
if beginFrame.equalTo(endFrame) {
return
} else {
let indexPath = IndexPath(item: 0, section: 0)
if UIScreen.main.bounds.height == 812 {
collectionView?.contentInset = UIEdgeInsets(top: 318 + view.safeAreaInsets.bottom, left: 0, bottom: 73, right: 0)
}
collectionView?.scrollToItem(at: indexPath, at: .bottom, animated: true)
}
}
The problem is that you have configured the wrong notification. Do not use UIKeyboardDidShow. Use UIKeyboardWillShow, and examine the old frame, the new frame, and whether the new frame will cover your view.
Arriving at a robust implementation is not trivial, but it is certainly a well established previously solved problem that has been explained here many times.
For UIViewController instances you need observer these notifications only when view is visible and not subscribe to notification multiple times. Best way to do so is:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(onKeyboardWillChangeFrame(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
And here is working code from my project to handle keayboard frame
#objc private func onKeyboardWillChangeFrame(_ notification: NSNotification) {
// extract values
if let userInfo = notification.userInfo,
let keyboardFrameEnd = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue,
let animationCurveInt = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.intValue {
/*
СУКАБЛЯТЬ
With upgrate to Swift 4.2 UIView.AnimationCurve(rawValue: 7) returns actual instance of
UIView.AnimationCurve which crashes on access, mod by 4 limits value to max
*/
let animationCurve = UIView.AnimationCurve(rawValue: animationCurveInt % 4) ?? .easeIn
// View chanages
let topPoint = self.view.convert(keyboardFrameEnd.origin, from: self.view.window)
let height = self.view.bounds.size.height - topPoint.y
... update your constraints or manually update vars that affect layoutSubviews()...
let animationDuration: TimeInterval = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
var animationOptions: UIView.AnimationOptions = []
switch animationCurve {
case .easeInOut: animationOptions = .curveEaseInOut
case .easeIn: animationOptions = .curveEaseIn
case .easeOut: animationOptions = .curveEaseOut
case .linear: animationOptions = .curveLinear
}
// run animation
UIView.animate(withDuration: animationDuration, delay: 0, options: animationOptions, animations: {
self.view.layoutIfNeeded()
})
}
}
Related
I have a simple ViewController with a UITextView inside of a UIStackView. I've also stored an NSLayoutConstraint which is the bottom anchor of the stackView.
When the textView becomes first responder or resigns from first responder, two Notifications get triggered. The keyboardDidShowNotification works great. The issue is with keyboardWillHideNotification.
I’ve noticed that when I change the keyboard's frame by clicking the emojis button and then dismiss the keyboard, something breaks with the constraints.
As you can see in the gif below, as long as I don't go to the emojis section, the keyboard dismisses properly and the constraints are updated also properly. When I do go to the emojis section, then the bottomStackViewBottomConstraint and schoolTextViewTopConstraint go below that they should.
Here's what happens;
And here is the code;
fileprivate var schoolTextViewTopConstraint: NSLayoutConstraint!
fileprivate var bottomStackViewBottomConstraint: NSLayoutConstraint!
fileprivate func setupNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardDidShow), name: UIResponder.keyboardDidShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
#objc fileprivate func keyboardDidShow(notification: Notification) {
if let infoKey = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey], let rawFrame = (infoKey as AnyObject).cgRectValue {
let keyboardFrame = self.view.convert(rawFrame, from: nil)
bottomStackViewBottomConstraint.constant = -(keyboardFrame.height) + self.view.safeAreaInsets.bottom
schoolTextViewTopConstraint.constant = -keyboardFrame.height + 180
}
UIView.animate(withDuration: 0, animations: {
self.sendCommentButton.isHidden = false
self.toggleMuteButton.isHidden = true
self.shareButton.isHidden = true
self.leaveRoomButton.isHidden = true
self.view.layoutIfNeeded()
}, completion: nil)
}
#objc fileprivate func keyboardWillHide(notification: Notification) {
schoolTextViewTopConstraint.constant = 0
bottomStackViewBottomConstraint.constant = 0
sendCommentButton.isHidden = true
UIView.animate(withDuration: 0.5, animations: {
self.toggleMuteButton.isHidden = self.currentUserRole == VoiceRoomRole.listener
self.shareButton.isHidden = false
self.leaveRoomButton.isHidden = false
self.view.layoutIfNeeded()
}, completion: nil)
}
I am using swift and having issues with TouchUpInside: if I'm using UIKeyboardWillChangeFrame or UIKeyboardWillShow/UIKeyboardWillHide, & the keyboard is showing, & the button I'm trying to press is behind the keyboard when keyboard is shown initially. (If I scroll down to the button till visible and press, no touchUpInside called).
TouchDown seems to work consistently whether the keyboard is showing or not, but TouchUpInside is not called. If the button is above the top of the keyboard when the keyboard is initially shown, TouchUpInside works. I'm using keyboardNotification to set the height of a view below my scrollView in order to raise up my scrollView when keyboard is showing. From what I can see it's only usually when the button is the last element in the scrollView (and therefore likely to be behind the keyboard when keyboard shown).
#IBOutlet var keyboardHeightLayoutConstraint: NSLayoutConstraint?
#IBOutlet weak var textField: UITextField!
#IBOutlet weak var saveButton: UIButton!
#IBAction func saveTouchUpInside(_ sender: UIButton) {
print("touchupinside = does not work")
}
#objc func saveTouchDown(notification:NSNotification){
print("touchdown = works")
}
viewWillAppear:
textField.delegate = self
NotificationCenter.default.addObserver(self,selector:#selector(self.keyboardNotification(notification:)),name:
NSNotification.Name.UIKeyboardWillChangeFrame,object: nil)
self.saveButton.addTarget(self, action:#selector(ViewController.saveTouchDown(notification:)), for: .touchDown)
deinit {
NotificationCenter.default.removeObserver(self)
}
#objc func keyboardNotification(notification: NSNotification) {
if let userInfo = notification.userInfo {
let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let endFrameY = endFrame?.origin.y ?? 0
let duration:TimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber
let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions.curveEaseInOut.rawValue
let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw)
if endFrameY >= UIScreen.main.bounds.size.height {
self.keyboardHeightLayoutConstraint?.constant = 0.0
} else {
self.keyboardHeightLayoutConstraint?.constant = endFrame?.size.height ?? 0.0
}
UIView.animate(withDuration: duration, delay: TimeInterval(0),options: animationCurve, animations: { self.view.layoutIfNeeded() }, completion: nil)
}
}
I would like to dismiss the keyboard and call saveTouchUpInside at the same time, without using TouchDown.
I abstract the keyboard interaction as a separate class so that my controllers do not get bloated(also follows separation of concerns). Here is the keyboard manager class that I use.
import UIKit
/**
* To adjust the scroll view associated with the displayed view to accommodate
* the display of keyboard so that the view gets adjusted accordingly without getting hidden
*/
class KeyboardManager {
private var scrollView: UIScrollView
/**
* -parameter scrollView: ScrollView that need to be adjusted so that it does not get clipped by the presence of the keyboard
*/
init(scrollView: UIScrollView) {
self.scrollView = scrollView
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
selector: #selector(adjustForKeyboard),
name: UIResponder.keyboardWillHideNotification, object: nil)
notificationCenter.addObserver(self,
selector: #selector(adjustForKeyboard),
name: UIResponder.keyboardDidChangeFrameNotification, object: nil)
}
/**
* Indicates that the on-screen keyboard is about to be presented.
* -parameter notification: Contains animation and frame details on the keyboard
*
*/
#objc func adjustForKeyboard(notification: Notification) {
guard let containedView = scrollView.superview else { return }
let userInfo = notification.userInfo!
let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let keyboardViewEndFrame = containedView.convert(keyboardScreenEndFrame, to: containedView.window)
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber
let rawAnimationCurveValue = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! NSNumber).uintValue
UIView.animate(withDuration: TimeInterval(truncating: duration),
delay: 0,
options: [UIView.AnimationOptions(rawValue: rawAnimationCurveValue)],
animations: {
if notification.name == UIResponder.keyboardWillHideNotification {
self.scrollView.contentInset = UIEdgeInsets.zero
} else {
self.scrollView.contentInset = UIEdgeInsets(top: 0,
left: 0,
bottom: keyboardViewEndFrame.height,
right: 0)
}
self.scrollView.scrollIndicatorInsets = self.scrollView.contentInset
},
completion: nil)
}
deinit {
let notificationCenter = NotificationCenter.default
notificationCenter.removeObserver(self)
}
}
Its usage is like this
create a reference to the keyboard manager
private var keyboardManager: KeyboardManager!
and assign the keyboard manager class like below in viewDidLoad where self.scrollView is the scrollView that you are working with
self.keyboardManager = KeyboardManager(scrollView: self.scrollView)
This should take care of the issue. If that does not work, probably a sample project might help to take a deep dive into that.
On my loginViewController there is a textField and the button for searching. I want to make sure that when entering text in the textField my interface is not overlapped by the keyboard, but scrolled to the size of this keyboard and there was access to all the elements. For this I wrote this code:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(kbDidShow), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(kbDidHide), name: NSNotification.Name.UIKeyboardDidHide, object: nil)
}
#objc func kbDidShow(notification: Notification) {
guard let userInfo = notification.userInfo else { return }
let kdFrameSize = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
(self.view as! UIScrollView).contentSize = CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height + kdFrameSize.height)
(self.view as! UIScrollView).scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: kdFrameSize.height, right: 0)
}
#objc func kbDidHide() {
(self.view as! UIScrollView).contentSize = CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height)
}
When I run the application I have a side view of the scrollbar, but the interface itself does not scroll.
What could be the problem?
Try using IQKeyboardManager this will do your job automatically
Link - https://github.com/hackiftekhar/IQKeyboardManager
You just need to add this line in AppDelegate
All of the textfield will be adjusted automatically, in every view.
IQKeyboardManager.sharedManager().enable = true
Change the content offset rather than setting the contentSize.
#objc func kbDidShow(notification: Notification) {
guard let userInfo = notification.userInfo else { return }
let kdFrameSize = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
(self.view as! UIScrollView).contentOffset.y += kdFrameSize.size.height
(self.view as! UIScrollView).scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: kdFrameSize.height, right: 0)
}
I have an NSTabViewController, and I want to create some custom transition.
I added some NSViewControllerTransitionOptions values and when transition method is called with my values, a custom animation should run.
Bellow is the intermediate code that I written until now. Animation run exactly how what I want, but there is a problem.
nextVC is not presented (I think). That controller should be first responder, after animation that is not respond to keyboard import.
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewControllerTransitionOptions = [], completionHandler completion: (() -> Void)? = nil) {
if options.contains(.analogToThemes) {
if let firstVC = fromViewController as? MBCustomizeController {
let nextVC = toViewController
let themesContainer = nextVC.view
themesContainer.setFrameOrigin(NSMakePoint(-250, -510))
var sketchContainer:NSView?
var panelsContainer:NSView?
firstVC.view.addSubview(themesContainer)
for item in firstVC.view.subviews {
if item.identifier == "sketchContainer" {
sketchContainer = item
}
if item.identifier == "customizePanlesContainer"{
panelsContainer = item
}
}
NSAnimationContext.runAnimationGroup({ context in
context.duration = animationDuration
themesContainer.animator().setFrameOrigin(NSMakePoint(0, 0))
sketchContainer!.animator().setFrameOrigin(NSMakePoint(sketchContainer!.frame.origin.x, 520))
panelsContainer!.animator().setFrameOrigin(NSMakePoint(panelsContainer!.frame.origin.x + panelsContainer!.frame.width , 0))
}, completionHandler: {
firstVC.dismiss(nil)
})
}
return
}
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
}
How can I present nextVC correctly?
Thanks.
The gray box at the bottom is a text view. When I tap the text view, the keyboard will pop up from bottom. However, the text view has been covered by the pop up keyboard.
What functions should I add in order to move up the whole view when the keyboard pops up?
To detect when a keyboard shows up you could listen to the NSNotificationCenter
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: “keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)
That will call keyboardWillShow and keyboardWillHide. Here you can do what you want with your UITextfield
func keyboardWillShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
if let keyboardSize = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
//use keyboardSize.height to determine the height of the keyboard and set the height of your textfield accordingly
}
}
}
func keyboardWillHide(notification: NSNotification) {
//pull everything down again
}
As Milo says, to do this yourself you register for for keyboard show/hide notifications.
You then need to write code that figures out how much of the screen the keyboard is hiding, and how high on the screen the field in question is located, so you know how much to shift your views.
Once you've done that what you do depends on whether you're using AutoLayout or autoresizing masks (a.k.a. "Struts and springs" style layout.)
I wrote a developer blog post about a project that includes working code for shifting the keyboard. See this link:
http://wareto.com/animating-shapes-using-cashapelayer-and-cabasicanimation
In that post, look for the link titled "Sliding your views to make room for the keyboard" at the bottom.
Swift 4 complete solution.
I use this in all of my projects that need it.
on viewWillAppear register to listen for the keyboard showing/hiding and use the functions below to move the view up and back down.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
subscribeToKeyboardNotifications()
}
// stop listening for changes when view is dissappearing
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
unsubscribeFromKeyboardNotifications()
}
// listen for keyboard show/show events
func subscribeToKeyboardNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow(_:)), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide(_:)), name: .UIKeyboardWillHide, object: nil)
}
func unsubscribeFromKeyboardNotifications() {
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: nil)
}
#objc func keyboardWillHide(_ notification: Notification) {
view.frame.origin.y = 0
}
In my case I have a text field at the bottom which gets hidden by the keyboard so if this is in use then I move the view up
#objc func keyboardWillShow(_ notification: Notification) {
if bottomTextField.isFirstResponder {
view.frame.origin.y = -getKeyboardHeight(notification: notification)
}
}
func getKeyboardHeight(notification: Notification) -> CGFloat {
let userInfo = notification.userInfo
let keyboardSize = userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue
return keyboardSize.cgRectValue.height
}
Swift 4
This code is not so perfect but worth a try!
First embed the whole view in a scrollView.
Add this little cute function to your class:
#objc func keyboardNotification(_ notification: Notification) {
if let userInfo = (notification as NSNotification).userInfo {
let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let duration:TimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber
let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions().rawValue
let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw)
if (endFrame?.origin.y)! >= UIScreen.main.bounds.size.height {
scrollViewBottomConstraint?.constant = 0
} else {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
let keyboardHeight:Int = Int(keyboardSize.height)
scrollViewBottomConstraint?.constant = CGFloat(keyboardHeight)
scrollView.setContentOffset(CGPoint(x: 0, y: (scrollViewBottomConstraint?.constant)! / 2), animated: true)
}
}
UIView.animate(withDuration: duration, delay: TimeInterval(0), options: animationCurve, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}
.
Add this one to your ViewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardNotification(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
Don't forget to connect bottom constraint of your scrollView to your class (with name: "scrollViewBottomConstraint").