Input Accessory View Behave Unexpectedly with keyboard hide and show events - swift

I am facing an strange behavior with InputAccessoryView, I am working on chat screen where I have used InputAccessoryView where I have registered for KeyboardWillShow and KeyboardWillHide notifications. When my chat screen appears it automatically calls the KeyboardWillShowMethod once and after that it hides automatically without calling the KeyboardWillHide notification. After Loading chats when I click on textbox to type text it calls KeyboardWillShow which is fine. But when I try to hide keybaord it calls two methods first it will call KeyboardWillHide and after that it will call KeyboardWillShow which is strange.
This is my chat screen Image when keyboard is hidden.
This is when keyboard is shown Image
I am using this InputAccessoryView Code Programatically inputAccessoryView
This is how I have registered for keyboard notifications.
func handleKeyBoard(){
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)
}
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
var contentInset = self.collectionView?.contentInset
contentInset?.bottom = keyboardSize.maxY
self.collectionView?.contentInset = contentInset!
self.collectionView?.scrollIndicatorInsets = contentInset!
// collectionViewBottomAnchor?.constant = keyboardSize.height + 50
// print ("input height \(inputAccessoryView?.frame.maxY) ")
// print("keyboard height \(keyboardSize.height)")
// print("keyboard Y \(keyboardSize.maxY)")
// print("keyboard Y \(keyboardSize.minY)")
//print("keyboard Y \(inputAccessoryView.framemaxY)")
if self.messages.count > 0{
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
}, completion: { (completed:Bool) in
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
self.collectionView?.scrollToItem(at: indexPath, at: .bottom, animated: true)
})
}
}
}
#objc func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
print("keyboard hide")
self.collectionView?.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: 52, right: 0)
self.collectionView?.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 52, right: 0)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
}, completion: { (completed:Bool) in
})
}
}
In selectors I am trying to change my CollectionView Insets according to a Y Index of Keyboard because I am not getting Height of keybaord that is also an issue. Height of kyeboard is always 50 as of height of inputAccessoryView.

Here is the solution which I have found Thanx to #Amit. Instead of using UIKeyboardFrameBeginUserInfoKey I have used UIKeyboardFrameEndUserInfoKey after doing this I was able to get accurate hight of keyboard in KeyboardWillAppear method. Now the problem which remains was that KeyboardWillShow method was get called after KeyboardWillHide but at that time the KeyboardWillShow have a hight of keyboard 50.
That means when i try to hide a keyboard it will call KeyboardWillHide which is fine and after that It automatically calls KeyboardWillShow but height of keyboard remains 50 so I put condition there.
Now following method will make effect only when height is more than 50.
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
if keyboardSize.height > 50{
var contentInset = self.collectionView?.contentInset
contentInset?.bottom = keyboardSize.height + 50
self.collectionView?.contentInset = contentInset!
self.collectionView?.scrollIndicatorInsets = contentInset!
if self.messages.count > 0{
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
}, completion: { (completed:Bool) in
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
self.collectionView?.scrollToItem(at: indexPath, at: .bottom, animated: true)
})
}
}
}
}
#objc func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
self.collectionView?.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: 52, right: 0)
self.collectionView?.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 52, right: 0)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
}, completion: { (completed:Bool) in
})
}
}

When keyboard has input accessory view, keyboardDidHideNotification is observed twice:
When keyboard is hidden.
When input accessroy view is hidden (note that input accessory view is visible for a while after keyboard is dismissed).
If your implementation depends on selector to be called just once, you can do one of following workarounds:
Option A: Check keyboard frame:
#objc
private func keyboardDidHide(_ notification: Notification) {
guard let keyboardRect = notification.keyboardRect, keyboardRect.origin.y == view.frame.maxY else {
return
}
// Do whatever you need...
}
extension Notification {
var keyboardRect: CGRect? {
guard let keyboardSize = userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
return nil
}
return keyboardSize.cgRectValue
}
}
Option B: Throttle reaction using GDC:
private var pendingKeyboardDidHideRequestWorkItem: DispatchWorkItem?
private func keyboardDidHide(_ notification: Notification) {
pendingKeyboardDidHideRequestWorkItem?.cancel()
let keyboardDidHideRequestWorkItem = DispatchWorkItem { [weak self] in
// Do whatever you need...
}
pendingKeyboardDidHideRequestWorkItem = keyboardDidHideRequestWorkItem
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500), execute: keyboardDidHideRequestWorkItem)
}

Related

Constraint animation doesnt animate y position change

This code is to expand a label by disabling the constraint which is keeping it at a max height of 54 to a full size. It works as it should but it jumps to the new y position straight away (without animating) and changes frame height after which makes it look like it jumps before animating
#IBOutlet var descriptionHeightConstraint: NSLayoutConstraint!
#IBAction func toggleFullDescription(_ sender: Any) {
if descriptionHeightConstraint.isActive {
self.layoutIfNeeded()
descriptionHeightConstraint.isActive = false
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: {
self.layoutIfNeeded()
}, completion: nil)
//seeMoreButton.setTitle("See less", for: .normal)
} else {
self.layoutIfNeeded()
descriptionHeightConstraint.isActive = true
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: {
self.layoutIfNeeded()
}, completion: nil)
//seeMoreButton.setTitle("See more", for: .normal)
}
}
Video of the issue

UIButton resize animation is not animating

I'm trying to make that my button double its size using the UIView animation, but for some reason it is not working it the size goes right, but no animated.
The function that should animate the button resizing
Irrelevant code above
#objc func createButtonPressed(){
//Removes the bottom stack with buttons
if let stackButton = self.view.viewWithTag(50){
stackButton.removeFromSuperview()
}
//Add the button back with half ares size
bottomHolder.addSubview(rightButton)
rightButton.setImage(nil, for: .normal)
rightButton.addTarget(self, action: #selector(anima), for: .touchUpInside)
rightButton.topAnchor.constraint(equalTo: bottomHolder.topAnchor).isActive = true
rightButton.trailingAnchor.constraint(equalTo: bottomHolder.trailingAnchor).isActive = true
rightButton.bottomAnchor.constraint(equalTo: bottomHolder.bottomAnchor).isActive = true
}
#objc func anima(){
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.rightButton.leadingAnchor.constraint(equalTo: self.bottomHolder.leadingAnchor).isActive = true
self.rightButton.layoutIfNeeded()
}, completion: nil)
}
Irrelevant code below
try this one:
let buttonFinalWidth = UIScreen.main.bounds.width
DispatchQueue.main.async {
UIView.animate(withDuration: 5.0) {
self.rightButton.widthAnchor.constraint(equalToConstant: buttonFinalWidth).isActive = true
self.view.layoutIfNeeded()
}
}

Swift - Open UIViewController from within slide-in menu

I have a slide-in menu that presents itself when I press the menu button in my navigation bar. Dismissing it can be accomplished by either tapping the background or the menu itself. Within this view I have several items, one of which is an user profile button. When I press this button, I want the menu to dismiss itself and then instantly open the user profile view controller. That's why I call handleDismiss() first and then set the view that needs to be opened
However, it keeps telling me that the view is not in the window hierarchy. I know what the problem is (view stack), but I somehow can't get it to work properly. How should I tackle this problem?
RootViewController -> HomeController (tabBar index 0) -> Slide-In Menu -> UserProfileController
Button to open the controller
profileCell.profileButton.addTarget(self, action: #selector(handleUserProfile(sender:)), for: .touchUpInside)
Functions
func handleDismiss() {
UIView.animate(withDuration: 0.5) {
self.blackView.alpha = 0
if let window = UIApplication.shared.keyWindow {
self.collectionView.frame = CGRect(x: 0, y: window.frame.height, width: self.collectionView.frame.width, height: self.collectionView.frame.height)
}
}
}
func handleUserProfile(sender: UIButton) {
handleDismiss()
let userProfileController = UserProfileController()
openProfileController(userProfileController)
}
func openProfileController(_ controller: UIViewController) {
present(controller, animated: false, completion: nil)
}
I fixed the problem by setting a reference to the rootViewController in my menu.
HomeController
let menuLauncher = MenuLauncher()
func tappedMenu(sender: UIButton) {
menuLauncher.showMenu(self)
}
MenuController
func MenuLauncher() {
self.m_ParentViewController = nil
}
func showMenu(_ parentViewController: UIViewController) {
self.m_ParentViewController = parentViewController
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(MenuLauncherImageCell.self, forCellWithReuseIdentifier: cellIdImage)
collectionView.register(MenuLauncherInfoCell.self, forCellWithReuseIdentifier: cellIdInfo)
if let window = UIApplication.shared.keyWindow {
blackView.backgroundColor = UIColor(white: 0, alpha: 0.5)
blackView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleDismiss)))
window.addSubview(blackView)
window.addSubview(collectionView)
collectionView.frame = CGRect(x: 0, y: window.frame.height, width: window.frame.width, height: 350)
blackView.frame = window.frame
blackView.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.blackView.alpha = 1
self.collectionView.frame = CGRect(x: 0, y: window.frame.height - 350, width: self.collectionView.frame.width, height: self.collectionView.frame.height)
}, completion: nil)
}
}
func handleDismiss() {
UIView.animate(withDuration: 0.5) {
self.blackView.alpha = 0
if let window = UIApplication.shared.keyWindow {
self.collectionView.frame = CGRect(x: 0, y: window.frame.height, width: self.collectionView.frame.width, height: self.collectionView.frame.height)
}
}
}
func handleUserProfile(sender: UIButton) {
handleDismiss()
let userProfileController = UserProfileController()
self.m_ParentViewController?.present(userProfileController, animated: true, completion: nil)
}

Switching between UITextViews resets view Layout

I implemented some functions to move the UIView, whenever a TextField or a TextView is tapped. The height for the UIView to move upwards is calculated depending how much the keyboard would overlap the active TextField or TextView. When I tap outside of the TextField, or -View the keyboard will be dismissed and the View will be resetted. Now everything is working fine, but when I switch from one TextField directly to another above (without dismissing the keyboard) it seems like the UIView will return to the initial position, instead of just keeping the shifted view (because the now active TextView would not be overlapped by the keyboard since it is above the former). It looks like some method is called to reset the view, resulting in the keyboard overlapping the upper TextView. Is there a way to suppress this behavior?
private func observeKeyboardNotification(){
NotificationCenter.default.addObserver(self, selector: #selector(keyboardShow), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardHide), name: .UIKeyboardWillHide, object: nil)
}
var distanceOfKeyboardToTextView: CGFloat = 0
var activeTextElement: UIView?
var viewIsShifted = false
func keyboardShow(notification: NSNotification){
findActiveTextField(subviews: self.view.subviews, textField : &activeTextElement)
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue, let activeTextElement = activeTextElement, !viewIsShifted {
let viewYPosition = (activeTextElement.superview?.convert(activeTextElement.frame.origin, to: nil).y)! + activeTextElement.frame.height
let keyboardYPosition = view.frame.height - keyboardSize.height
distanceOfKeyboardToTextView = viewYPosition - keyboardYPosition
if distanceOfKeyboardToTextView > 0 {
UIView.animate(withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 1, options: .curveEaseOut,
animations: {
self.customView.frame = CGRect(x: 0, y: self.customView.frame.origin.y - self.distanceOfKeyboardToTextView, width: self.customView.frame.width, height: self.customView.frame.height)
}, completion: nil)
viewIsShifted = true
}
}
activeTextElement = nil
}
func keyboardHide(){
UIView.animate(withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 1, options: .curveEaseOut,
animations: {
self.customView.frame = CGRect(x: 0, y: self.customView.frame.origin.y + self.distanceOfKeyboardToTextView, width: self.customView.frame.width, height: self.customView.frame.height)
}, completion: nil)
viewIsShifted = false
distanceOfKeyboardToTextView = 0
}
func findActiveTextField (subviews : [UIView], textField : inout UIView?) {
guard textField == nil else { return }
for view in subviews {
if view.isFirstResponder {
textField = view
break
}
else if !view.subviews.isEmpty {
findActiveTextField (subviews: view.subviews, textField: &textField)
}
}
}
Update:
After tapping from an active textfield directly onto another textfield, keyboardShow is call, but since the view is already shifed, UIView.animate will not be performed. However, the view is resetted like no keyboard would be displayed, but since the other textfield is active, the keyboard is visible.
I dismiss the keyboard using this extension:
extension UIViewController {
//functions to hide the keyboard
func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
}
func dismissKeyboard() {
view.endEditing(true)
}
}
and in my ViewController:
override func viewDidLoad() {
super.viewDidLoad()
observeKeyboardNotification()
self.hideKeyboardWhenTappedAround()
...
}

How to fix `prepareForSegue()` override `UIViewAnimation`

I have MainViewController which has stack view and its width is over screen's width. There is slide out menu at left side of storyboard. And there is Menu button on the screen which triggers an animation to changes UIStackView's from -260 to 0. But when I click some button at slide out menu, prepareForSegue() overrides the animation and content changes immediately. How can fix that?
You can find a gif below.
Here is the codes:
class MainViewController: UIViewController
{
var container: ContainerViewController?
#IBOutlet weak var superView: UIStackView!
#IBAction func menuButtonsChangeContent(sender: AnyObject) {
switch(sender.tag){
case 1:
container?.changeContent("first")
case 2:
container?.changeContent("second")
case 3:
container?.changeContent("third")
default:
break
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "Container"{
self.slideOutMenu("")
container = segue.destinationViewController as? ContainerViewController
}
}
#IBAction func slideOutMenu(sender: AnyObject) {
if self.superView.frame.origin.x != -260 {
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0.5, options: .CurveEaseInOut, animations: ({
self.superView.frame = CGRect(x: -260, y: 20, width: self.superView.frame.width, height: self.superView.frame.height)
}), completion: nil)
}else{
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0.5, options: .CurveEaseInOut, animations: ({
self.superView.frame = CGRect(x: 0, y: 20, width: self.superView.frame.width, height: self.superView.frame.height)
}), completion: nil)
}
}
}
//ContainerViewController.swift
class ContainerViewController: UIViewController
{
var sourceVC: UIViewController?
var destinationVC: UIViewController?
var segueIdentifier: String?
var counter = 0
func changeContent(segueIdentifier: String)
{
self.performSegueWithIdentifier(segueIdentifier, sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0.5, options: .CurveEaseInOut, animations: ({
if self.sourceVC != nil {
self.sourceVC?.view.removeFromSuperview()
print("deleted")
}
self.destinationVC = segue.destinationViewController
self.addChildViewController(self.destinationVC!)
self.destinationVC?.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)
self.view.addSubview((self.destinationVC?.view)!)
self.destinationVC?.didMoveToParentViewController(self)
self.sourceVC = self.destinationVC
}), completion: nil)
}
}
And Storyboard:
GIF:
I was using a UIStackView as super view in MainViewController but an issue about shadows came up and I changed UIStackView to UIView. Maybe it is weird but the problem is gone.