how to navigate from swiftui to viewcontroller using existing navigation controller - ios

i have a navigation controller which navigates to a view controller, which navigates to uihostingcontroller. How would i push to another view controller from swift ui
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
self.window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
let navigationView = UINavigationController(rootViewController: PreviewViewVideoController())
navigationView.isToolbarHidden = true
self.window?.rootViewController = navigationView
self.window?.makeKeyAndVisible()
}
in preview video controller
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch cards[indexPath.row] {
case .trans_human:
let controller = UIHostingControllerCustom(rootView: FilmOverviewView(overview: TransHumanOverview()))
self.navigationController?.pushViewController(controller, animated: true)
controller.navigationItem.title = cards[indexPath.row].rawValue
}
}
in filmOverviewView
struct FilmOverviewView: View {
var filmOverview: FilmOverview!
var imageResource: String!
init(overview: FilmOverview) {
filmOverview = overview
}
var body: some View {
ScrollView(Axis.Set.vertical, showsIndicators: false) {
VStack {
Image(filmOverview.imageResource)
.resizable()
.frame(width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height / 2)
.overlay(StartButtonOverlay(), alignment: .bottom)
Button(action: {})
}
}
How would i navigate from a swiftui button view action to a new view controller using existing navigation stack

I would inject UINavigationController into SwiftUI stack via environment values
struct NavigationControllerKey: EnvironmentKey {
static let defaultValue: UINavigationController? = nil
}
extension EnvironmentValues {
var navigationController: NavigationControllerKey.Value {
get {
return self[NavigationControllerKey.self]
}
set {
self[NavigationControllerKey.self] = newValue
}
}
}
then in table view delegate inject it
let controller = UIHostingControllerCustom(rootView:
FilmOverviewView(overview: TransHumanOverview()
.environment(\.navigationController, self.navigationController))
and now in FilmOverviewView we can use it
struct FilmOverviewView: View {
#Environment(\.navigationController) var navigationController
// ... other code
var body: some View {
ScrollView(Axis.Set.vertical, showsIndicators: false) {
VStack {
// ... other code
Button("Demo") {
let controller = UIHostingControllerCustom(rootView:
SomeOtherView() // inject if needed again
.environment(\.navigationController, self.navigationController)))
self.navigationController?.pushViewController(controller, animated: true)
}
}
}
Update: updated custom hosting controller
class UIHostingControllerCustom<V: View>: UIHostingController<V> {
override func viewWillAppear(_ animated: Bool) {
navigationController?.setNavigationBarHidden(true, animated: false)
}
}

Related

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)

when need to switch the rootViewController

I've been working on a Swift project and I have two view controllers, the login view controller & the home view controller. When a user launches the app, I want to display the login view controller if the user is not logged in, on the other hand, if the user is logged in, I want to display the home view controller.
So the flow is gonna be something like this.
When the user is not logged in, display
LoginViewController
HomeViewController
When the user is already logged in, display
HomeViewController
In the scene delegate, I've written
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: scene.coordinateSpace.bounds)
window?.windowScene = scene
window?.rootViewController = HomeViewController() or LoginViewController() depending on the user's login status
window?.makeKeyAndVisible()
}
I was wondering if I should apply the HomeViewController as a rootviewcontroller regardless of the user's login status (and maybe present loginVC on the homeVC when the user is not logged in), or I should switch the view controller depending on the user's login status.
So, in this case, what is the point of switching rootviewcontroller? and why it is (or isn't important) to switch the root view controller?
Is there anything I should consider when I apply view controller to the root viewcontroller property?
// SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let _ = (scene as? UIWindowScene) else { return }
// if user is logged in before
if let loggedUsername = UserDefaults.standard.string(forKey: "username") {
window?.rootViewController = HomeViewController()
} else {
// if user isn't logged in
window?.rootViewController = LoginViewController()
}
}
I think there can be another cases, like RootVC is a container ViewContoller consists of HomeVC and LoginVC.
example)
final class RootVC: UIViewController {
private let loginVC = LoginVC()
private let homeVC = HomeVC()
override func viewDidLoad() {
super.viewDidLoad()
addChild(homeVC)
view.addSubview(homeVC.view)
addChild(loginVC)
view.addSubview(loginVC.view)
}
func showVC() {
if isLogin {
homeVC.hide()
loginVC.show()
} else {
reverse()
}
}
}
Hi all i have one idea for set a RootViewController in SceneDelegate. First we need to create the method setViewController and variable currentScene in SceneDelegate class kindly feel free to refer the code below.
Two different viewcontroller as per your example HomeViewController, LoginViewController
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var currentScene: UIScene?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
currentScene = scene
if UserDefaults.standard.bool(forKey: "isLoggedIn") == true{
self.setRootViewController(LoginViewController())
}else{
self.setRootViewController(HomeViewController())
}
}
func setRootViewController(_ viewController: UIViewController){
guard let scene = (currentScene as? UIWindowScene) else { return }
window = UIWindow(frame: scene.coordinateSpace.bounds)
window?.windowScene = scene
window?.rootViewController = viewController
window?.makeKeyAndVisible()
}
}
class ButtonViewController: UIViewController {
lazy var button: UIButton! = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .darkGray
button.setTitleColor(.white, for: .normal)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
button.tag = 1
button.setTitle("Tap", for: .normal)
return button
}()
override func loadView() {
super.loadView()
self.view.backgroundColor = .white
setConstraint()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
func setConstraint(){
self.view.addSubview(self.button)
NSLayoutConstraint.activate([
self.button.heightAnchor.constraint(equalToConstant: 60),
self.button.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width * 0.66),
self.button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
self.button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
])
}
#objc func buttonAction(){ }
}
class HomeViewController: ButtonViewController{
override func loadView() {
super.loadView()
self.view.backgroundColor = .red
self.button.setTitle(String(describing: HomeViewController.self), for: .normal)
}
override func buttonAction() {
let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as! SceneDelegate
sceneDelegate.setRootViewController(LoginViewController())
}
}
class LoginViewController: ButtonViewController{
override func loadView() {
super.loadView()
self.view.backgroundColor = .green
self.button.setTitle(String(describing: LoginViewController.self), for: .normal)
}
override func buttonAction() {
let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as! SceneDelegate
sceneDelegate.setRootViewController(HomeViewController())
}
}
If click the button view controller can be change as rootViewController.
Output:

Hide UINavigationController's navigationBar when the root controller is a UIHostingController

I am struggling to hide the navigationBar, which would properly be hidden if the root controller wasn't a SwiftUI UIHostingController.
I tried the following:
Setting navigationController.isNavigationBarHidden = true after creating it, at viewDidLoad and viewWillAppear.
Adding both .navigationBarHidden(true) and .navigationBarBackButtonHidden(true) for the UIHostingController's rootView.
Could it be an Apple bug? I am using Xcode 11.6.
All my attempts together:
class LoginController: UINavigationController, ObservableObject
{
static var newAccount: LoginController
{
let controller = LoginController()
let view = LoginViewStep1()
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
controller.viewControllers = [UIHostingController(rootView: view)]
controller.isNavigationBarHidden = true
return controller
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.isNavigationBarHidden = true
}
override func viewDidLoad()
{
super.viewDidLoad()
self.isNavigationBarHidden = true
}
}
struct LoginViewStep1: View
{
// ...
var body: some View
{
VStack {
// ...
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
Here is a solution. Tested with Xcode 11.4 / iOS 13.4
Modified your code:
class LoginController: UINavigationController, ObservableObject
{
static var newAccount: LoginController
{
let controller = LoginController()
let view = LoginViewStep1()
controller.viewControllers = [UIHostingController(rootView: view)]
// make it delayed, so view hierarchy become constructed !!!
DispatchQueue.main.async {
controller.isNavigationBarHidden = true
}
return controller
}
}
struct LoginViewStep1: View
{
var body: some View
{
VStack {
Text("Hello World!")
}
}
}
tested part in SceneDelegate
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = LoginController.newAccount
self.window = window
window.makeKeyAndVisible()
}
Alternate solution is to use UINavigationControllerDelegate:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
// Required because pushing UIHostingController makes the navigationBar appear again
navigationController.isNavigationBarHidden = true
}
Tested with iOS 14.5 - Xcode 12.5

How to scroll from top after scrolling down like Facebook - Swift 4.2

I want to implement scrolling functionality like Facebook app, where tap on Tab bar item if scrolling a page down and tap on same tab bar item it starts scrolling from top.
Here is my code -
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
let indexOfTab = tabBar.items?.index(of: item)
if indexOfTab == 0{
let feed = HomeFeedViewController()
feed.scrollToTop()
print("pressed tabBar: \(String(describing: indexOfTab))")
}else if indexOfTab == 1{
print("pressed tabBar: \(String(describing: indexOfTab))")
}else if indexOfTab == 2{
print("pressed tabBar: \(String(describing: indexOfTab))")
}else if indexOfTab == 3{
let feed = QueueViewController()
print("pressed tabBar: \(String(describing: indexOfTab))")
}else if indexOfTab == 4{
print("pressed tabBar: \(String(describing: indexOfTab))")
}
}
func scrollToTop(){
feedsTableView.setContentOffset(.zero, animated: true)
}
For Swift
extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: self.view)
}
}
To use
var previousController: UIViewController?
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
//when user is in FeedViewController and scroll at last record and want to
if previousController == viewController {
if let navVC = viewController as? UINavigationController, let vc = navVC.viewControllers.first as? HomeViewController {
if vc.isViewLoaded && (vc.view.window != nil) {
vc.scrollToTop()
}
print("same")
}
}
else{
print("No same")
}
previousController = viewController
return true;
}
Please try my code
var canScrollToTop:Bool = true
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
// Allows scrolling to top on second tab bar click
if (viewController.isKindOfClass(CustomNavigationBarClass) && tabBarController.selectedIndex == 0) {
if (viewControllerRef!.canScrollToTop) {
viewControllerRef!.scrollToTop()
}
}
}
// Scrolls to top
func scrollToTop() {
self.View.setContentOffset(CGPoint.zero, animated: true)
self.tblView.setContentOffset(CGPoint.zero, animated: true)
self.collView.setContentOffset(CGPoint.zero, animated: true)
}
// Called when the view becomes available
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
canScrollToTop = true
}
// Called when the view becomes unavailable
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
canScrollToTop = false
}

Tap UITabBarItem To Scroll To Top Like Instagram and Twitter

I'm having issues making this feature work and i would like to get some help.
My hierarchy is TabBarController -> Navigation Controller -> TableViewController
What i want is if you are on current tab and you scrolled down you will be able to tap the current View's UITabBarItem and you will be scrolled back to the top,Like Instagram and Twitter does for example.
I have tried many things right here :
Older Question
but sadly non of the answers did the job for me.
I would really appreciate any help about this manner ,
Thank you in advance!
Here is my TableView`controller's Code :
import UIKit
class BarsViewController: UITableViewController,UISearchResultsUpdating,UISearchBarDelegate,UISearchDisplayDelegate,UITabBarControllerDelegate{
//TableView Data & non related stuff....
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.searchController.searchBar.resignFirstResponder()
self.searchController.searchBar.endEditing(true)
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
if tabBarIndex == 0 {
let indexPath = IndexPath(row: 0, section: 0)
let navigVC = viewController as? UINavigationController
let finalVC = navigVC?.viewControllers[0] as? BarsViewController
finalVC?.tableView.scrollToRow(at: indexPath as IndexPath, at: .top, animated: true)
}
}
}
TabBarController.Swift Code ( Code doesn't work ) :
import UIKit
class TabBarController: UITabBarController,UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let viewControllers = viewControllers else { return false }
if viewController == viewControllers[selectedIndex] {
if let nav = viewController as? UINavigationController {
guard let topController = nav.viewControllers.last else { return true }
if !topController.isScrolledToTop {
topController.scrollToTop()
return false
} else {
nav.popViewController(animated: true)
}
return true
}
}
return true
}
}
extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: view)
}
var isScrolledToTop: Bool {
for subView in view.subviews {
if let scrollView = subView as? UIScrollView {
return (scrollView.contentOffset.y == 0)
}
}
return true
}
}
Here you go, this should work:
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let viewControllers = viewControllers else { return false }
if viewController == viewControllers[selectedIndex] {
if let nav = viewController as? ZBNavigationController {
guard let topController = nav.viewControllers.last else { return true }
if !topController.isScrolledToTop {
topController.scrollToTop()
return false
} else {
nav.popViewController(animated: true)
}
return true
}
}
return true
}
and then...
extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: view)
}
// Changed this
var isScrolledToTop: Bool {
if self is UITableViewController {
return (self as! UITableViewController).tableView.contentOffset.y == 0
}
for subView in view.subviews {
if let scrollView = subView as? UIScrollView {
return (scrollView.contentOffset.y == 0)
}
}
return true
}
}
There's a bit extra in this function so that if the UIViewController is already at the top it will pop to the previous controller
Try this code in your TabViewController:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
if tabBarIndex == 0 {
let indexPath = NSIndexPath(row: 0, section: 0)
let navigVC = viewController as? UINavigationController
let finalVC = navigVC?.viewControllers[0] as? YourVC
finalVC?.tableView.scrollToRow(at: indexPath as IndexPath, at: .top, animated: true)
}
}
Also, your TabViewController should inherit from UITabBarControllerDelegate
final code:
import UIKit
class tabViewController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
if tabBarIndex == 0 {
let indexPath = NSIndexPath(row: 0, section: 0)
let navigVC = viewController as? UINavigationController
let finalVC = navigVC?.viewControllers[0] as? YourVC
finalVC?.tableView.scrollToRow(at: indexPath as IndexPath, at: .top, animated: true)
}
}
}
Remember to change tabBarIndex and set self.delegate = self in viewDidLoad
You have just to create a file TabBarViewController with this code :
class TabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
guard let viewControllers = viewControllers else { return false }
if viewController == viewControllers[selectedIndex] {
if let nav = viewController as? UINavigationController {
guard let topController = nav.viewControllers.last else { return true }
if !topController.isScrolledToTop {
topController.scrollToTop()
return false
} else {
nav.popViewController(animated: true)
}
return true
}
}
return true
}
}
extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: view)
}
var isScrolledToTop: Bool {
if self is UITableViewController {
return (self as! UITableViewController).tableView.contentOffset.y == 0
}
for subView in view.subviews {
if let scrollView = subView as? UIScrollView {
return (scrollView.contentOffset.y == 0)
}
}
return true
}
}
And then in your storyboard, set custom class TabBarController like this :
Great examples! I've tried some things also and I guess this small piece of code for our delegate is all we need:
extension AppDelegate: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
// Scroll to top when corresponding view controller was already selected and no other viewcontrollers are pushed
guard tabBarController.viewControllers?[tabBarController.selectedIndex] == viewController,
let navigationController = viewController as? UINavigationController, navigationController.viewControllers.count == 1,
let topViewController = navigationController.viewControllers.first,
let scrollView = topViewController.view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView else {
return true
}
scrollView.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: true)
return false
}
}
I just had to implement this and was surprised to find out it wasn't as easy as I thought it would be. This is how I implemented it:
A few notes on my application's setup
The MainTabBarcontroller is our root view controller and we generally access it from the UISharedApplication.
Our navigation structure is MainTabBarController -> Nav Controller -> Visible View Controller
The main issue I found with implementing this is I only wanted to scroll to the top if I was already on the first screen in the tab bar but once you try to make this check from tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) you've already lost a reference to the view controller that was visible before you tapped it. It is also a problem to compare view controllers when you're switching tabs since the visible view controller might be in another tab.
So I created a property in my TabBar class to hold a reference to the previousTopVC and created a protocol to help me set this property from the currently visible VC.
protocol TopScreenFindable {
func setVisibleViewController()
}
extension TopScreenFindable where Self: UIViewController {
func setVisibleViewController() {
guard let tabController = UIApplication.shared.keyWindow?.rootViewController as? MainTabBarController else { return }
tabController.previousTopVC = self
}
}
I then called conformed to this protocol and setVisibleViewController from the View Controllers' viewDidAppear and now had a reference to the previous visible VC when any screen showed up.
I created a delegate for my MainTabBarController class
protocol MainTabBarDelegate: class {
func firstScreenShouldScrollToTop()
}
Then called it from the UITabBarControllerDelegate method
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
guard let navController = viewController as? UINavigationController, let firstVC = navController.viewControllers.first, let previousVC = previousTopController else { return }
if firstVC == previousVC {
mainTabBarDelegate?.firstScreenShouldScrollToTop()
}
}
And in the First View Controllers conformed to the MainTabBarDelegate
extension FirstViewController: MainTabBarDelegate {
func firstScreenShouldScrollToTop() {
collectionView.setContentOffset(CGPoint(x: 0, y: collectionView.contentInset.top), animated: true)
}
I set the tabBar.mainTabBarDelegate = self on the FirstViewController's viewDidAppear so that the delegate was set every time the screen showed up. If I did it on viewDidLoad it wouldn't work when switching tabs.
A couple things I didn't like about my approach.
Having a reference to the tab bar in my view controllers just so it can set itself as its delegate.
Making every relevant screen in the app conform to the TopScreenFindable protocol

Resources