Trying to find a better approach to custom tab bar - ios

I'm trying to create a custom TabBar.
My approach so far, was to create one UIViewController (Let's call it the TabBarController), In the TabBarController, I added a childVC (Let's call it UserViewController).
I couldn't figure out a way to change the UserViewController without making the TabBarController to disappear.
I started to think maybe this is not a good option, and that there most be a better, actually working way to do so.
All I really trying to achieve is a TabBarController, separated from the UserViewController, the TabBarController will always be displayed on the bottom of the screen, and by clicking the items in it, the UserViewController will change accordingly.
I searched for hours, tried different solutions, nothing worked. Really hope you could guide me, maybe share a tutorial or article you have about this.

Create a subclass of UITabBarViewController.
class TabBarController: UITabBarController {
var previousTabIndex: Int?
override func viewDidLoad() {
super.viewDidLoad()
let viewControllerOne = ViewController()
viewControllerOne.tabBarItem.image = UIImage(named: "one")
viewControllerOne.tabBarItem.title = "One"
let viewControllerTwo = ViewController()
viewControllerTwo.tabBarItem.image = UIImage(named: "two")
viewControllerTwo.tabBarItem.title = "Two"
let viewControllerThree = ViewController()
viewControllerThree.tabBarItem.title = "Premium"
viewControllerThree.tabBarItem.image = UIImage(named: "three")
let tabBarList = [
viewControllerOne,
viewControllerTwo,
viewControllerThree
]
self.viewControllers = tabBarList.map {
let nav = UINavigationController(rootViewController: $0)
return nav
}
}
}
And then in SceneDelegate, set window.rootViewController = TabBarController()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = TabBarController()
self.window = window
window.makeKeyAndVisible()
}
}
If you have created your project in older version of xCode, assign TabBarController() to window.rootController in didFinishLaunchingWithOptions of AppDelegate

Related

Programmatically embedding in navigation controller (Xcode 14.2, io2 16+)

I had a working program consisting basically of a table view controller embedded in a navigation controller, and I decided to try to get rid to some mysterious/obnoxious warnings that first appeared in iOS 16 ("UINavigationBar decoded as unlocked for UINavigationController, or navigationBar delegate set up incorrectly"). Following online suggestions, I got rid of the navigation controller on the storyboard, and added some code to
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window?.windowScene = windowScene
window?.makeKeyAndVisible()
// ViewController: UITableViewController is my VC class
let viewController = ViewController()
let navViewController = UINavigationController(rootViewController: viewController)
window?.rootViewController = navViewController
}
to embed it programmatically. The program would then crash with a message that stating that it could not deque a cell with identifier "itemCell", and "must register a nib or a class for the identifier or connect a prototype cell in a storyboard". I solved that by adding the line
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "itemCell")
in ViewDidLoad but I don't understand why this was necessary, since the identifier for the prototype cell is still there in the storyboard, just as it was before. Can someone illuminate me? Thanks.
This:
let viewController = ViewController()
let navViewController = UINavigationController(rootViewController: viewController)
window?.rootViewController = navViewController
won't give you what you want.
In Storyboard, you have to give ViewController a Storyboard ID (such as "FirstViewController"), then instantiate it via code:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let initialViewController = storyboard.instantiateViewController(withIdentifier: "FirstViewController")
let navViewController = UINavigationController(rootViewController: initialViewController)
Edit
Assuming the view controller we ID as "FirstViewController" in Storyboard is a table view controller, and we want to load it as the "root" controller for a navigation controller, the full code block would be:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window?.windowScene = windowScene
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let initialViewController = storyboard.instantiateViewController(withIdentifier: "FirstViewController")
let navViewController = UINavigationController(rootViewController: initialViewController)
window?.rootViewController = navViewController
window?.makeKeyAndVisible()
}

Can't show next view controller while using coordinator pattern

I am trying to use coordinator in my project.
I want to show next viewController on button click.
My code goes to navigationController.pushViewController(registrationViewController, animated: true) but nothing happens
My FirstViewController
class AuthViewController: UIViewController {
private var registrationCoordinator: RegistrationCoordinator?
...
#objc func registrationButtonPressed() {
registrationCoordinator = RegistrationCoordinator(navigationController: UINavigationController())
registrationCoordinator?.start()
}
}
My Coordinator
class RegistrationCoordinator {
private let navigationController: UINavigationController
var authViewController: AuthViewController?
//Init
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
//Functions
public func start() {
showRegistrationViewController()
}
private func showRegistrationViewController() {
let registrationViewController = RegistrationViewController()
navigationController.isNavigationBarHidden = true
registrationViewController.view.backgroundColor = .orange
navigationController.pushViewController(registrationViewController, animated: true)
}
}
My SceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var authCoordinator: AuthCoordinator?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let rootWindow = UIWindow(windowScene: windowScene)
let navigationController = UINavigationController()
authCoordinator = AuthCoordinator(navigationController: navigationController)
window = rootWindow
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
authCoordinator?.start()
}
#objc func registrationButtonPressed() {
registrationCoordinator = RegistrationCoordinator(navigationController:
UINavigationController())
registrationCoordinator?.start() }
When you call your coordinator you are instantiating the navigation controller.
Then you are using your navigation controller to push a viewcontroller, but yout navigation controller isn't in the view herarchy, not in the main window, not inside other view.
In other words, your navigation controller exists, but is not part of the interface. Therefore nothing it does would be shown.
You are not passing the same Navigation Controller you use in the SceneDelegate, you are creating a new one.
You can pass to the coordinator the navigation controller of your current viewcontroller.
registrationCoordinator =
RegistrationCoordinator(navigationController:
self.navigationController?)
That, of course, assuming that your current viewcontroller has a navigation controller (and your coordinator would have to accept optionals)

SWIFT - Problems with navigationController using .xib files - when a i present a new viewController, navigation Controller does not follow up

when using storyboard, i just need to "embed Navigation Controller" to have all my viewControllers with navigationController
i've started to use .xib files instead of storyboard
well i've set ONE viewController on sceneDelegate as root already
is a tableView, and when one of the cells are clicked i present a new viewController
but the navigationViewController does not follow up this new viewController
i want to have a navigationController on this new viewController too
I DONT WANT TO USE A TABBAR
here the code that i set my root viewController
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
/// 1. Capture the scene
guard let windowScene = (scene as? UIWindowScene) else { return }
/// 2. Create a new UIWindow using the windowScene constructor which takes in a window scene.
let window = UIWindow(windowScene: windowScene)
/// 3. Create a view hierarchy programmatically
let categoryVC = CategoryViewController()
let todoVC = TodoViewController()
let categoryNC = UINavigationController(rootViewController: categoryVC)
let todoNC = UINavigationController(rootViewController: todoVC)
/// 4. Set the root view controller of the window with your view controller
window.rootViewController = categoryNC
/// 5. Set the window and call makeKeyAndVisible()
self.window = window
window.makeKeyAndVisible()
here is how im presenting a new viewController clicking on cell
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//deselect cell
tableView.deselectRow(at: indexPath, animated: true)
if let indexPath = tableView.indexPathForSelectedRow {
todoViewController.selectCategory = categoryBrain.categoryArray[indexPath.row]
}
todoViewController.modalPresentationStyle = .fullScreen
present(todoViewController, animated: true)
sorry for my english, im working to improve.
thank you in advance s2.

Loading screen off centre based on launch storyboard?

I have 2 storyboards: Onboarding and Main. Everything works great as far as execution goes. The issue is that I have loading screen at the start of Main.storyboard that looks different depending on which storyboard launches.
Scenario 1: New user launches the app, it goes through Onboarding.storyboard then Main.storyboard launches and the loading screen looks as it should like this: Correct Screen
Scenario 2: User launches the app for the second time, therefore Onboarding.storyboard is not launched: Loading screen is off centre like this: Wrong Screen
Long story short: If there is no Onboarding.storyboard, it doesn't look right. The issue is only shown on iPad landscape; portrait looks fine.
All of my loading screen code is inside my first view controller of Main.Storyboard so I'm guessing that the issue is the order in which the code is called?
Onboarding.Storyboard only has 1 view controller
Main.Storyboard goes: TabBarController -> NavController -> FirstVC
I placed my SceneDelegate code below along with the loading screen code of FirstVC.
In case you want to dig in deeper, I'm using RevealingSplashView available on github. Since I don't think that's the issue I didn't get into that further.
SceneDelegate:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
let onboardingStoryboard = UIStoryboard(name: "Onboarding", bundle: nil)
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
var vc: UIViewController
if UserDefaults.standard.value(forKey: "firstTimer") == nil {
vc = onboardingStoryboard.instantiateInitialViewController()!
} else {
vc = mainStoryboard.instantiateInitialViewController()!
}
self.window?.rootViewController = vc
self.window?.makeKeyAndVisible()
}
FirstVC relevant code:
class firstVC: UIViewController {
var revealingSplashView : RevealingSplashView!
override func viewDidLoad() {
super.viewDidLoad()
showLoadingScreen()
}
func showLoadingScreen() {
revealingSplashView = RevealingSplashView(iconImage: UIImage(named: "Icon")!, iconInitialSize: CGSize(width: 150, height: 150), backgroundImage: UIImage(named: "loadBackground")!)
revealingSplashView.animationType = .heartBeat
revealingSplashView.startAnimation()
view.addSubview(revealingSplashView)
}
Check to see that your layout constraints are the same in both of your storyboards. I'd recommend just setting them to center horizontal and center vertical on both of them.
In your scene delegate callback, why are you calling instantiateInitialViewController() twice per storyboard?
In my iOS 12 project, I used storyboards to kick things off. In app delegate didFinishLaunching, I would override and set the window if and only if it was a new user. So my "Main" storyboard would kick in most of the time.
Same technique could be applied here. Check out the Info.plist values you need to set for UIScene storyboard to work. You can override the window if you want to show the first time onboarding, otherwise let the system instantiate your main storyboard.
Further testing revealed that if I return SceneDelegate to its "factory default" state, therefore launching Main.storyboard, I had no issues.
To fix the issue I simply changed my SceneDelegate to:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
if UserDefaults.standard.value(forKey: "firstTimer") == nil {
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
let onboardingStoryboard = UIStoryboard(name: "Onboarding", bundle: nil)
var vc: UIViewController
vc = onboardingStoryboard.instantiateInitialViewController()!
self.window?.rootViewController = vc
self.window?.makeKeyAndVisible()
}
}

Second start of application only display a black view

On firstStart of the application I want the user to start at ViewController1 (VC1) and after completing the onboarding process and a button gets clicked in ViewController3 (VC3) should the first item of the Tab Bar controller be the new initial view controller.
The first launch works but when closing the app and upon reopening the app everything goes black.
When I set VC3 as the new initial viewController everything works fine.
My SceneDelegate
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
self.window = UIWindow(windowScene: windowScene)
//self.window = UIWindow(frame: UIScreen.main.bounds)
// Is it the apps first start?
let firstView = UserDefaults.standard.string(forKey: "firstStart")
let storyboard = UIStoryboard(name: "Main", bundle: nil)
// Create the root view controller as needed
guard let vc = storyboard.instantiateViewController(identifier: firstView ?? "Welcome") as? ViewController else {
print("ViewController not found")
return
}
let nc = UINavigationController(rootViewController: vc)
// Create the window.
let win = UIWindow(windowScene: windowScene)
win.rootViewController = nc
win.makeKeyAndVisible()
window = win
}
My button:
#IBAction func buttonActivate(_ sender: Any) {
// Change start View to "Home"
UserDefaults.standard.set("Home", forKey: "firstStart")
}
I've tried with changing keys in Info.plist and read through UIApplicationSceneManifest but without any success. Tried having an additional navigation controller as tab bar controller item 2 shows but same result with a black screen.
You shouldn't try and change the window, instead change the .rootViewController.
VC1 is the entry point in storyboard which means that the initial view controller your app starts with every time, during the "didFinishLaunchingWithOptions" in app-delegate you can ask whether the user has been promoted with the onboarding and in that case switch the rootViewController as follows:
func showMainStoryboard() {
let homeViewController = UIStoryboard(name: "StoryboardName", bundle: Bundle.main).instantiateInitialViewController()
window?.rootViewController = homeViewController
}
In order for the following example to work for you, you must create a separate storyboard: 1 for the onboarding and 1 for main.
Flow #1:
The first time user installs the app he is promoted with the onboarding VC after he clicked the button on VC3 you switch the rootViewController on that event.
Flow #2:
After the user clicked the button and reentered the app he won't be promoted with the onboarding but routed to the "main" VC, a.k.a Start in your case.

Resources