UIViewcontroller disappear after switch tabs UITabBarController - ios

I'm working on an app that uses UITabBarController and sudenlly one tabItem stops to appear. Then I start to investigate and end up with a problem related with UISearchController and UITabBarController.
To isolate the problem a build a simple app to demostrate the situation.
This is how I instanciate the TabBar at didFinishLaunchingWithOptions :
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.backgroundColor = .white
let first:SearchController = {
let sc = SearchController()
sc.tabBarItem = UITabBarItem(title: "First", image: UIImage(named:"iphone"), tag: 0)
return sc
}()
let second:SecondViewController = {
let s = SecondViewController()
s.tabBarItem = UITabBarItem(title: "Second", image: UIImage(named:"iphone"), tag: 1)
return s
}()
let tabBar = UITabBarController()
let controllers = [first, second]
tabBar.viewControllers = controllers.map {
UINavigationController(rootViewController: $0)
}
self.window?.rootViewController = tabBar
self.window?.makeKeyAndVisible()
return true
}
This is the viewController with UISearchController:
class SearchController: UIViewController {
var matchingItems:[String] = [] {
didSet{
self.tableView.reloadData()
}
}
lazy var searchController:UISearchController = UISearchController(searchResultsController: nil)
lazy var tableView: UITableView = { [unowned self] in
let tv = UITableView(frame: CGRect.zero, style: .grouped)
tv.translatesAutoresizingMaskIntoConstraints = false
tv.delegate = self
tv.dataSource = self
tv.register(UITableViewCell.self, forCellReuseIdentifier: "id")
return tv
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "First"
view.addSubview(self.tableView)
self.navigationItem.searchController = searchController
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.perform(#selector(showKeyboard), with: nil, afterDelay: 0.1)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
searchController.searchBar.resignFirstResponder()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let layout = self.view.safeAreaLayoutGuide
tableView.topAnchor.constraint(equalTo: layout.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: layout.bottomAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: layout.rightAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo: layout.leftAnchor).isActive = true
}
#objc func showKeyboard() {
self.searchController.searchBar.becomeFirstResponder()
self.searchController.searchBar.text = ""
}
}
In run time, when system finished rendering this view controller, the debug console print this:
SearchBar+TabBar[15454:895018] [MC] Reading from private effective user settings.
2018-04-22 12:07:19.595887-0300 SearchBar+TabBar[15454:895018] +[CATransaction synchronize] called within transaction
2018-04-22 12:07:19.608090-0300 SearchBar+TabBar[15454:895018] +[CATransaction synchronize] called within transaction
2018-04-22 12:07:19.608269-0300 SearchBar+TabBar[15454:895018] +[CATransaction synchronize] called within transaction
2018-04-22 12:07:19.608516-0300 SearchBar+TabBar[15454:895018] +[CATransaction synchronize] called within transaction
Searching on the stackoverflow I found that this message +[CATransaction synchronize] called within transaction
is related to rendering more than one animation in the main thread.
I'm wondering if this viewcontroller visualisation problem is related to that. So I commented the searchController instantiation line in SearchController class:
self.navigationItem.searchController = searchController
Now the UITabBarController works perfectly without the UISearchController in my first view controller.
My questions are:
Is there another way to instanciate UISearchController to avoid
this?
If yes, how should I do that?
GitHub repository with the sample code: sample code

In Your TabBarController class implement this function:
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool
In this function you should check if Controller with searchBar was selected before and make SearchController inactive
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if (tabBarController.selectedIndex == ControllerWithSearchBarIndex) {
((viewControllers![ControllerWithSearchBarIndex] as! UINavigationController).viewControllers.first! as! ControllerWithSearchBarIndex).searchController.isActive = false
}
return true
}

A friend of my suggests to add UITabBarController as root of UINavigationController in the at didFinishLaunchingWithOptions and it works perfectly and solve the problem:
let tabBar = UITabBarController()
let controllers = [first, second]
tabBar.viewControllers = controllers.map {
UINavigationController(rootViewController: $0)
}
let nav = UINavigationController(rootViewController: tabBar)
nav.isNavigationBarHidden = true
nav.navigationBar.isUserInteractionEnabled = false

Set definesPresentationContext = true on the view controller with the tableView (SearchController in your example). This will allow the UISearchController to remain active when you switch tabs.

Related

CollectionView `DidSelect` method is being called but not pushing the next VC, how to fix it?

I am trying to push another VC from UICollectionView DidSelect delegate, it is being called,
but oddly, it is not pushing the initiated VC, how can i fix it?
my AppDelegate.swift looks like this:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
weak var coordinator: MainCoordinator?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
self.window = UIWindow(frame: UIScreen.main.bounds)
coordinator = MainCoordinator(navigationController: UINavigationController())
self.window?.rootViewController = UIStoryboard.splashViewController()
self.window?.makeKeyAndVisible()
return true
}
}
and my SplashViewController, where I switch the RootVC to MainVC in DispatchQueue after the SplashViewController completed:
class SplashViewController: UIViewController {
weak var coordinator: MainCoordinator?
//MARK: -viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
// MARK: - viewDidAppear()
override func viewDidAppear(_ animated: Bool) {
let animationView = AnimationView()
let animation = Animation.named("SplashAnimation", bundle: Bundle.main)
animationView.animation = animation
animationView.frame = CGRect(origin: .zero, size: CGSize(width: self.view.frame.size.width, height: 200))
animationView.center = self.view.center
animationView.loopMode = .playOnce
animationView.contentMode = .scaleAspectFit
animationView.play { (finished) in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
if let vc = self.storyboard?.instantiateViewController(identifier: "MainViewController") as? MainViewController {
self.view.window?.rootViewController = vc
self.view.window?.makeKeyAndVisible()
}
})
}
view.addSubview(animationView)
}
}
and in my UICollectionView's DidSelect delegate, I am doing this:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if collectionView == categoryCollectionView {
let vc = self.storyboard?.instantiateViewController(identifier: "SymptomViewController") as! SymptomViewController
self.navigationController?.pushViewController(vc, animated: true)
} else {
print("next collectionView not ready yet.. .")
}
}
which usually runs expected, but now it is printing the print statement but never pushing the next VC.. .
First of all, you need to make sure that if collectionView == categoryCollectionView really equals true.
Second, I noticed that your MainViewController doesn't seem to be embedded in a NavigationController, so when you call self.navigationController?.pushViewController(vc, animated: true), it's possibly going to do nothing as the optional chaining will detect that self.navigationController is nil.
Due to my low reputation I cannot add this as a comment, therefore I am posting it in this way.
Have you tried placing a breakpoint on the first line of the collectionView delegate method and checking the collectionView with "po" command in debugger? Since your if statement fails, most probably it is some other collectionView triggering this delegate method. You have to make sure that if collectionView == categoryCollectionView truly returns true.
Also, once app execution is halted on the breakpoint, it is often very useful to check the debug navigator (in case you haven't yet) and get to know what lead to the specified breakpoint.

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)
}

iOS10: Hide status bar when using a UITabBarController()

I have a UITabBarController() that I use and assign in AppDelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
showTabBar()
return true
}
func showTabBar() {
let tabBarVC = TabBarVC()
if let window = self.window {
window.rootViewController = tabBarVC
}
}
I have the following key is in info.plist:
In my Target under General, I have the following setting:
I use the following code in one of my tabs to hide the Status Bar:
class ViewController: UIViewController {
var statusBarShouldBeHidden = false
override func viewDidLoad() {
super.viewDidLoad()
}
override var prefersStatusBarHidden: Bool {
return statusBarShouldBeHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
#IBAction func buttonHideShowStatusBarTapped(_ sender: UIButton) {
statusBarShouldBeHidden = !statusBarShouldBeHidden
UIView.animate(withDuration: 0.25) {
self.setNeedsStatusBarAppearanceUpdate()
print("animating")
}
}
}
When the button is tapped, "animating" prints in the log; however, the status bar does not hide.
I am not sure if this is related to UITabBarController(), but the code above seems to work fine in a project without it.
How can I hide the status bar in iOS10 when using UITabBarController()?
You have taken TabBarVC as UIViewController subclass rather than UITabBarController subclass and then initialised and added the UITabBarController instance to it's view, I think TabBarVC should be subclass of UITabBarController and should be the rootViewController of the window. If you change the TabBarVC to subclass of UITabbarViewController status bar is working fine. Check the code below
class TabBarVC: UITabBarController, UITabBarControllerDelegate, UINavigationControllerDelegate {
//var mainTabBarController = UITabBarController() //not needed
init() {
super.init(nibName: nil, bundle: nil)
self.delegate = self
self.navigationController?.delegate = self
self.selectedIndex = 0
self.customizableViewControllers = []
self.setViewControllers(self.topLevelControllers(), animated: false)
}
You need to setNeedsStatusBarAppearanceUpdate() in your root view controller, i.e. TabBarVC. Here is the solution:
Override prefersStatusBarHidden in TabBarVC to return value of selectedViewController
override var prefersStatusBarHidden: Bool {
return mainTabBarController.selectedViewController?.prefersStatusBarHidden ?? false
}
Add reference to TabBarVC in ViewController class
var tabBarVC: UIViewController?
Set tabBarVC variable on topLevelControllers() method
let one = self.viewControllerFromStoryBoard(storyboardName: "One",
sceneName: "Initial",
iconName: "",
title: "Tab One") as! ViewController
one.tabBarVC = self
Finally, on your #IBAction update your status bar
self.tabBarVC?.setNeedsStatusBarAppearanceUpdate()

can't change title navigation Controller

I can't change the title of my navigationBar in the options view. I am not using Storyboards. I can't add button too.
This is my App Delegate code :
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let viewController = MenuTableViewController(nibName: nil, bundle: nil) //ViewController = Name of your controller
let navigationController = UINavigationController(rootViewController: viewController)
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()
return true
}
This is my MenuTabeViewController file
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Options", style: .plain, target: self, action: #selector(handleOptions))
}
func handleOptions() {
let optionViewController = optionsViewController()
present(optionViewController, animated: true, completion: nil)
}
this is my options file
class optionsViewController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
view?.backgroundColor = UIColor.white
//I've tried 3 solutions
self.navigationItem.title = "Options"
self.title = "Options"
self.navigationBar.topItem?.title = "Options"
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
dismiss(animated: true, completion: nil)
}
}
I believe the issue is due to the fact that you're not setting up the new navigation / view controller properly when presenting it.
When presenting your new UINavigationController, as you did previously in the AppDelegate, you'll want to create a UIViewController and set it as the rootViewController. So you'll want something like OptionsNavController and OptionsViewController instead of a single nav controller.
Then in your OptionsViewController simple call self.title = #"Options".
EDIT
I'm including an example below.
func handleOptions() {
let optionsViewController = OptionsViewController()
let optionsNavController = UINavigationController(rootViewController: optionsViewController)
present(optionsNavController, animated: true, completion: nil)
}
so then as stated above, call self.title = #"Options" in OptionsViewController.

Swift View Layout below tabbar

class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
self.window?.backgroundColor = UIColor.whiteColor()
self.window?.makeKeyAndVisible()
let vc = ViewController()
vc.tabBarItem = UITabBarItem(...)
...
let tabbar = UITabBarController()
tabbar.setViewControllers([...,vc,...], animated: false)
self.window?.rootViewController = tabbar
tabbar.selectedIndex = 2
return true
}
}
class ViewController: UIViewController {
override func loadView() {
super.loadView()
self.view.backgroundColor = UIColor.yellowColor()
//self.automaticallyAdjustsScrollViewInsets = false;
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
}
}
I am not using a story board.
The above causes the ViewControllers view to extend below the tab bar. How can i stop this?
I've tried setting the views frame to
CGRectMake(0, 0, self.view.frame.width, self.view.frame.height - tabBarController.view.frame.height))
but that did not work.
You can use the edgesForExtendedLayout property of UIViewController to set which edges to extend under navigation bars. If you don't want any, you can simply say:
self.edgesForExtendedLayout = .None
For Swift 5 or above
self.edgesForExtendedLayout = []

Resources