How to persist navigationItem.titleView between screen? - ios

I am using NavigationController to manage three screens. All three screens share a common image as title. I set the image in the viewWillAppear in each VC as follow:
self.navigationItem.titleView = myImageView
The problem is whenever a screen is pushed/popped, the navigation is animated and a new image will come from right or left. I want the image to persist and remain in the middle none-animated.
Is there a way to disable this animation?

It's reproducible, only if you place/setting titleView code in viewWillAppear. it's moving because viewWillAppear called during forth(push) & back(pop) both operation. Set it into viewDidLoad, it will fix this issue
override fun viewDidLoad() {
super.viewDidLoad()
self.navigationItem.titleView = myImageView
}
One more alternate solution for this problem is
var isViewDidLoadCalled = false
override fun viewDidLoad() {
super.viewDidLoad()
isViewDidLoadCalled = true
}
override fun viewWillAppear(_ animated: Bool) {
super. viewWillAppear(animated)
if (isViewDidLoadCalled) {
self.navigationItem.titleView = myImageView
isViewDidLoadCalled = false
}
}
I recommend to use, viewDidLoad to setup your titleView

Another hard but feasible solution is:
You need to use the UINavigationController delegate methods to find out when the UIViewController is being shown. Then for each UIViewController, need to make a boolean variable like isInitialized property, which help you to determine when the UIViewController is being pushed on the stack, or when it's being shown upon back of next view controller.
Your FirstViewController:
func navigationController(_ navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
if viewController == self {
if self.isInitialized {
var navigationBarAnimation = CATransition()
navigationBarAnimation.duration = 1.5
navigationBarAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
navigationBarAnimation.type = kCATransitionFade
navigationBarAnimation.subtype = kCATransitionFade
navigationBarAnimation.removedOnCompletion = true
self.navigationController?.navigationBar?.layer?.addAnimation(navigationBarAnimation, forKey: nil)
} else {
self.isInitialized = true;
}
}
}
func navigationController(_ navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool) {
if viewController == self {
if self.isInitialized {
self.navigationController?.navigationBar?.layer?.removeAllAnimations()
}
}
}
Your SecondViewController:
func navigationController(_ navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
if viewController == self {
if !self.isInitialized {
var navigationBarAnimation = CATransition()
navigationBarAnimation.duration = 1.5
navigationBarAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
navigationBarAnimation.type = kCATransitionFade
navigationBarAnimation.subtype = kCATransitionFade
navigationBarAnimation.removedOnCompletion = true
self.navigationController?.navigationBar?.layer?.addAnimation(navigationBarAnimation, forKey: nil)
self.isInitialized = true;
}
}
}
func navigationController(_ navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool) {
if viewController == self {
if self.isInitialized {
self.navigationController?.navigationBar?.layer?.removeAllAnimations()
}
}
}

Related

Swift. Xcode 10.2.1. Error Thread 1: EXC_BAD_ACCESS (code=2,...) - Navigation between screens

I learn the book iOS Animations by Tutorials
But I don't use Storyboard. I have several ViewControllers created programmatically. I have added RootVC in AppDelegate.swift. This application is working without navigation to the RootVC (going to the beginning) and the screens looks like that:
My question is about how to create such a navigation between different screens (ViewControllers) in Swift 4 (Xcode 10.2.1). It looks like there is an issue with looping... when the last ViewController instantiates the first RootVC and so on...
At the end I would like to have different custom navigation transitions on one ViewController (with .present() and with .navigationController?.pushViewController()
import UIKit
class FadePresentAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let duration = 1.0
var presenting = true
var originFrame = CGRect.zero
var dismissCompletion: (()->Void)?
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
//Setting the transition’s context
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//Adding a fade transition
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
containerView.addSubview(toView)
toView.alpha = 0.0
UIView.animate(withDuration: duration,
animations: {
toView.alpha = 1.0
},
completion: { _ in
transitionContext.completeTransition(true)
}
)
}
}
import UIKit
//UIViewControllerTransitioningDelegate for self.present(self.nextScreen, animated: true, completion: nil)
//UINavigationControllerDelegate for self.navigationController?.pushViewController(self.nextScreen, animated: true)
class Screen3: UIViewController, UIViewControllerTransitioningDelegate, UINavigationControllerDelegate {
let nextScreen = RootVC() //4th Screen //<----- EXEC ERRROR
let transition = FadePresentAnimator()
let btnSize:CGFloat = 56.0
let btn1 = ClickableButton()
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Screen 3"
view.backgroundColor = HexColor.Named.BabyBlue
self.navigationController?.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupLayout()
}
private func setupLayout() {
//btn1:
view.addSubview(btn1)
btn1.setDefaultTitle(title: "▶") // ⏹ "▶" "■"
btn1.apply(height: btnSize)
btn1.apply(width: btnSize)
btn1.applyDefaultStyle()
if #available(iOS 11.0, *) {
btn1.alignXCenter(to: view.safeAreaLayoutGuide.centerXAnchor)
} else {
// Fallback on earlier versions
}
if #available(iOS 11.0, *) {
btn1.alignYCenter(to: view.safeAreaLayoutGuide.centerYAnchor)
} else {
// Fallback on earlier versions
}
btn1.clickHandler {
self.nextScreen.transitioningDelegate = self
//self.present(self.nextScreen, animated: true, completion: nil)
self.navigationController?.pushViewController(self.nextScreen, animated: true)
}
}
//forward
func animationController(forPresented presented: UIViewController,
presenting: UIViewController, source: UIViewController) ->
UIViewControllerAnimatedTransitioning? {
return transition
}
//backward
func animationController(forDismissed dismissed: UIViewController) ->
UIViewControllerAnimatedTransitioning? {
return nil
}
}
//
// AppDelegate.swift
// Anime-Control-01
//
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//No Storyboards!
window = UIWindow(frame:UIScreen.main.bounds)
window?.makeKeyAndVisible()
let rootVC = RootVC() //RootVC.swift
let rootController = UINavigationController(rootViewController: rootVC)
window?.rootViewController = rootController
return true
}
}
It looks like there is an issue with looping... when the last ViewController instantiates the first RootVC and so on...
Yeah, you're totally right. The thing is you're creating next ViewController right when current ViewController is initialized.
The simplest way to fix this is to make nextScreen initialized lazily "on demand" by replacing this line
let nextScreen = RootVC()
by this one lazy var nextScreen = RootVC()
Or to create nextScreen variable right before the transition:
btn1.clickHandler {
let nextScreen = RootVC()
nextScreen.transitioningDelegate = self
navigationController?.pushViewController(nextScreen, animated: true)
}

Bottom Navigation Drawer doesn't show up (Swift)

I currently working on an iOS app and I want to use a bottom navigation drawer from material-io. So I did it like it is explained in the examples on the site. But when I present the navigation Drawer the ViewController only gets a bit darker and the contentView of the drawer isn't shown.
Here is my Code:
import Foundation
import UIKit
import MaterialComponents
class CreateSubjectView: UIViewController, UITextFieldDelegate {
...
override func viewDidLoad() {
...
let bottomDrawerViewController = MDCBottomDrawerViewController()
self.modalPresentationStyle = .popover
let newViewController = self.storyboard?.instantiateViewController(withIdentifier: "TEST")
bottomDrawerViewController.contentViewController = newViewController
present(bottomDrawerViewController, animated: true, completion: nil)
...
}
...
}
Your view controller to be shown in drawer must have specified preferred content size.
Here is a demo of minimal controller. (Note: modalPresentationStyle = .popover has no effect on MDCBottomDrawerViewController)
Tested with Xcode 12
// button action in parent controller
#objc private func presentNavigationDrawer() {
let bottomDrawerViewController = MDCBottomDrawerViewController()
bottomDrawerViewController.contentViewController = DemoViewController()
present(bottomDrawerViewController, animated: true, completion: nil)
}
}
class DemoViewController: UIViewController {
override func loadView() {
super.loadView()
let view = UIView()
view.backgroundColor = .red
self.view = view
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// specify your content preferred height explicitly
self.preferredContentSize = CGSize(width: 0, height: 400) // required !!
}
#available(iOS 11.0, *)
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
// specify your content preferred height explicitly
self.preferredContentSize = CGSize(width: 0, height: 400) // required !!
}
}
Move this to viewWillAppear/ viewDidAppear once as it's too early for viewDidLoad to present a vc
class CreateSubjectView: UIViewController, UITextFieldDelegate {
let bottomDrawerViewController = MDCBottomDrawerViewController()
var once = true
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if once {
let newViewController = self.storyboard?.instantiateViewController(withIdentifier: "TEST")
bottomDrawerViewController.contentViewController = newViewController
present(bottomDrawerViewController, animated: true, completion: nil)
once = false
}
}
}

How to present a landscape only ViewController from portrait bottom of iPhone in swift?

I have portrait only app with one screen in landscape only mode.
What I did:
I created a UINavigationController subclass and overridden the following:
import UIKit
class LandscapeVCNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscape
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .landscapeLeft
}
}
Then using this navigationController to present my landscape viewcontroller like this:
if let vc = storyboard?.instantiateViewController(withIdentifier: StoryBoardIdentifiers.playerVCStoryboardIdentifier) as? PlayerViewController {
let navController = LandscapeVCNavigationController.init(rootViewController: vc)
present(navController, animated: true, completion: {
})
}
This works fine.
What I need:I need the landscape VC to be presented from the bottom(home button side) and should dismiss to the bottom(home button side). This is what happens currently.
You have to add a custom transition animation to change this default behaviour.
let transition = CATransition()
transition.duration = 0.3
transition.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseInEaseOut)
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromLeft
self.view!.window!.layer.add(transition, forKey: nil)
self.present(navController, animated: false, completion:nil)

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?

How to stop UINavigationBar title from animating while push transition

Is there any way to stop the titleView on UINavigationBar to animate when I push/pop view controllers. TitleView for each screen is same (app's logo).
Currently when I push a view, titleView on the navigation bar also slide with the view.
Set the title of the navigation item in each view controller to an empty string and add UILabel subview to the navigation bar:
UILabel *titleLabel = [[UILabel alloc] initWithFrame:self.navigationController.navigationBar.bounds];
titleLabel.font = [UIFont fontWithName:#"Avenir-Roman"
size:20.f];
titleLabel.text = #"TEST TITLE";
titleLabel.textAlignment = NSTextAlignmentCenter;
[self.navigationController.navigationBar addSubview:titleLabel];
This should give you a static title that doesn't move when switching between view controllers.
Try,
[self.navigationController pushViewController:viewController animated:NO];
- (void)viewDidLoad
{
self.navigationItem.title=#"";
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
Swift
A feasible solution is:
Use UINavigationController delegate methods to find out when the UIViewController is being shown. Then for each UIViewController, need to make a boolean variable like isInitialized property, which help you to determine when the UIViewController is being pushed on the stack, or when it's being shown upon back of next view controller.
Your FirstViewController:
func navigationController(_ navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
if viewController == self {
if self.isInitialized {
var navigationBarAnimation = CATransition()
navigationBarAnimation.duration = 1.5
navigationBarAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
navigationBarAnimation.type = kCATransitionFade
navigationBarAnimation.subtype = kCATransitionFade
navigationBarAnimation.removedOnCompletion = true
self.navigationController?.navigationBar?.layer?.addAnimation(navigationBarAnimation, forKey: nil)
}
else
{
self.isInitialized = true;
}
}
}
func navigationController(_ navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool) {
if viewController == self {
if self.isInitialized {
self.navigationController?.navigationBar?.layer?.removeAllAnimations()
}
}
}
Your SecondViewController:
func navigationController(_ navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
if viewController == self {
if !self.isInitialized {
var navigationBarAnimation = CATransition()
navigationBarAnimation.duration = 1.5
navigationBarAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
navigationBarAnimation.type = kCATransitionFade
navigationBarAnimation.subtype = kCATransitionFade
navigationBarAnimation.removedOnCompletion = true
self.navigationController?.navigationBar?.layer?.addAnimation(navigationBarAnimation, forKey: nil)
self.isInitialized = true;
}
}
}
func navigationController(_ navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool) {
if viewController == self {
if self.isInitialized {
self.navigationController?.navigationBar?.layer?.removeAllAnimations()
}
}
}

Resources