I've subscribed to keyboardWillShowNotification and keyboardWillHideNotification to move around my UI. I've noticed that when I dismiss the keyboard by tapping the "Go" button, keyboardWillShowNotification is called twice (thus reseting some of my constraints) however if dismiss by hitting return on the keyboard (MacBook) then it's not called twice.
How can I avoid it being called twice? Why is this behaviour even there? I can't find any mention of it (lots of references to it being called twice with input views...etc) but never when being dismissed.
Here's my code:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardNotification(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWasDismissed(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
and...
#objc func keyboardNotification(notification: NSNotification) {
guard
let animationDuration = notification.userInfo?["UIKeyboardAnimationDurationUserInfoKey"] as? Double,
let animationCurve = notification.userInfo?["UIKeyboardAnimationCurveUserInfoKey"] as? NSNumber,
let frameEnd = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect,
let frameBegin = notification.userInfo?["UIKeyboardFrameBeginUserInfoKey"]
else {
print("No userInfo recived from NSNotification.Name.UIKeyboardWillShow")
return
}
print("WILL SHOW")
let margin = self.view.safeAreaLayoutGuide
constraintsWhenKeyboardVisible = [
boxOffice.leadingAnchor.constraint(equalTo: margin.leadingAnchor),
boxOffice.trailingAnchor.constraint(equalTo: margin.trailingAnchor),
boxOffice.bottomAnchor.constraint(equalTo: margin.bottomAnchor),
boxOffice.topAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -(frameEnd.height + 50))
]
NSLayoutConstraint.deactivate(boxOfficeFinalConstraints)
NSLayoutConstraint.activate(constraintsWhenKeyboardVisible)
UIView.animate(withDuration: animationDuration,
delay: TimeInterval(0),
options: UIView.AnimationOptions(rawValue: animationCurve.uintValue),
animations: {
self.boxOffice.answerField.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.view.layoutIfNeeded()
},
completion: nil)
}
#objc func keyboardWasDismissed(notification: NSNotification) {
guard
let animationDuration = notification.userInfo?["UIKeyboardAnimationDurationUserInfoKey"] as? Double,
let animationCurve = notification.userInfo?["UIKeyboardAnimationCurveUserInfoKey"] as? NSNumber
else {
print("No userInfo recived from NSNotification.Name.UIKeyboardWillShow")
return
}
print("WILL HIDE")
//print(notification)
NSLayoutConstraint.deactivate(self.constraintsWhenKeyboardVisible)
NSLayoutConstraint.activate(self.boxOfficeFinalConstraints)
UIView.animate(withDuration: animationDuration,
delay: TimeInterval(0),
options: UIView.AnimationOptions(rawValue: animationCurve.uintValue),
animations: {
self.boxOffice.answerField.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMinXMinYCorner]
self.view.layoutIfNeeded()
},
completion: nil)
}
I haven't solved the issue of keyboardWillShowNotification being posted when Return is hit on the iOS keyboard simulator but not on the hardware keyboard in the simulator, but I have modified my code so that when the keyboard is shown I don't add constraints, I simply modify the constant of the constraint using the height from the keyboard notification. This has solved it.
boxOfficeWhenKeyboardVisible[3].constant = -(frameEnd.height + 50)
Related
I have a text input field at the bottom of my view, which I'm trying to animate up and down to stay on top of the keyboard.
func setupKeyboardObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardDidShow), name: UIResponder.keyboardDidShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillChangeFrame), name: UIResponder.keyboardWillHideNotification, object: nil)
}
#objc func handleKeyboardWillChangeFrame(notification: NSNotification) {
let keyboardFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let keyboardDuration = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double)
print(keyboardFrame)
orderDetailView?.textInputViewBottomAnchor?.constant = -keyboardFrame!.height
UIView.animate(withDuration: keyboardDuration!) {
self.view.layoutIfNeeded()
}
}
OrderDetailView is the view for the viewcontroller.
The textinputview is the part that animates, and it works correctly when the keyboard first shows up, but does not animate back when I send a message and the keyboard resigns first responder, nor if I resignfirstresponder by clicking outside the keyboard.
When I print the cgrect value from keyboardFrameEndUserInfoKey, it returns the same frame value as when the keyboard is present (instead of 0).
This only seems to work properly when I drag down the keyboard from the view.
Thanks for your help.
In your case the height is still non-zero when keyboard hides which I assume is your issue. You need to convert keyboard frame to your view coordinate system and setup constraints according to that. Check the following:
#objc private func onKeyboardChange(notification: NSNotification) {
guard let info = notification.userInfo else { return }
guard let value: NSValue = info[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }
let newFrame = value.cgRectValue
if let durationNumber = info[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber, let keyboardCurveNumber = info[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber {
let duration = durationNumber.doubleValue
let keyboardCurve = keyboardCurveNumber.uintValue
UIView.animate(withDuration: duration, delay: 0, options: UIViewAnimationOptions(rawValue: keyboardCurve), animations: {
self.updateFromKeyboardChangeToFrame(newFrame)
}, completion: { _ in
// After animation
})
} else {
self.updateFromKeyboardChangeToFrame(newFrame)
}
}
private func updateFromKeyboardChangeToFrame(_ keyboardFrame: CGRect) {
let view: UIView! // Whatever view that uses bottom constraint
let bottomConstraint: NSLayoutConstraint! // Your bottom constraint
let constant = view.bounds.height-max(0, view.convert(keyboardFrame, from: nil).origin.y)
bottomConstraint.constant = max(0, constant)
view.layoutIfNeeded()
}
In your case you seem to use
let view = self.view
let bottomConstraint = orderDetailView?.textInputViewBottomAnchor
and it depends on how you define your constraint but it seems you will need to use negative values as bottomConstraint.constant = -max(0, constant).
Hi I am trying to make a view's bottom align with the top of UIKeyboard.
Update 1: I have created a github project if you would like to give it a try: https://github.com/JCzz/KeyboardProject
Note: I need the aView to be dynamic.
Update 2: Just pushed - to include using frames
I might have been looking at this for too long, I can not wrap my brain around it :-)
Do you know how?
How do I know if the UIKeyboard is on the way down or up?
If UIKeyboard is up, then how to align it with the view(attachKeyboardToFrame - see code).
I have found the following UIView extension:
import UIKit
extension UIView {
func bindToKeyboard(){
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChange), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}
func unbindFromKeyboard(){
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}
#objc
func keyboardWillChange(notification: NSNotification) {
guard let userInfo = notification.userInfo else { return }
let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as! Double
let curve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as! UInt
let curFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
let targetFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
// To get the total height of view
let topView = UIApplication.shared.windows.last
//
let attachKeyboardToFrame = Singleton.sharedInstance.attachKeyboardToFrame
let global_attachKeyboardToFrame = self.superview?.convert(attachKeyboardToFrame!, to: topView)
if (targetFrame.height + attachKeyboardToFrame!.height) > (topView?.frame.height)! {
self.frame.origin.y = -targetFrame.origin.y
}else{
}
}
}
You can achieve it using following Autolayout solution.
First you need UILayoutGuide that will be used simulate Keyboard aware bottom anchor, and a NSLayoutConstraint that will control this layout guide:
fileprivate let keyboardAwareBottomLayoutGuide: UILayoutGuide = UILayoutGuide()
fileprivate var keyboardTopAnchorConstraint: NSLayoutConstraint!
In the viewDidLoad add the keyboardAwareBottomLayoutGuide to the view and setup the appropriate contraints:
self.view.addLayoutGuide(self.keyboardAwareBottomLayoutGuide)
// this will control keyboardAwareBottomLayoutGuide.topAnchor to be so far from bottom of the bottom as is the height of the presented keyboard
self.keyboardTopAnchorConstraint = self.view.layoutMarginsGuide.bottomAnchor.constraint(equalTo: keyboardAwareBottomLayoutGuide.topAnchor, constant: 0)
self.keyboardTopAnchorConstraint.isActive = true
self.keyboardAwareBottomLayoutGuide.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor).isActive = true
Then use following lines to start listening to keyboard showing and hiding:
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowNotification(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHideNotification(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
And finally, use following methods to control the keyboardAwareBottomLayoutGuide to mimic the keyboard:
#objc fileprivate func keyboardWillShowNotification(notification: NSNotification) {
updateKeyboardAwareBottomLayoutGuide(with: notification, hiding: false)
}
#objc fileprivate func keyboardWillHideNotification(notification: NSNotification) {
updateKeyboardAwareBottomLayoutGuide(with: notification, hiding: true)
}
fileprivate func updateKeyboardAwareBottomLayoutGuide(with notification: NSNotification, hiding: Bool) {
let userInfo = notification.userInfo
let animationDuration = (userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue
let keyboardEndFrame = (userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let rawAnimationCurve = (userInfo?[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber)?.uint32Value
guard let animDuration = animationDuration,
let keybrdEndFrame = keyboardEndFrame,
let rawAnimCurve = rawAnimationCurve else {
return
}
let convertedKeyboardEndFrame = view.convert(keybrdEndFrame, from: view.window)
let rawAnimCurveAdjusted = UInt(rawAnimCurve << 16)
let animationCurve = UIViewAnimationOptions(rawValue: rawAnimCurveAdjusted)
// this will move the topAnchor of the keyboardAwareBottomLayoutGuide to height of the keyboard
self.keyboardTopAnchorConstraint.constant = hiding ? 0 : convertedKeyboardEndFrame.size.height
self.view.setNeedsLayout()
UIView.animate(withDuration: animDuration, delay: 0.0, options: [.beginFromCurrentState, animationCurve], animations: {
self.view.layoutIfNeeded()
}, completion: { success in
//
})
}
Now with all this set up, you can use Autolayout to constraint your views to keyboardAwareBottomLayoutGuide.topAnchor instead of self.view.layoutMarginsGuide.bottomAnchor (or self.view.bottomAnchor, whichever you use). keyboardAwareBottomLayoutGuide will automatically adjust to the keyboard showed or hidden.
Example:
uiTextField.bottomAnchor.constraint(equalTo: keyboardAwareBottomLayoutGuide.topAnchor).isActive = true
EDIT: Directly setting frames
While I strongly recommend using Autolayout, in cases when you cannot go with this, directly setting frames can be also a solution. You can use the same principle. In this approach you don't need layout guide, so you don't need any additional instance properties. Just use viewDidLoad to register for listening notifications:
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowNotification(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHideNotification(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
Then implement methods that will react to these notifications:
#objc fileprivate func keyboardWillShowNotification(notification: NSNotification) {
adjustToKeyboard(with: notification, hiding: false)
}
#objc fileprivate func keyboardWillHideNotification(notification: NSNotification) {
adjustToKeyboard(with: notification, hiding: true)
}
fileprivate func adjustToKeyboard(with notification: NSNotification, hiding: Bool) {
let userInfo = notification.userInfo
let animationDuration = (userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue
let keyboardEndFrame = (userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let rawAnimationCurve = (userInfo?[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber)?.uint32Value
guard let animDuration = animationDuration,
let keybrdEndFrame = keyboardEndFrame,
let rawAnimCurve = rawAnimationCurve else {
return
}
let convertedKeyboardEndFrame = view.convert(keybrdEndFrame, from: view.window)
let rawAnimCurveAdjusted = UInt(rawAnimCurve << 16)
let animationCurve = UIViewAnimationOptions(rawValue: rawAnimCurveAdjusted)
// we will go either up or down depending on whether the keyboard is being hidden or shown
let diffInHeight = hiding ? convertedKeyboardEndFrame.size.height : -convertedKeyboardEndFrame.size.height
UIView.animate(withDuration: animDuration, delay: 0.0, options: [.beginFromCurrentState, animationCurve], animations: {
// this will move the frame of the aView according to the diffInHeight calculated above
// of course here you need to set all the frames that would be affected by the keyboard (this is why I prefer using autolayout)
self.aView?.frame = (self.aView?.frame.offsetBy(dx: 0, dy: diff))!
// of course, you can do anything more complex than just moving the aView up..
})
}
In both cases, don't forget to unregister observing the notifications once the viewController is deinitialized to prevent retain cycles:
deinit {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
I have a UIScrollView and a UITextView, just like in any messaging / chat app, whenUIScrollView is scrolled down, the keypad interactively being dragged too.
I need to detect keyboard height while UIScrollView is scrolled, I tried UIKeyboardWillChangeFrame observer, but this event is called after scroll tap is released.
Without knowing keyboard height, I am unable to update the UITextView bottom constraint, and I get a gap between the keypad and bottom view #screenshot.
Also attaching screenshot from Viber, that does align the bottom bar when keyboard being dragged from scroll bar, also can be seen in WhatsApp too.
As of iOS 10, Apple doesn't provide a NSNotification observer to detect the frame change while the keypad is dragged interactively by UIScrollView, UIKeyboardWillChangeFrame and UIKeyboardDidChangeFrame are observed only once releasing tap.
Anyways, after looking around DAKeyboardControl library, I had the idea to attach UIScrollView.UIPanGestureRecognizer in the UIViewController, so any gesture events that are produced will be handled in UIViewController as well. After screwing around several hours, I got it to work, here is all the code that is necessary for this:
class ViewController: UIViewController, UIGestureRecognizerDelegate {
fileprivate let collectionView = UICollectionView(frame: .zero)
private let bottomView = UIView()
fileprivate var bottomInset: NSLayoutConstraint!
// This holds height of keypad
private var maxKeypadHeight: CGFloat = 0 {
didSet {
self.updateCollectionViewInsets(maxKeypadHeight + self.bottomView.frame.height)
self.bottomInset.constant = -maxKeypadHeight
}
}
private var isListeningKeypadChange = false
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(keypadWillChange(_:)), name: .UIKeyboardWillChangeFrame, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keypadWillShow(_:)), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keypadWillHide(_:)), name: .UIKeyboardWillHide, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keypadDidHide), name: .UIKeyboardDidHide, object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self)
}
func keypadWillShow(_ notification: Notification) {
guard !self.isListeningKeypadChange, let userInfo = notification.userInfo as? [String : Any],
let animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? TimeInterval,
let animationCurve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? UInt,
let value = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue
else {
return
}
self.maxKeypadHeight = value.cgRectValue.height
let options = UIViewAnimationOptions.beginFromCurrentState.union(UIViewAnimationOptions(rawValue: animationCurve))
UIView.animate(withDuration: animationDuration, delay: 0, options: options, animations: { [weak self] in
self?.view.layoutIfNeeded()
}, completion: { finished in
guard finished else { return }
// Some delay of about 500MS, before ready to listen other keypad events
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.beginListeningKeypadChange()
}
})
}
func handlePanGestureRecognizer(_ pan: UIPanGestureRecognizer) {
guard self.isListeningKeypadChange, let windowHeight = self.view.window?.frame.height else { return }
let barHeight = self.bottomView.frame.height
let keypadHeight = abs(self.bottomInset.constant)
let usedHeight = keypadHeight + barHeight
let dragY = windowHeight - pan.location(in: self.view.window).y
let newValue = min(dragY < usedHeight ? max(dragY, 0) : dragY, self.maxKeypadHeight)
print("Old: \(keypadHeight) New: \(newValue) Drag: \(dragY) Used: \(usedHeight)")
guard keypadHeight != newValue else { return }
self.updateCollectionViewInsets(newValue + barHeight)
self.bottomInset.constant = -newValue
}
func keypadWillChange(_ notification: Notification) {
if self.isListeningKeypadChange, let value = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue {
self.maxKeypadHeight = value.cgRectValue.height
}
}
func keypadWillHide(_ notification: Notification) {
guard let userInfo = notification.userInfo as? [String : Any] else { return }
self.maxKeypadHeight = 0
var options = UIViewAnimationOptions.beginFromCurrentState
if let animationCurve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? UInt {
options = options.union(UIViewAnimationOptions(rawValue: animationCurve))
}
let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? TimeInterval
UIView.animate(withDuration: duration ?? 0, delay: 0, options: options, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
func keypadDidHide() {
self.collectionView.panGestureRecognizer.removeTarget(self, action: nil)
self.isListeningKeypadChange = false
if (self.maxKeypadHeight != 0 || self.bottomInset.constant != 0) {
self.maxKeypadHeight = 0
}
}
private func beginListeningKeypadChange() {
self.isListeningKeypadChange = true
self.collectionView.panGestureRecognizer.addTarget(self, action: #selector(self.handlePanGestureRecognizer(_:)))
}
fileprivate func updateCollectionViewInsets(_ value: CGFloat) {
let insets = UIEdgeInsets(top: 0, left: 0, bottom: value + 8, right: 0)
self.collectionView.contentInset = insets
self.collectionView.scrollIndicatorInsets = insets
}
}
You can simply add this pod:
pod 'IQKeyboardManagerSwift'
Then in your AppDelegate.swift add:
import IQKeyboardManagerSwift
And a in the didFinishLaunchingWithOptions function
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
IQKeyboardManager.sharedManager().enable = true // ADD THIS !!!
return true
}
That this simple.
It seems like you have a wrong constant of bottom constraint.
Try to reset bottom constraint everytime and set new height value
func keyboardDidChangeFrame(notification: Notification) {
if let userInfo = notification.userInfo {
if 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 {
self.inputBarBottomSpacing.constant = 0
} else {
//the most important logic branch, reset current bottom constant constraint value
if self.inputBarBottomSpacing.constant != 0 {
self.inputBarBottomSpacing.constant = 0
}
self.inputBarBottomSpacing.constant = -endFrame.size.height
}
UIView.animate(withDuration: duration, delay: TimeInterval(0), options: animationCurve, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}
}
swift 3.0
You can try this way, i have implemented in my project. Hope it will help you.
#IBOutlet weak var constant_ViewBottom: NSLayoutConstraint! // 0
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector:#selector(self.keyboardWillShow(_:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector:#selector(self.keyboardWillHide(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
func keyboardWillShow(_ notification: NSNotification){
if let keyboardRectValue = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size {
let keyboardHeight = keyboardRectValue.height
print("keyboardHeight:=\(keyboardHeight)")
constant_ViewBottom.constant = keyboardHeight
self.view.layoutIfNeeded()
}
}
func keyboardWillHide(_ notification: NSNotification){
constant_ViewBottom.constant = 0.0
self.view.layoutIfNeeded()
}
I'm working on an iOS app and currently all my elements are in a scroll view and when the keyboard is present I move the view up 250 pts. This solved my problem but the keyboard is always a different size per device.
How could I detect how far from the bottom of the screen my text field is and how tall the keyboard is?
You should observe the notification for showing and hiding the keyboard. And after that you can get the exact keyboard size and either shift or change the content insets of your scroll view. Here's a sample code:
extension UIViewController {
func registerForKeyboardDidShowNotification(scrollView: UIScrollView, usingBlock block: (NSNotification -> Void)? = nil) {
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardDidShowNotification, object: nil, queue: nil, usingBlock: { (notification) -> Void in
let userInfo = notification.userInfo!
let keyboardSize = userInfo[UIKeyboardFrameBeginUserInfoKey]?.CGRectValue.size
let contentInsets = UIEdgeInsetsMake(scrollView.contentInset.top, scrollView.contentInset.left, keyboardSize!.height, scrollView.contentInset.right)
scrollView.scrollEnabled = true
scrollView.setContentInsetAndScrollIndicatorInsets(contentInsets)
block?(notification)
})
}
func registerForKeyboardWillHideNotification(scrollView: UIScrollView, usingBlock block: (NSNotification -> Void)? = nil) {
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardWillHideNotification, object: nil, queue: nil, usingBlock: { (notification) -> Void in
let contentInsets = UIEdgeInsetsMake(scrollView.contentInset.top, scrollView.contentInset.left, 0, scrollView.contentInset.right)
scrollView.setContentInsetAndScrollIndicatorInsets(contentInsets)
scrollView.scrollEnabled = false
block?(notification)
})
}
}
extension UIScrollView {
func setContentInsetAndScrollIndicatorInsets(edgeInsets: UIEdgeInsets) {
self.contentInset = edgeInsets
self.scrollIndicatorInsets = edgeInsets
}
}
And in your UIViewController in which you want to shift the scrollview, just add next lines under the viewDidLoad() function
override func viewDidLoad() {
super.viewDidLoad()
registerForKeyboardDidShowNotification(scrollView)
registerForKeyboardWillHideNotification(scrollView)
}
I'm currently work on this and found a solution. First you need to add a notification to the view controller to identify whether the keyboard is on or not. For that you need to register this notification in viewDidLoad().
override func viewDidLoad() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardWillChangeFrameNotification, object: nil)
}
And also don't forget to remove this notification, when the view controller removed from the view. So remove this notification on viewDidDisappear().
override func viewDidDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillChangeFrameNotification, object: nil)
}
And the final thing is to manage the scroll view when the keyboard is on or off. So first you need to identify the keyboard height.Then for pretty smooth animation, you can use keyboard animation mood and duration time to animate your scroll view.
func keyboardDidShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue()
let duration:NSTimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber
let animationCurveRaw = animationCurveRawNSN?.unsignedLongValue ?? UIViewAnimationOptions.CurveEaseInOut.rawValue
let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw)
if endFrame?.origin.y >= UIScreen.mainScreen().bounds.size.height {
//isKeyboardActive = false
UIView.animateWithDuration(duration,
delay: NSTimeInterval(0),
options: animationCurve,
animations: {
// move scroll view height to 0.0
},
completion: { _ in
})
} else {
//isKeyboardActive = true
UIView.animateWithDuration(duration,
delay: NSTimeInterval(0),
options: animationCurve,
animations: {
// move scroll view height to endFrame?.size.height ?? 0.0
},
completion: { _ in
})
}
}
}
#noir_eagle answer seems right.
But there may be is a simpler solution. Maybe you could try using IQKeyboardManager. It allows you to handle these keyboard things in a simple and seamless way.
I think you really should, at least, spend few minutes looking at it.
I need to move a UIView up as soon as the keyboard will become visible. But the problem I'm facing right now is that my UIKeyboardWillShowNotification is called three times when I'm using a custom Keyboard (e.g. SwiftKey) which results in a bad animation.
Is there a way to handle only the last notification? I could easily dodge the first one because the height is 0, but the second one looks like a valid height and I don't find an answer on how to solve this.
Here is what I've so far:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillAppear:", name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillDisappear:", name: UIKeyboardWillHideNotification, object: nil)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func keyboardWillAppear(notification: NSNotification){
print("keyboard appear")
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue() {
print("with height: \(keyboardSize.height)")
if keyboardSize.height == 0.0 {
return
}
self.txtViewBottomSpace.constant = keyboardSize.height
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.layoutIfNeeded()
})
}
}
func keyboardWillDisappear(notification: NSNotification){
print("Keyboard disappear")
self.txtViewBottomSpace.constant = 0.0
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.layoutIfNeeded()
})
}
My Log output is:
keyboard appear
with height: 0.0
keyboard appear
with height: 216.0
keyboard appear
with height: 258.0
Keyboard disappear
So is there any way to only handle the third notification and "ignore" the first two?
Set all bellow fields to NO can resolve this problem.
Capitalizaion: None
Correction: No
Smart Dashes: No
Smart insert: No
Smart Quote: No
Spell Checking: No
Change the notification name UIKeyboardDidShowNotification and UIKeyboardDidHideNotification then solve the problem
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillAppear:", name: UIKeyboardDidShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillDisappear:", name: UIKeyboardDidHideNotification, object: nil)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func keyboardWillAppear(notification: NSNotification){
print("keyboard appear")
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue() {
print("with height: \(keyboardSize.height)")
if keyboardSize.height == 0.0 {
return
}
self.txtViewBottomSpace.constant = keyboardSize.height
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.layoutIfNeeded()
})
}
}
func keyboardWillDisappear(notification: NSNotification){
print("Keyboard disappear")
self.txtViewBottomSpace.constant = 0.0
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.layoutIfNeeded()
})
}
I suggest to replace the static animation duration (0.4) with the animation duration of the keyboard, returned in the userInfo dictionary of the notification under UIKeyboardAnimationDurationUserInfoKey.
In this way your animation will be in sync with the keyboard animation. You can also retrieve the animation curve used by the keyboard with the UIKeyboardAnimationCurveUserInfoKey key.
func keyboardWillAppear(notification: NSNotification){
print("keyboard appear")
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue() {
let animationDuration = notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey]?.doubleValue;
print("with height: \(keyboardSize.height)")
if keyboardSize.height == 0.0 {
return
}
self.txtViewBottomSpace.constant = keyboardSize.height
UIView.animateWithDuration(animationDuration!, delay: 0.0, options: .BeginFromCurrentState, animations: { () -> Void in
self.view.layoutIfNeeded()
})
}
}
func keyboardWillDisappear(notification: NSNotification){
print("Keyboard disappear")
let animationDuration = notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey]?.doubleValue;
self.txtViewBottomSpace.constant = 0.0
UIView.animateWithDuration(animationDuration!, delay: 0.0, options: .BeginFromCurrentState, animations: { () -> Void in
self.view.layoutIfNeeded()
})
}
The reason for this is because keyboards can have different sizes, especially third party ones. So the first notification you receive will always be for the default system height, and any you receive after that will include the new heights of a third party keyboard extension if one is loaded. In order to get around this, in your method that handles the notification, you need to get the height originally, and then set that as a default height (I think 226). Then set a variable to this first height, and then for resulting calls to the notification method you can check if the new height is greater than the original height, and if it is you can find the delta, and then readjust your frames accordingly.