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
Related
I have a SignInVc as a starting point and a skip button. If user taps skip, e goes to home page and when he taps any button he is pushed to SignInVc.
The homeVC has TabBar, like its one of the 4 tab bar Vc's.
if let viewControllers = self.navigationController?.viewControllers {
for controller in viewControllers
{
if controller == (tabBarController?.viewControllers![0]){
print("FOUND IT")
}
print(controller)
}
}
While debugging with breakpoints, i can see the home page in navigationController?.viewControllers
But i cannot access it!!, the print is not executed. What should i use in the RHS of == ?
The plan is to push to the homeVC instead of print code.
EDIT:
I'm adding the screen shots of the debug below
Here i want to get to the view controller at Index 2
[
I do not think that you have such hierarchy as you've described.
Really you have
NavigationController -> TabBarController -> HomeViewController or
TabBarController -> NavigationController -> HomeViewController
Properties .navigationController and .tabBarController find nearest accessible Navigation and TabBar controller.
Just exam your hierarchy in storyboard or in code, and you will fix your problem.
UPD.
Based on your screen, you should find tabBar controller first, and find HomeViewController in tabBarController. I think, the code should looks like:
if let tabBar = navController.viewControllers.first(where: { $0 is UITabBarController} ){
let homeController = tabController.viewControllers?.first(where: { $0 is HomeViewController})
print("Home controller: \(home)")
}
I am not sure whether am right,
logically declare a global variable,
var initiateHomePage: Bool? // declare above or outside any swift file
For button actions
case 1
#IBAction func skipButtonTapped(_ sender: Any) {
initiateHomePage = true // should go homepage
}
case 2
#IBAction func anyButtonTapped(_ sender: Any) {
initiateHomePage = false // should go SignInVC
}
Atlast
while executing
if initiateHomePage == true {
// redirect to home page
// use this to redirect to tab bar
if let viewControllers = self.navigationController?.viewControllers {
for controller in viewControllers
{
if controller == (tabBarController?.viewControllers![0]){
print("FOUND IT")
}
print(controller)
}
}
// or use this
let ViewController:UIStoryboard = UIStoryboard(name: "Module", bundle: nil)
let tabBarController = ViewController.instantiateViewController(withIdentifier: "tabBar") as! UITabBarController
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window?.rootViewController = tabBarController
} else {
// redirect to SignINVC
}
I'm trying to learn how to integrate coordinator pattern into iOS development.
I have an app which like this. In the storyboard, it looks like this. The navigation controllers and tabbars are not added in the storyboard because according to coordinator pattern, they will be added programatically.
The first view controller is PhoneViewController which takes user's phone number. This view controller is embedded in a navigation controller. After entering the phone number, it moves to the VerifyPhoneViewController. After verification, it moves to MainViewController a tabbarcontroller which contains three tabs. Each of these view controller will have a separate navigation controller of their own.
I have a protocol which contains all the necessary properties and functions each coordinator needs to implement.
protocol Coordinator {
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
}
I created a separate coordinator called AuthCoordinator for the authentication flow part of the app.
class AuthCoordinator: Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
// The initial view
func start() {
let phoneViewController = PhoneViewController.instantiate()
phoneViewController.coordinator = self
navigationController.pushViewController(phoneViewController, animated: false)
}
func submit(phoneNo: String) {
let verifyPhoneViewController = VerifyPhoneViewController.instantiate()
verifyPhoneViewController.coordinator = self
verifyPhoneViewController.phoneNo = phoneNo
navigationController.pushViewController(verifyPhoneViewController, animated: true)
}
// Move to the tabbarcontroller
func main() {
let mainViewController = MainViewController.instantiate()
navigationController.pushViewController(mainViewController, animated: true)
}
}
The navigation works fine. However there's a small issue.
Notice after moving to the tabbarcontroller, the titles don't show in the navigationbar when I switch between view controllers (I do set them in viewDidLoad method of each view controller). Plus the back button to VerifyPhoneViewController is still there too.
The issue is obvious. The navigationcontroller I initialized for the AuthCoordinator is still there at the top. I'm literally pushing the MainViewController on to that stack.
func main() {
let mainViewController = MainViewController.instantiate()
navigationController.pushViewController(mainViewController, animated: true)
}
What I can't figure out is a way to not do it like this. I can hide the navigationbar in the start method but then it's not ideal because well, it hides the navigationbar and I don't want that.
func start() {
let phoneViewController = PhoneViewController.instantiate()
phoneViewController.coordinator = self
navigationController.navigationBar.isHidden = true
navigationController.pushViewController(phoneViewController, animated: false)
}
Is there a different way to keep the navigationcontroller for the duration of the auth flow and then discard it when/soon after showing the MainViewController?
The demo project is uploaded here.
I have a Tabbed App with two tabs... the first tab has the main Action, and the second has Settings that can be updated. I am trying to pass some variables data from Settings to the Action tab. Based on some suggestions, I have used the following code for the Update button:
#IBAction func updateBut(_ sender: Any) {
let myVC = self.storyboard?.instantiateViewController(withIdentifier: "FirstViewController") as! FirstViewController
myVC.totTime = totalTime
myVC.timeInt = intTime
self.present(myVC, animated: true, completion: nil)
}
The data does pass to the first view controller, however, the tabs have disappeared on this view now. So, how can I get the tabs back on the screen? I am quite the beginner to any form of app development, and am just trying to learn by doing... the Tabbed App has been created using one of the Xcode New Project templates. Thanks.
try this way
(self.tabBarController?.viewControllers?[0] as! FirstViewController).totTime = totalTime
(self.tabBarController?.viewControllers?[0] as! FirstViewController).timeInt = intTime
self.tabBarController?.selectedIndex = 0
self.tabBarController?.tabBar.isHidden = false try this
A much better way to pass data by using protocols, you can define a protocol like below
protocol PassDataDelegate{
func updateFirstVc(totalTime:String)
}
and in your SecondViewController class have a delegate property like below
class SecondViewController:UIViewController{
myDelegate:PassDataDelegate?//declaration part
#IBAction func updateBut(_ sender: Any) {
myDelegate.updateFirstVc(totalTime:totalTime)
}
}
And finally in your UITabController class implement the protocol
class myTabController:UITabController,PassDataDelegate{
var firstController:FirstViewController? //declaration part
var secondController:SecondViewController?
override func viewDidLoad(){
super.viewDidLoad()
//initialize your view controller here
self.firstViewController = FirstViewController()
self.secondViewController = SecondViewController()
self.secondViewController.myDelegate = self //assign delegate to second vc
self.viewcontrollers = [firstController, secondController]
}
updateFirstVc(totalTime:totalTime){
self.firstViewController?.totTime = totalTime
self.selectedIndex = 0// here change the tab to first vc if you want to switch the tab after passing data
}
}
I am new to iOS correct me If I am wrong,
The reason why the TabBar is not visible since you are instantiating new FirstViewController which is present on top of your TabBar.
TabBar by default does this Job or Add the new viewController to the TabBar Stack.
tabBarController.viewControllers.append(myVC)
For passing the data TabBar holds the reference of all its ViewControllers. So you can set or get in each other ViewControllers like this
var yourData{
set{
let yourVC = self.tabBarController?.viewController[0] as? FirstViewController ?? ErrorClass
yourVC.dataObj = newValue
}
get{
let yourVC = self.tabBarController?.viewController[0] as? FirstViewController ?? ErrorClass
return yourVC.dataObj
}
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
Suppose I have three view controllers in a Main.storyboard. Two of the three, vc_login and vc_studyDesc load the other view controller using a UIButton with 'present modally' option.
The other one vc_signup has a UIButton, which may go back to the previous controller. To implement this, I used the following methods:
vc_studyDesc has an identifier of studyDesc; I let it pass its identifier to vc_signup. In the same way, vc_login has login as an identifier.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if sender as! UIButton == qnaSignUp {
let signup = segue.destinationViewController as! vc_signup
signup.latestVC = "studyDesc"}}
This one is in the UIViewController class for vc_signup. By referencing a string latestVC, the method determines which VC to move on.
#IBAction func backBtnClick(sender: UIButton) {
print("latestVS: \(latestVC)")
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier(latestVC)
vc.modalTransitionStyle = UIModalTransitionStyle.CrossDissolve
print("check check")
self.presentViewController(vc, animated: true, completion: nil)}
The problem I have is that the app gets terminated when vc_studyDesc is called by vc_signup. I found that this is because I missed a significant variable which must be loaded in vc_signup.
vc_studyDesc has some data to be referenced from Firebase when it is loaded. I did this by loading a variable postID from the prior vc to vc_studyDesc; which is vc_list.
So I just saved postID using NSUserdefaults.standardUserDefaults(). It is solved but I'm wondering if there's any way to pass data using the way I used in vc_signup.
As far as I see, I cannot find any way to pass the data into vc_studyDesc.swift; for the vc is chosen by its identifier..
Can I pass the variable I want in the way I want?? And adding tags would be appreciated!
So there are a couple problems with this design.
When you instantiate a viewController you are creating a new instance of that class, and presenting it adds it to the stack. Think of the stack like a deck of cards, you start with one card and then add or remove them, the top card being the visible vc. When you are going back to studyDesc you are instantiating and presenting it so you will have 3 VCs in your stack, of which two are studyDesc (the one you started with and the one you add when you try to go back)
To remove a VC from the stack you can use
dismissViewController(animated: true, completion: nil)
or if you have the VCs in a navigation controller you can use
popViewControllerController(animated: true, completion: nil)
in terms of passing information between viewControllers, if the info is in the VC you use to present your new controller you can use prepareForSegue like you already have. To pass information back you should use a delegate pattern. So to implement a delegate pattern in this case you would do the following:
Declare a protocol (not inside your classes, above there but below your import's)
protocol SignUpDelegate {
signInCompleted(infoToPass: AnyObject)
}
Then have your studyDesc class conform to this protocol and implement the function signInCompleted
StudyDescVC: UIViewController, SignUpDelegate {
func signInCompleted(infoToPass: AnyObject) {
// do what you want with the info here
}
}
Then in your signUpVc add a var delegate (which will be used to call the signInCompeleted function)
class SignInVC: UIViewController {
var delegate: SignUpDelegate!
func finishedSigningIn() {
delegate.signInCompleted(infoToPass: //yourinfo)
self.dismissViewControllerAnimated(true, completion: nil)
}
And then in your prepareForSegue set the delegate
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if sender as! UIButton == qnaSignUp {
let signup = segue.destinationViewController as! vc_signup
signup.delegate = self
}
}
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier(latestVC) as! YOUR_VIEW_CONTROLLER_NAME
vc.modalTransitionStyle = UIModalTransitionStyle.CrossDissolve
vc.name = "Andrew"
print("check check")
self.presentViewController(vc, animated: true, completion: nil)
//set a variale or property to your viewController
class YOUR_VIEW_CONTROLLER_NAME: UIViewController {
var name: String?
}