large titles with search bar animation glitch - ios

When going back from view controller with:
navigationItem.largeTitleDisplayMode = .never
to view controller with:
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .always
in viewDidLoad I have a strange animation glitch on navigation bar - a white stripe appears
It's appears only when searchBar is visible
How can I fix this?

I found problem in appear transition, there are some clear space between navigationbar and searchbar. They have independent animation. Here is it.
So, you can add common background for them throw transition in "viewWillAppear" method using transitionCoordinator. That its view.
edited: strong text add logic, when your transition from search table, and navigation is hide at this moment. (searchController.hidesNavigationBarDuringPresentation = true)
override func viewWillAppear(_ animated: Bool) {
let navigationBack = UIView()
if navigationItem.searchController?.isActive == false {
navigationBack.frame = self.navigationController?.navigationBar.frame ?? CGRect.zero
}
navigationBack.backgroundColor = navigationController?.navigationBar.barTintColor
let containerView = transitionCoordinator?.containerView
transitionCoordinator?.animateAlongsideTransition(in: containerView, animation: { (context) in
containerView?.addSubview(navigationBack)
navigationBack.frame.size.height += self.navigationItem.searchController?.searchBar.frame.height ?? 0
}, completion: { (context) in
navigationBack.removeFromSuperview()
})
super.viewWillAppear(animated)
}

Set background color of navigation controller view to the same color of navigation bar seems to work around the issue for me.
navigationController?.view.backgroundColor = navigationController?.navigationBar.barTintColor

What helped was to add this in viewController with largeTitles enabled:
override func viewWillAppear(_ animated: Bool) {
let navigationBack = UIView()
navigationBack.frame = (self.navigationController?.navigationBar.frame)!
navigationBack.frame.size.height = 44
navigationBack.backgroundColor = navigationController?.navigationBar.barTintColor
let containerView = transitionCoordinator?.containerView
transitionCoordinator?.animateAlongsideTransition(in: containerView, animation: { (context) in
containerView?.addSubview(navigationBack)
navigationBack.frame.size.height = (self.navigationItem.searchController?.searchBar.frame.height)! + (self.navigationController?.navigationBar.frame.height)!
}, completion: { (context) in
navigationBack.removeFromSuperview()
})
super.viewWillAppear(animated)
}
and this in viewController with largeTitles disabled:
override func viewWillAppear(_ animated: Bool) {
let navigationBack = UIView()
navigationBack.frame = self.navigationController?.navigationBar.frame ?? CGRect.zero
navigationBack.backgroundColor = navigationController?.navigationBar.barTintColor
let containerView = transitionCoordinator?.containerView
transitionCoordinator?.animateAlongsideTransition(in: containerView, animation: { (context) in
containerView?.addSubview(navigationBack)
navigationBack.frame.size.height = 44
}, completion: { (context) in
navigationBack.removeFromSuperview()
})
super.viewWillAppear(animated)
}

Related

Navigation Controller pushing content down

I am having this issue where the content of my view controller is pushed down after pushing a view controller to the navigation controller and then popping that view controller.
The base view controller is a storyboard, and then I use that as a container for a view controller I created via XIB
This is how I set up the base view controller, in my viewDidLoad I call this function:
func loadScoresView(){
if scoresVC.parent != nil {
refresh()
return
}
scoresVC.view.removeFromSuperview()
scoresVC = LiveScoresViewController(nibName: "LiveScoresViewController", bundle: nil)
scoresVC.delegate = self
let collectionViewHeight:CGFloat = 80
scoresVC.view.frame = CGRect(x: view.bounds.minX, y: view.bounds.minY + collectionViewHeight, width: view.bounds.width, height: view.bounds.height - collectionViewHeight)
scoresVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.view.addSubview(scoresVC.view)
scoresVC.didMove(toParent: self)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
navigationController?.navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
navigationController?.navigationBar.shadowImage = UIImage()
navigationController?.view.setNeedsLayout() // force update layout
navigationController?.view.layoutIfNeeded() // to fix height of the navigation bar
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.interactivePopGestureRecognizer?.delegate = nil
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}

Status bar color won't change no matter what

I'm trying to change the status bar color for one specific screen in my application.
The background in that screen is dark so I'm trying to use the white status bar instead.
I tried using:
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 0.5, animations: {
self.navigationController?.navigationBar.barStyle = .black
}, completion: nil)
}
Which did worked at first, But I added a TabBarController to my application and there I added 4 navigation controllers (one for each item), this is how it looks:
override func viewDidLoad() {
super.viewDidLoad()
let homeController = HomeVC()
let homeNavigationController = UINavigationController(rootViewController: homeController)
homeNavigationController.navigationBar.isHidden = true
let favoritesController = WishlistVC()
let favoritesNavigationController = UINavigationController(rootViewController: favoritesController)
favoritesNavigationController.navigationBar.isHidden = true
let cartController = CartVC()
let cartNavigationController = UINavigationController(rootViewController: cartController)
cartNavigationController.navigationBar.isHidden = true
let storeController = BaseStoreVC()
let storeNavigationController = UINavigationController(rootViewController: storeController)
storeNavigationController.navigationBar.isHidden = true
homeController.tabBarItem = UITabBarItem(title: home_string, image: UIImage(named: "home"), tag: 0)
favoritesController.tabBarItem = UITabBarItem(title: favorites_string, image: UIImage(named: "favorites"), tag: 1)
cartController.tabBarItem = UITabBarItem(title: cart_string, image: UIImage(named: "cart"), tag: 2)
storeController.tabBarItem = UITabBarItem(title: store_string, image: UIImage(named: "store"), tag: 3)
let tabBarItems = [homeNavigationController, favoritesNavigationController, cartNavigationController, storeNavigationController]
viewControllers = tabBarItems
setupTabBarAppearance()
}
Since then, the status bar color on the screen stays black.
The screen I'm trying to change belongs to the homeNavigationController (not the homeController though).
I move from the homeController to the screen I'm trying to change like this:
let vc = ProductDetailVC()
self.navigationController?.pushViewController(vc, animated: true)
I tried using this in the ProductDetailVC() too:
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
Which also didn't work.
The right to govern the status bar style belongs to the top view controller. Since you changed your architecture, that is now the tab bar controller. It relegates the decision to its current child, which is a navigation controller. Only the tab bar controller or its current child has any say in the matter. So it is the home navigation controller’s preferredStatusBarStyle that counts. The product detail VC is not consulted. That explains why your attempt to use preferredStatusBarStyle fails.
However, that was never going to work anyway, because, as you already know, the way a navigation controller decides its status bar coloring is based on the nav bar's bar style. In the architecture you describe, it worked for me to have the product VC set the bar style:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
UIView.animate(withDuration: 0.5, animations: {
self.navigationController?.navigationBar.barStyle = .black
}, completion: nil)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
UIView.animate(withDuration: 0.5, animations: {
self.navigationController?.navigationBar.barStyle = .default
}, completion: nil)
}

Make a transparent navigation bar on push and restore it on pop

I'm trying to create an effect that even though it's close to what I desire but it has some UI glitches which I'll explain.
I have, let's say, my Home navigation controller which I tap a cell that pushes a new view controller.
On that view controller's viewWillAppear(:) I've implemented the following:
self.navigationController?.navigationBar.isTranslucent = true
self.navigationController?.navigationBar.backgroundColor = .clear
self.navigationController?.navigationBar.tintColor = .white
self.navigationController?.navigationBar.barTintColor = .clear
self.navigationController?.navigationBar.shadowImage = UIImage()
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
By doing this, the pushed view controller will have its navigationBar transparent, and still keeps the buttons visible (which is what I desire), but on the push animation, it shows a black bar on the parent controller, because it hides the parent's navigationBaras well.
And then on the pushed view controllers viewWillDisappear(_:) I've implemented the following:
self.navigationController?.navigationBar.shadowImage = nil
self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
self.navigationController?.navigationBar.isTranslucent = false
self.navigationController?.navigationBar.backgroundColor = .white
self.navigationController?.navigationBar.barTintColor = .white
By doing this, I'm trying to reset the parent's navigationBar default properties, but by doing so I see a black bar during the animation, before it completes the animation, which causes a bad UI/UX.
Am I doing something wrong here, or there is any better approach on this?
Thank you.
So after some digging and some pretty useful hints from #Paulo I have managed to solve this as I wanted to.
This is something that should be way more simple to achieve, and Apple should give developers that simplicity option and not tweaking around some hack to achieve it, but anyway.
I found that one of the secret was that I was abusing the navigationBar.isTranslucent = true / false when navigating through the view controllers.
In order to do this I set the default navigationBar properties in the parentViewController, the one that will push to the view controller with the transparent navigationBar; I've done it as the following:
self.navigationController?.navigationBar.backgroundColor = .white
self.navigationController?.navigationBar.barTintColor = .white
self.navigationController?.navigationBar.shadowImage = nil
self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
On the pushedViewController viewWillAppear(_:) you need to implement the following:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard self.navigationController?.topViewController === self else { return }
self.transitionCoordinator?.animate(alongsideTransition: { [weak self](context) in
self?.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
self?.navigationController?.navigationBar.shadowImage = UIImage()
self?.navigationController?.navigationBar.backgroundColor = .clear
self?.navigationController?.navigationBar.barTintColor = .clear
}, completion: nil)
}
Here I set the desired navigationBar transparency, but as you notice, no need to use the isTranslucent property, I noticed by forcing it the UI would show some flickering and weird layout on the push animation.
Then on the same view controller (pushed) you need to implement the default, desired, navigationBar properties that you've implemented in the parentViewController:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.transitionCoordinator?.animate(alongsideTransition: { [weak self](context) in
self?.navigationController?.navigationBar.setBackgroundImage(nil, for: UIBarMetrics.default)
self?.navigationController?.navigationBar.shadowImage = nil
self?.navigationController?.navigationBar.backgroundColor = .white
self?.navigationController?.navigationBar.barTintColor = .white
}, completion: nil)
}
And by doing this everything should work as expected.
Hope it helps someone in the future.
After DAYS of figuring this out I think I've come up with the best (but far from perfect) solution for this. This way you can customize navigation bar when transition takes place and also handle interrupted transitions, like when user cancels popping back.
func yourCustomizationMethod(bar: UINavigationBar) {
// Modify the navigation bar
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard navigationController?.topViewController == self else {
return
}
// The coordinator is nil in some cases
if let coordinator = transitionCoordinator,
coordinator.animate(alongsideTransition: { context in
guard let bar = self.navigationController?.navigationBar else {
return
}
self.yourCustomizationMethod(bar: bar)
}, completion: nil) {
return
} else if let bar = navigationController?.navigationBar {
yourCustomizationMethod(bar: bar)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Customize only if user transition gets canceled to avoid multiple customizations.
guard transitionCoordinator?.isCancelled == true,
let bar = navigationController?.navigationBar else {
return
}
yourCustomizationMethod(bar: bar)
}

UINavigationBar change colors on push

I'm using 2 different bar tint colors at UINavigationBar in different views. I'n changing color with that method in both views:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.barTintColor = COLOR
}
When I tap on back button color is not changed smoothly (you can see blink on last second).
But everything is okay if just swipe view back instead of tapping on back button.
How to make smooth transition in both situations?
To achieve this kind of animation you should use UIViewControllerTransitionCoordinator as Apple documentation say it is :
An object that adopts the UIViewControllerTransitionCoordinator protocol provides support for animations associated with a view controller transition.(...)
So every UIViewController has own transitionController. To get this you should call in the UIViewControllerClass :
self.transitionCoordinator()
From documentation:
Returns the active transition coordinator object.
So to get the result that you want you should implement animateAlongsideTransition method in viewController transitionCoordinatior. Animation works when you click backButton and swipe to back.
Example :
First Controller :
class ViewControllerA: UIViewController {
override func loadView() {
super.loadView()
title = "A"
view.backgroundColor = .white
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "NEXT", style: .plain, target: self, action: #selector(self.showController))
setColors()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
animate()
}
func showController() {
navigationController?.pushViewController(ViewControllerB(), animated: true)
}
private func animate() {
guard let coordinator = self.transitionCoordinator else {
return
}
coordinator.animate(alongsideTransition: {
[weak self] context in
self?.setColors()
}, completion: nil)
}
private func setColors() {
navigationController?.navigationBar.tintColor = .black
navigationController?.navigationBar.barTintColor = .red
}
}
Second Controller:
class ViewControllerB : UIViewController {
override func loadView() {
super.loadView()
title = "B"
view.backgroundColor = .white
setColors()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
animate()
}
override func willMove(toParentViewController parent: UIViewController?) { // tricky part in iOS 10
navigationController?.navigationBar.barTintColor = .red //previous color
super.willMove(toParentViewController: parent)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.navigationBar.barTintColor = .blue
}
private func animate() {
guard let coordinator = self.transitionCoordinator else {
return
}
coordinator.animate(alongsideTransition: {
[weak self] context in
self?.setColors()
}, completion: nil)
}
private func setColors(){
navigationController?.navigationBar.tintColor = .black
navigationController?.navigationBar.barTintColor = .blue
}
}
UPDATE iOS 10
In the iOS 10 the tricky part is to add the willMoveTo(parentViewController parent: UIViewController?) in the second ViewController. And set the navigationBar tintColor to the color value of previous controller. Also, in viewDidAppear method in second ViewControler set the navigationBar.tintColor to the color from second viewController.
Check out my example project on github
I've coded final solution that looks most comfortable to use (don't need to use a lot of overrides in own view controllers). It works perfectly at iOS 10 and easy adoptable for own purposes.
GitHub
You can check GitHub Gist for full class code and more detailed guide, I won't post full code here because Stackoverflow is not intended for storing a lot of code.
Usage
Download Swift file for GitHub. To make it work just use ColorableNavigationController instead of UINavigationController and adopt needed child view controllers to NavigationBarColorable protocol.
Example:
class ViewControllerA: UIViewController, NavigationBarColorable {
public var navigationBarTintColor: UIColor? { return UIColor.blue }
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Push", style: .plain, target: self, action: #selector(self.showController))
}
func showController() {
navigationController?.pushViewController(ViewControllerB(), animated: true)
}
}
class ViewControllerB: UIViewController, NavigationBarColorable {
public var navigationBarTintColor: UIColor? { return UIColor.red }
}
let navigationController = ColorableNavigationController(rootViewController: ViewControllerA())
This worked for me:
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
navigationController?.navigationBar.barTintColor = previous view controller's navigation bar color
}
I am just wondering. For the same purpose I use UINavigationControllerDelegate. In navigationController(_:willShow:) I start the animation using transitionCoordinator?.animate(alongsideTransition:completion:). It works great when pushing new controllers, however pop doesn't.
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
let dst = viewController as! ViewController
guard animated else {
navigationController.navigationBar.barTintColor = dst.navigationBarColor
navigationController.navigationBar.tintColor = dst.tintColor
navigationController.navigationBar.barStyle = dst.barStyle
return
}
navigationController.transitionCoordinator?.animate(alongsideTransition: { context in
navigationController.navigationBar.barTintColor = dst.navigationBarColor
navigationController.navigationBar.tintColor = dst.tintColor
navigationController.navigationBar.barStyle = dst.barStyle
}, completion: { context in
if context.isCancelled {
let source = context.viewController(forKey: UITransitionContextViewControllerKey.from) as! ViewController
navigationController.navigationBar.barTintColor = source.navigationBarColor
navigationController.navigationBar.tintColor = source.tintColor
navigationController.navigationBar.barStyle = source.barStyle
}
})
Do you see any reason why it should work with pushes but not pops?

Modal view controller does not cover status bar

I'm developing an ios app. I have a a main view and in this view
im trying to present a modal view controller with dimmed background(black with opacity).
The problem is that the status bar is not affected by this color and remains the same.
This is how i present the view controller:
let shareViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ShareViewController") as! ShareViewController
shareViewController.battle = battle
shareViewController.delegate = self
let animation = CATransition()
animation.duration = 1
animation.type = kCATransitionFade
self.view.window?.layer.addAnimation(animation, forKey: kCATransition)
presentViewController(shareViewController, animated: false) {
() in
// nothing here
}
Here are some screenshots to demonstrate the problem:
This is the problem(status bar color):
Problem illustration
This is the modal view in storyboard:
storyboard
I cannot reproduce your problem, the following code works without problems in my single view app:
let viewController = UIViewController()
viewController.modalPresentationStyle = .overFullScreen
viewController.view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
let animation = CATransition()
animation.duration = 1
animation.type = kCATransitionFade
self.view.window?.layer.add(animation, forKey: kCATransition)
self.present(viewController, animated: false, completion: nil)
However note that you should be presenting over the root controller of the view. Sometimes you can get strange effects when presenting from your internal controllers:
self.view.window?.rootViewController?.present(viewController, animated: false, completion: nil)
Also make sure you are using the correct modalPresentationStyle.
Set your view controller as the root view controller of a UIWindow, then present the window at the UIWindowLevelAlert level.
Below is a Swift 3 class used to animate a modal popup over all other UI elements, including the status bar. A scrim view is used to shade background UI and intercept touches to dismiss the view.
import UIKit
class ModalViewController: UIViewController {
private let scrimView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.black
view.alpha = 0.0
return view
}()
private var myWindow: UIWindow?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.clear
// Setup scrim View
view.addSubview(scrimView)
view.topAnchor.constraint(equalTo: scrimView.topAnchor).isActive = true
view.leftAnchor.constraint(equalTo: scrimView.leftAnchor).isActive = true
view.rightAnchor.constraint(equalTo: scrimView.rightAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: scrimView.bottomAnchor).isActive = true
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismiss as (Void) -> Void))
scrimView.addGestureRecognizer(tapGestureRecognizer)
// Layout custom popups or action sheets
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.25) {
self.scrimView.alpha = 0.5
// Animate in custom popups or action sheets
}
}
func present() {
myWindow = UIWindow(frame: UIScreen.main.bounds)
myWindow?.windowLevel = UIWindowLevelAlert
myWindow?.backgroundColor = UIColor.clear
myWindow?.rootViewController = self
myWindow?.isHidden = false
}
func dismiss() {
UIView.animate(
withDuration: 0.25,
animations: {
self.scrimView.alpha = 0.0
// Animate out custom popups or action sheets
},
completion: { success in
self.myWindow = nil
}
)
}
}
To present the view:
let modalView = ModalViewController()
modalView.present()
To dismiss the view, tap anywhere on the scrim.
this code works for me, when I am presenting UIViewController with alpha != 1. present UIViewController like:
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let destinationVC = storyBoard.instantiateViewController(withIdentifier: "AddComment") as! AddCommentViewController
destinationVC.modalPresentationStyle = .overCurrentContext //this line is important
destinationVC.delegate = self
destinationVC.restId = self.restaurant.id
self.present(destinationVC, animated: true, completion: nil)
then in destinationVC view controller
override func viewWillDisappear(_: Bool) {
UIView.animate(withDuration: 1, animations: { () in
self.view.backgroundColor = .clear
})
super.viewWillDisappear(true)
}
override func viewWillAppear(_: Bool) {
UIView.animate(withDuration: 1, animations: { () in
self.view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
})
super.viewWillAppear(true)
}
and set its backgroundColor to .clear in viewDidLoad or storyboard. So UIViewController covers whole screen including status bar.
Here is the solution you might be looking for:
if let window = UIApplication.shared.keyWindow {
window.windowLevel = UIWindowLevelStatusBar + 1
}
The main idea behind this code is, window of your application has a window level which is lower than status bar window level. And what this code does is, just put your window's window level higher than status bar window level, and your window can now cover the status bar. Don't forget, this code has to be called on main thread, just before presenting your view controller. Good luck!
Custom animation transitions should be performed using UIViewControllerAnimatedTransitioning. Here is a tutorial for this purpose:
https://www.raywenderlich.com/110536/custom-uiviewcontroller-transitions
If all you want is a fade animation you can have it by changing the modalTransitionStyle property of the viewController you are going to display.
Try by fixing your code this way:
guard let shareViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ShareViewController") as! ShareViewController else {
//Fallback in case of nil?
return
}
shareViewController.modalTransitionStyle = .crossDissolve
presentViewController(shareViewController, animated: true, completion: nil)
Also please note that presentViewController(shareViewController, animated: true, completion: nil) is for swift 2. The equivalent swift 3 would be present(shareViewController, animated: true, completion: nil)
you can add this code to view controller for Swift 3:
let statusView: UIView = UIView(frame: CGRect(x: 0.0, y: -20.0, width: UIScreen.main.bounds.size.width, height: 20.0))
statusView.backgroundColor = UIColor.black
statusView.alpha = 0.8
self.addSubview(self.statusView)
You could be extremely practical and simply hide the status bar when your modal view controller is up:
override func prefersStatusBarHidden() -> Bool {
return true
}

Resources