iOS: Show login viewcontroller or "another" viewcontroller inside of UITabbarController? - ios

I have a tabbar with 5 tabs.
3 of these tabs requires authentication by having an account.
I know one solution is to present a modal viewcontroller when pressing one of these tabs.
I wish to present the login viewcontroller inside of the tabs instead of showing it modally. How can this be done and how can I "reload" the tabbar with the other viewcontrollers once a user has logged in?

I would do this by creating a subclass of UINavigationController which receives a UIViewController to show if user is logged in, and shows the login page in the other case.
class CustomNavController:UINavigationViewController {
let loggedInViewController:UIViewController
init(loggedInVC:UIViewController) {
loggedInViewController = loggedInVC
if (userLoggedIn) {
onLogin()
} else {
onLogout()
}
//setup listeners for authentication
super.init()
}
onLogout () {
self.viewControllers = [AuthenticationVC()]
}
onLogin () {
self.viewControllers = [loggedInViewController]
}
}
//code for setting up your UITabBarViewController
class MyTabbar:UITabBarViewController {
init() {
viewControllers = [
FirstVC(),
SecondVC(),
CustomNavController(ThirdVC()),
CustomNavController(ForthVC()),
CustomNavController(FifthVC())
]
}
}

Related

present a ViewController from a Swift class derived from an NSObject?

This project was written in Objective C and a bridging header and Swift files were added so they can be used at run time. When the app starts, Initializer called in Mock API client is printed in the debugger. Is it possible to present a ViewController from the initializer?
Xcode Error:
Value of type 'MockApiClient' has no member 'present'
//MockApiclient.Swift
import Foundation
class MockApiClient: NSObject
{
override init ()
{
print("Initializer called in Mock API client")
if isLevelOneCompleted == false
{
print("It's false")
let yourVC = ViewController()
self.present(yourVC, animated: true, completion: nil)
} else
{
print("It's true")
}
}
var isLevelOneCompleted = false
#objc func executeRequest()
{
print("The execute request has been called")
isLevelOneCompleted = true
if isLevelOneCompleted {
print("It's true")
} else {
//do this
}
}
}
Update - ViewController.m
// prints "The execute request has been called" from the debugger window
- (void)viewDidLoad {
[super viewDidLoad];
MockApiClient *client = [MockApiClient new];
[client executeRequest];
}
You can't call present(_:animated:completion) because it is a method of UIViewController, not NSObject.
Why not pass a viewController reference to the MockApiClient to present on instead like so. Be sure to check Leaks or Allocations on instruments to avoid the client retaining the controller.
class MockApiClient: NSObject {
var referencedViewController: UIViewController?
override init() {
let presentableViewController = ViewController()
referencedViewController.present(presentableViewController, animated: true, completion: nil)
}
deinit {
referencedViewController = nil
}
}
let apiClient = MockApiClient()
apiClient.referencedViewController = // The view controller you want to present on
Assuming you're using UIKit, you'll have to present the view controller from the nearest available attached view controller. If you know for certain that no other view controllers would currently be presented then you can safely present from the root view controller:
UIApplication.shared.keyWindow?.rootViewController?.present(someViewController, animated: true, completion: nil)
This concept of attached and unattached/detached view controllers is never officially explained but the infamous UIKit warning of presenting view controllers on detached view controllers is real. And the workaround is finding the nearest available attached view controller, which at first (when nothing is currently being presented) is the root view controller (of the window). To then present an additional view controller (while one is currently being presented), you'd have to present from that presented view controller or its nearest parent view controller if it has children (i.e. if you presented a navigation view controller).
If you subclass UIViewController, you can add this functionality into it to make life easier:
class CustomViewController: UIViewController {
var nearestAvailablePresenter: UIViewController {
if appDelegate.rootViewController.presentedViewController == nil {
return appDelegate.rootViewController
} else if let parent = parent {
return parent
} else {
return self
}
}
}
Then when you wish to present, you can simply do it through this computed property:
nearestAvailablePresenter.present(someViewController, animated: true, completion: nil)

How to check in AppDelegate if a particular ViewController is currently open

I am trying to prevent a push notification show on the app home screen when a certain userMessagesViewController is currently open.
I don't want users receiving a push notification if this specific viewController is open. My function that sends the push notification is in the appDelegate. How can I check. Here is my implementation so far.
let messagesVC = UserMessageViewController()
if messagesVC.view.window != nil {
print("Messages viewcontroller is visible and open")
} else {
print("Messages viewcontroller isnt visible and not open")
}
By initiating messagesVC, you're creating a brand new UserMessageViewController that won't have been presented yet. The particular instance of the controller you want will already be instantiated, so you must find it using the view controller hierarchy.
The AppDelegate gives you access to the rootViewController of your application which will be the very first controller you have in your storyboard. From this controller, you can make your way through the child view controllers in search of a UserMessageViewController.
Here is an extension that will start at the rootViewController and bubble its way up until it reaches the top of the view controller hierarchy stack.
extension UIApplication {
func topViewController(_ base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
switch (base) {
case let controller as UINavigationController:
return topViewController(controller.visibleViewController)
case let controller as UITabBarController:
return controller.selectedViewController.flatMap { topViewController($0) } ?? base
default:
return base?.presentedViewController.flatMap { topViewController($0) } ?? base
}
}
}
Create a new file called UIApplication+TopViewController.swift and paste in the above extension. Then inside AppDelegate, you will be able to get the current view controller that is being presented using UIApplication.shared.topViewController():
if let messagesVC = UIApplication.shared.topViewController() as? UserMessageViewController {
print("Messages viewcontroller is visible and open")
} else {
print("Messages viewcontroller isnt visible and not open")
}
By casting the top view controller to UserMessageViewController, we can determine whether or not the notification should be presented.
This should work for you:
if messagesVC.viewIfLoaded?.window != nil {
// viewController is visible, handle notification silently.
}
Your appDelegate will have a reference to the VC. It should probably be a property of the delegate.

Swift Deeplink to already open App's already open viewcontroller?

I have an app that accepts a Deeplink URL and opens a viewcontroller with variables from the link and it works well if the App is opened/run for the first time by the user using the Deeplink.
However, if the App is already open/or in the background and has that view controller open... it then opens the same viewcontroller back up again so then I have two. I do not want to open the viewcontroller an additional time.
Is there some way I can identify that viewcontroller that is already open and pass the variables from the Deeplink to it?
or do I need to close it in some way and re-open it?
I am open to suggestions.... thanks in advance.
Try using UIApplication.shared.keyWindow?.rootViewController and testing what class it is. For example:
if let vc = UIApplication.shared.keyWindow?.rootViewController {
if vc is SomeViewController {
// Do something.
}
}
You can find the visible view controller with the following method
func getVisibleViewController(_ rootViewController: UIViewController?) -> UIViewController? {
var rootVC = rootViewController
if rootVC == nil {
rootVC = UIApplication.shared.keyWindow?.rootViewController
}
if rootVC?.presentedViewController == nil {
return rootVC
}
if let presented = rootVC?.presentedViewController {
if presented.isKind(of: UINavigationController.self) {
let navigationController = presented as! UINavigationController
return navigationController.viewControllers.last!
}
if presented.isKind(of: UITabBarController.self) {
let tabBarController = presented as! UITabBarController
return tabBarController.selectedViewController!
}
return getVisibleViewController(presented)
}
return nil
}
you can then switch on the presented view
if let presentedView = getVisibleViewController(window?.rootViewController) {
switch presentedView {
//code
default:
//code
}
}
and of course in the switch present a view controller if it is not the one that you want to be open.
No need to close a viewcontroller before opening it!

In TabBarViewController, how can I access my children directly?

I want to access a child called SubscriptionsViewController (3rd tab)
This is what I'm doing, but it doesn't work.
var subscriptionsViewController: SubscriptionsViewController? {
get {
let viewControllers = self.childViewControllers
for viewController in viewControllers {
if let vc = viewController as? SubscriptionsViewController {
return vc
}
}
return nil
}
}
Assuming you have an instance of tab bar controller, you can do it as follows:
var subscriptionsViewController: SubscriptionsViewController? {
get {
let viewControllers = tabController.viewControllers //assuming you have a property tabBarController
for viewController in viewControllers {
if viewController is SubscriptionsViewController {
return vc
}
}
return nil
}
}
You can access a child of your tab bar controller with the following :
self.tabBarController.viewControllers[2]

How to refresh UITabBar after changing viewControllers

I'm implementing a user authentication system for an app. If the user is logged in they get one set of available tabs to select. If They're not logged in they get another. Now the issue I'm running into is that after a user logs in (app redirects to safari to some oauth stuff and then returns to the app), I update the tabs from the UITabBarController like so:
private var accessibleViewControllers = [UIViewController]()
func setUpView() {
var viewControllersToSet = [UIViewController]()
if let user = theUser {
for controller in accessibleViewControllers {
if !(controller is LogInViewController) {
viewControllersToSet.append(controller)
}
}
} else {
for controller in accessibleViewControllers {
if controller is LogInViewController || controller is HomeNavigator {
viewControllersToSet.append(controller)
}
}
}
setViewControllers(viewControllersToSet, animated: false)
}
Now the funny thing is, the tab icons don't refresh, but I can still click on the spaces where the new icons would be to link through to the associated view. How do I refresh the tabs so that the right icons appear?
This was a threading issue. I was loading the user data over a background thread and then calling the delegation method setUpView from the same background thread. To fix this I ran it back on the main queue:
dispatch_async(dispatch_get_main_queue()) {
if self.accessibleViewControllers != nil {
var viewControllersToSet = [UIViewController]()
if let user = MediatedUser.shared {
for controller in self.accessibleViewControllers! {
if !(controller is LogInViewController) {
viewControllersToSet.append(controller)
}
}
} else {
for controller in self.accessibleViewControllers! {
if controller is LogInViewController || controller is HomeNavigator {
viewControllersToSet.append(controller)
}
}
}
self.viewControllers = viewControllersToSet
}
}

Resources