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.
Related
I would like a smooth animation of this view whenever the search bar is selected and deselected. Right now it's choppy:
Heres my code below in the searchResultsUpdater. From what I understand, these functions should handle the animations, I'm not sure what's wrong here:
func updateSearchResults(for searchController: UISearchController) {
//MapView moves up when search bar is selected
if searchController.isActive == true{
UIView.animateKeyframes(withDuration: 0.25, delay: 0.0, options: UIView.KeyframeAnimationOptions(rawValue: 7), animations: {
self.mapView.frame.origin.y=self.view.safeAreaLayoutGuide.layoutFrame.origin.y
},completion: nil)
}
//MapView moves down when cancel button is selected
if searchController.isActive == false{
UIView.animateKeyframes(withDuration: 0.25, delay: 0.0, options: UIView.KeyframeAnimationOptions(rawValue: 7), animations: {
self.mapView.frame.origin.y=self.view.safeAreaLayoutGuide.layoutFrame.origin.y
},completion: nil)
}
}
Any help is appreciated, thank you!
I found the solution. Originally I had the CGRect for the mapView frame in viewDidLayoutSubviews():
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//layout to fit frame of view
mapView.frame = CGRect(x: 0, y: view.safeAreaInsets.top, width:
view.frame.size.width, height: view.frame.size.height -
view.safeAreaInsets.top)
}
I moved it to viewDidLoad(), and now it works:
override func viewDidLoad() {
super.viewDidLoad()
//add gesture recognizer for mapView
mapView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(mapViewAnimation)))
//layout to fit frame of view
mapView.frame = CGRect(x: 0, y: view.safeAreaInsets.top, width:
view.frame.size.width, height: view.frame.size.height -
view.safeAreaInsets.top)
}
I also added a gesture recognizer instead so that the searchResultsUpdater wouldn't be so cluttered:
//handle the animation for mapview
#objc func mapViewAnimation(){
UIView.animateKeyframes(withDuration: 0.25, delay: 0.0, options:
UIView.KeyframeAnimationOptions(rawValue: 7), animations: {
self.mapView.frame.origin.y =
self.view.safeAreaLayoutGuide.layoutFrame.origin.y
},completion: nil)
}
If anyone knows why it didn't work when I had the CGRect in didLayoutSubViews() feel free to let me know. Thanks!
After launch screen dismisses itself logo and title of my app (they are in container) should go closer to the top of the screen. Between dismissing launch screen and viewDidAppear method there is a strange 'blink' of my container in the background. As you can see I am using snapkit but it should have nothing to do with the problem. Here is my code:
class WelcomeScreenViewController: UIViewController {
var welcomeScreenView: WelcomeScreenView {
return view as! WelcomeScreenView
}
override func loadView() {
let contentView = WelcomeScreenView(frame: .zero)
view = contentView
}
override func viewDidLoad() {
super.viewDidLoad()
self.welcomeScreenView.checkWeatherButton.transform = CGAffineTransform(translationX: 0, y: 200)
self.welcomeScreenView.checkWeatherButton.addTarget(self, action: #selector(showCityChoiceVC), for: .touchUpInside)
navigationController?.isNavigationBarHidden = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.welcomeScreenView.appNameLogoContainerVerticalConstraint?.isActive = false
self.welcomeScreenView.appNameLogoContainer.snp.makeConstraints({ (make) in
make.top.equalTo(self.welcomeScreenView).offset(100)
})
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: [], animations: {
self.welcomeScreenView.layoutIfNeeded()
self.welcomeScreenView.checkWeatherButton.transform = CGAffineTransform(translationX: 0, y: 0)
}, completion: nil)
}
#objc private func showCityChoiceVC() {
self.navigationController?.pushViewController(RegisterViewController(), animated: true)
}
Blinking comes from setting constraints in viewDidAppear. Use viewWillAppear or viewDidLoad instead. viewDidAppear is invoked when your view actually appears on screen. So any changes that happen will be visible to the user.
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)
}
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)
}
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()
...
}