I've been following this tutorial to create a custom tab bar controller for an iPad app as I would like to implement a vertical tab bar. However, I would like one of the tabs to present a UISplitViewController, whilst the others just present UIViewControllers. My questions are:
1) Will this be accepted by the app store? Apple's documentation currently states adding UISplitViews as child views is not recommended but may be implemented with certain containers. Anyone had any experience with this?
2) Here is an extract from my custom tab bar controller. If secondViewController is presenting the UISplitView, can I leave it as is? I mean, it seems to work find when I run it, but is it acceptable?
class CustomTabBarController: UIViewController {
#IBOutlet weak var tabView: UIView!
#IBOutlet var tabButtons: [UIButton]!
var firstViewController: UIViewController!
var secondViewController: UISplitViewController!
var thirdViewController: UIViewController!
var viewControllerArray: [UIViewController]!
var selectedTabIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
firstViewController = storyboard.instantiateViewController(withIdentifier: "firstVC")
secondViewController = storyboard.instantiateViewController(withIdentifier: "secondVC") as! UISplitViewController
thirdViewController = storyboard.instantiateViewController(withIdentifier: "thirdVC")
viewControllerArray = [firstViewController, secondViewController, thirdViewController]
tabButtons[selectedTabIndex].isSelected = true
didPressTab(tabButtons[selectedTabIndex])
}
3) I can't really get my head around what (if anything) needs to go in AppDelegate? Again seems to run fine but just wondering if its safe.
Thanks.
1) I believe Apple is simply recommending against this as potentially bad design, since they refer you to the Human Interface Guidelines. You don't always have to agree with their recommendations and very rarely will your app get rejected for design choices- the only instances off the top of my head would be mimicking the App Store or other core OS functionality.
2) If, as you say, this is working, I don't see any glaring issue.
3) Again if it's working, you may not need to do anything. But here's how Apple sets up their template for a Master-Detail app:
If your splitViewController is set up like this and you want the same functionality as this template, here's how you should be able to get it.
First add this to the very bottom of AppDelegate.swift:
extension AppDelegate: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
if topAsDetailController.detailItem == nil {
// Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
return true
}
return false
}
}
Then, add this to the end of viewDidLoad in CustomTabBarController:
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {return}
let navigationController = secondViewController.viewControllers[secondViewController.viewControllers.count-1] as! UINavigationController
navigationController.topViewController!.navigationItem.leftBarButtonItem = secondViewController.displayModeButtonItem
secondViewController.delegate = appDelegate
Related
I have an app that has 4 tab bar items (A, B, C, D). I'm using notification data to direct a user to a child vc of A ('A-child') and I want to populate the search bar in A-child with a value from the notification.
I'm successfully getting the notification and value, that's fine. I'm also able to navigate to A just fine using:
let tabBarController = UIApplication.shared.keyWindow?.rootViewController as! UITabBarController
tabBarController.selectedIndex = 0
But I then need to navigate to A-Child VC from A and then set the search bar text (I'm less worried about the search bar text piece as I think I can work that out once I get the right VC stack in place). I could of course use a segue to go straight from where the user when they tap on the notification to A-child, but then I lose the expected navigation behaviour for the user from A-child.
I know I'm not the first to ask a question like this, and I've gone through everything I can find on SO relating to this - but can't make any of the answers click. Help is much appreciated!
Edit:
I've got it partially working with this:
if let tabbarController = UIApplication.shared.keyWindow?.rootViewController as?
UITabBarController {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "AChildViewController") as!
AChildViewController
vc.searchString = "text"
tabarController.present(vc, animated: true, completion: nil)
}
Not sure if this is an appropriate way or I'm going to get myself in trouble
It isn't being presented inside the navigation controller - so I'm not getting the top nav bar (including the critical search bar)
It isn't being presented inside the tab bar controller
when I tried to do tabBarController.navigationController? < the navigation controller is nil
EDIT 2 - Solution:
Found an unaccepted answer from a couple of years ago that did the trick for me via: https://stackoverflow.com/a/51763243/12481584
let tabBar: UITabBarController // get your tab bar
tabBar.selectedIndex = 0 // eg. zero. To be sure that you are on correct tab
if let navigation = tabBar.viewControllers?[tabBar.selectedIndex] as? UINavigationController {
let storyboard = UIStoryboard.init(name: "Main", bundle: Bundle.main)
if let chatViewController = storyboard.instantiateViewController(withIdentifier: "chatViewController") as? ChatViewController {
navigation.pushViewController(chatViewController, animated: true)
}
}
The question is not so clear, but I assume your problem is basically how to navigate to a child controller of TAB A from anywhere.
There are multiple ways to do this (deep-linking), but the most straight forward way literally just do your usual approach of pushing, popping, presenting, dismissing of controllers and combine local storage of your data that indicates where you should redirect the user to after tapping a push notification or deep-linking from anywhere such as a website.
An extension of getting the current or top most screen should help, for instance, the is how I do it:
import UIKit
var windowRootController: UIViewController? {
if #available(iOS 13.0, *) {
let windowScene = UIApplication.shared
.connectedScenes
.filter { $0.activationState == .foregroundActive }
.first
if let window = windowScene as? UIWindowScene {
return window.windows.last?.rootViewController
}
return UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController
} else {
return UIApplication.shared.keyWindow?.rootViewController
}
}
/// Category for any controller.
extension UIViewController {
/// Class function to get the current or top most screen.
class func current(controller: UIViewController? = windowRootController) -> UIViewController? {
guard let controller = controller else { return nil }
if let navigationController = controller as? UINavigationController {
return current(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return current(controller: selected)
}
}
if let presented = controller.presentedViewController {
return current(controller: presented)
}
return controller
}
}
Now, onto your specific problem. So assuming you really now handle the redirection to TAB A properly, the next thing you would do is push the Child A after going to the TAB A, and then in the Child A didAppear, put the text in the searchBar and do the searching.
I want to produce some function when user comes out of app
I found applicationDidEnterBackground function in appDelegate file but I can't reach view controller which is open right now therefore I can't reach needed function
I can't use instantiateViewController function because it creates a new one, but I need the info stored in the view which is already open
Is there any way to call function of already open instance of view controller???
I sometimes have a similar requirement, so I made a UIViewController extension with a static method to return the "top" viewController. It looks like this:
extension UIViewController {
static func topViewController(_ viewController: UIViewController? = nil) -> UIViewController? {
let viewController = viewController ?? UIApplication.shared.keyWindow?.rootViewController
if let navigationController = viewController as? UINavigationController, !navigationController.viewControllers.isEmpty {
return self.topViewController(navigationController.viewControllers.last)
} else if let tabBarController = viewController as? UITabBarController,
let selectedController = tabBarController.selectedViewController {
return self.topViewController(selectedController)
} else if let presentedController = viewController?.presentedViewController {
return self.topViewController(presentedController)
}
return viewController
}
}
This allows you to get a reference to the viewController being displayed by calling let top: UIViewController? = UIViewController.topViewController().
I found some way
I am pretty sure it is real stupid way, but it works in my situation because I exactly know which View Controller I need
1st step: create a variable in appDelegate class
class AppDelegate: UIResponder, UIApplicationDelegate {
var myViewController:ExampleViewController?
2nd step: in myViewController class
override func viewDidLoad() {
super.viewDidLoad()
let delegate = UIApplication.shared.delegate as! AppDelegate
delegate. myViewController = self
}
And now I can call any function of myViewController class from AppDelegate class
variable of view controller is optional so I unwrap it with "?" sign and don't have problems when app didEnterBackground from other view controllers
I'm working on an IOS app that uses tabs for navigation. The app gives access to users to a video library. However there are two types of users, those who purchase individual episodes and those who are subscribed. The former only have access to the videos they purchased while the latter have access to every single video in the library.
In my tab bar (in storyboard) I have a Purchases button, but if the user is a subscriber I don't want this tab to appear.
The app checks if a user is logged in upon launching and checks to see what the user status is (buyer or subscriber). I would like to know if there is a way to load different sets of tabs depending on the user type.
If any one could steer me in the right direction I'd really appreciate it. Thanks!
From the top of my head I can think of several ways but this could do it. I am assuming that you somehow know which kind of user is logged in based on the server's response or something similar.
Create your own class that mutates depending on the user eg:
MyTabBarController: UITabBarController {
override func viewDidLoad() {
if (currentUser == admin) {
setupAdminTabBar()
} else {
setupRegularTabBar()
}
}
}
then on each function do something like
func setupRegularTabBar() {
//do this many as many times as root view controllers you want
let searchNavController = createMyNavController(unselectedImage: UIImage(named: "yourimage"), selectedImage: UIImage(named: "yourimage"), rootViewController: UserSearchController(collectionViewLayout: UICollectionViewFlowLayout()))
//add the other controllers that you create like the one above...
viewControllers = [searchNavController]
}
fileprivate func createMyNavController (unselectedImage: UIImage, selectedImage: UIImage, rootViewController : UIViewController = UIViewController()) -> UINavigationController {
let viewController = rootViewController
let navController = UINavigationController(rootViewController: viewController)
navController.tabBarItem.image = unselectedImage
navController.tabBarItem.selectedImage = selectedImage
return navController
}
Subclass UITabBarController and use setViewControllers(_:animated:):
class MyTabBarController: UITabBarController
{
override func viewDidLoad()
{
super.viewDidLoad()
switch user
{
case .buyer:
guard let vc1 = storyboard?.instantiateViewController(withIdentifier: "first"),
let vc2 = storyboard?.instantiateViewController(withIdentifier: "second") else
{
return
}
setViewControllers([vc1, vc2], animated: true)
case .subscriber:
guard let vc3 = storyboard?.instantiateViewController(withIdentifier: "third"),
let vc4 = storyboard?.instantiateViewController(withIdentifier: "fourth") else
{
return
}
setViewControllers([vc3, vc4], animated: true)
}
}
}
You can use the setViewControllers function of UITabBarController:
func setViewControllers(_ viewControllers: [UIViewController]?, animated: Bool)
Set up all the possible controllers in the storyboard with a separate outlet for each one. Then pass an array of the outlets you wish to appear to setViewControllers
I'm trying to access the frontmost controller of the Application during the user navigation using this code:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
println(UIApplication.sharedApplication().keyWindow?.rootViewController)
}
But it seems that the rootViewController always refers to the first controller defined by the storyboard independently by when I'm accessing that property.
Is there something I'm doing wrong or I've misunderstood about the rootViewController property?
rootViewController is indeed the topmost, ultimate view controller owned by UIWindow.
To get the currently displaying view controller, you need to walk down the controller hierarchy. Here is an Objective-C category that you can add to your application, and using a bridging header you'll easily be able to call this UIWindow category from your swift code.
OK, based on the code that Michael pointed out, I wrote some Swift (1.2) code to do the same thing. You can add this as an extension to UIViewController (as I did), UIApplication, or for that matter simply make it a global function.
extension UIViewController {
static func getVisibleViewController () -> UIViewController {
let rootViewController = UIApplication.sharedApplication().keyWindow?.rootViewController
return getVisibleViewControllerFrom(rootViewController!)
}
static func getVisibleViewControllerFrom(viewController: UIViewController) -> UIViewController {
let vcToReturn: UIViewController
if let navController = viewController as? UINavigationController {
vcToReturn = UIViewController.getVisibleViewControllerFrom(navController.visibleViewController)
}
else if let tabBarController = viewController as? UITabBarController {
vcToReturn = UIViewController.getVisibleViewControllerFrom(tabBarController.selectedViewController!)
}
else {
if let presentedViewController = viewController.presentedViewController {
vcToReturn = UIViewController.getVisibleViewControllerFrom(presentedViewController)
}
else {
vcToReturn = viewController
}
}
return vcToReturn
}
}
You'd call this in the following way:
let visibleViewController = UIViewController.getVisibleViewController()
Hope this helps.
Andrew
PS I haven't tried this in Swift 2.0 yet, so I can't guarantee it will work without issues there. I know it won't work (as written) in Swift 1.1 or 1.0.
With the aim to implement a splash screen that only shows once i've modified didFinishLaunchingWithOptions in order to dynamically select the appropriate view controller. The logic seems to work fine, and the view I intended to load is the one launched
However, the UI seems to be missing elements that would otherwise be displayed should I have not altered the didFinishLaunchingWithOptions function.
func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool
{
window = UIWindow(frame: UIScreen.mainScreen().bounds)
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
var entryViewController: UIViewController?
if NSUserDefaults.standardUserDefaults().boolForKey("hasSeenWelcomeScreen") == true
{
entryViewController = storyBoard.instantiateViewControllerWithIdentifier("NavigationController") as? UIViewController
}
else
{
entryViewController = storyBoard.instantiateViewControllerWithIdentifier("WelcomeViewController") as? UIViewController
NSUserDefaults.standardUserDefaults().setValue(true, forKey: "hasSeenWelcomeScreen")
NSUserDefaults.standardUserDefaults().synchronize()
}
self.window?.rootViewController = entryViewController
self.window?.makeKeyAndVisible()
return true
}
My WelcomeViewController is a simple view with 1 label, 1 button and a movie which plays in the background (resembling Spotify/Vine's welcome screen). Debugging the code I can see the initialization methods do get executed, but is just the frame that does not seem to be displayed when I dynamically override the initial view
import UIKit
import MediaPlayer
import QuartzCore
class WelcomeViewController: UIViewController {
var moviePlayerController: MPMoviePlayerController = MPMoviePlayerController()
#IBOutlet weak var loginButton: UIButton!
#IBOutlet weak var appNameLabel: UILabel!
override func viewDidLoad()
{
super.viewDidLoad()
buildMoviePreview()
buildButtonDesign()
}
override func viewWillAppear(animated: Bool)
{
self.view.addSubview(self.moviePlayerController.view)
self.view.addSubview(self.loginButton)
self.view.addSubview(self.appNameLabel)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func prefersStatusBarHidden() -> Bool {
return true
}
private func buildButtonDesign()
{
loginButton.layer.borderColor = UIColor.whiteColor().CGColor
loginButton.layer.borderWidth = 2.0
loginButton.layer.cornerRadius = 7.0
}
private func buildMoviePreview()
{
let filePath = NSBundle.mainBundle().pathForResource("intro", ofType: "mov")
self.moviePlayerController.contentURL = NSURL.fileURLWithPath(filePath)
self.moviePlayerController.movieSourceType = .File
self.moviePlayerController.repeatMode = .One
self.moviePlayerController.view.frame = self.view.bounds
self.moviePlayerController.scalingMode = .AspectFill
self.moviePlayerController.controlStyle = .None
self.moviePlayerController.allowsAirPlay = false
self.moviePlayerController.shouldAutoplay = true
self.moviePlayerController.play()
}
}
For completeness, these are the discrepancies in the layout when using the XCode UI debugger. Please note that they differ even though they implement the same viewController. The only difference is that one has been programmatically set as the initial view, while the other has been set as the initial view through storyboard.
Screenshots of the rendering issue side-by-side
Your approach is... unusual. A Storyboard has a root view controller for a reason and typically at startup you would just let the application handle loading the storyboard and installing that root view controller as the window's main view controller. (The Storyboard loaded is specified in the application target's general settings as the "Main Interface")
In this case, what I would recommend is making your root view controller the "Normal" view of the application... the one you want users to see when they launch the app on a day-to-day basis.
Define your "on first launch" view controller as a separate view controller in the storyboard and add a modal segue from the root view controller to the on first launch view controller.
Then in your applicationDidFinishLaunching, if the user has never seen the first launch controller... simply ask the Storyboard to take that segue. If the user has already seen the first launch presentation that segue will be skipped.
Another issue I see with your code is in your viewWillAppear method. You should not have to add your views as subviews in viewWillAppear... those subviews should already have been set up at the time the view was loaded from the nib file.
The one exception is the view of your movie player, but your movie player is owned by a separate view controller. That separate view controller is detached from the view controller hierarchy and does not have it's own view controller methods called at the right times. (so it never receives calls like "viewWillAppear" that might tell it to get it's movie ready to play).
What you probably want to do is implement "awakeFromNib" and make sure that the movie player's view controller is a sub-controller of this view controller. (so in awakeFromNib for the WelcomeViewController use addChildViewController to make sure the movie controller is in the hierarchy).
It will be better to user 2 storyboards :
one with your welcome screen
the other one with the rest of your app
The application launching wil look like this :
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
var storyBoard : UIStoryboard!
if NSUserDefaults.standardUserDefaults().boolForKey("hasSeenWelcomeScreen") == true {
changeStoryBoard("Main")
}
else {
changeStoryBoard("Welcome")
}
return true
}
func changeStoryBoard(name :String) {
var storyBoard = UIStoryboard(name:name, bundle: nil)
var viewController: AnyObject! = storyBoard.instantiateInitialViewController() ;
self.window!.rootViewController = viewController as UIViewController
}