Passing data between tab viewed controllers in swift? - swift

What it does
When the first page in my tab bar controller loads, I retrieve data from a json file
I store it in an array (in the first view controller)
The data obtained will be displayed in the second view controller. The data is already loaded and stored in an array in the first view controller.
Problem:
I can't figure out a way to pass the data between the two view controllers. Can't pass data based on the segue identifier since it is a tab bar controller
Please help!

If you need to pass the data between view controllers then :
var secondTab = self.tabBarController?.viewControllers[1] as SecondViewController
secondTab.array = firstArray

I ended up using a singleton as Woodster suggested in his answer above.
In Swift 3
Create a new swift file and create a class:
class Items {
static let sharedInstance = Items()
var array = [String]()
}
In any of your view controllers you can access your array like this:
Items.sharedInstance.array.append("New String")
print(Items.sharedInstance.array)

H. Serdar's code example is right, that's the way to access another tab's view controller and give it data.
Keep in mind that when you pass an array in Swift, you're passing it by value, unlike Objective-C, which passes it by reference. This means that changes made by your second view controller won't be reflected in your first view controller, because your second one is using a copy of the array, not the same array. If you want both view controllers to modify the same array, put the array in a class, and pass a single instance of that class around.
Some other considerations:
You could subclass the TabBarController to give it a property that'll store your data, and that would be available to all tabs using:
if let tbc = tabBarController as? YourCustomTabBarSubclass {
println("here's my data \(tbc.array)")
}
In that situation, you'd be accessing the same array from multiple tabs, so changes in one tab would be reflected elsewhere.
I recommend against the approach of using your App Delegate as a centralized place to store data. That's not the purpose of the application's delegate. Its purpose is to handle delegate calls for the application object.
View Controllers should have all the data, encapsulated within them, that they need to do their job. They have a connection to their model data (such as your array, or a reference to a database or a managed object context) as opposed having a view controller reach out to another object by traversing a view controller graph or going into the delegate or even using a global variable. This modular, self contained construction of View Controllers lets you restructure your app for similar but unique designs on different devices, such as presenting a view controller in a popover on one device (like an iPad) and presenting it full screen on another, such as an iPhone.

SWIFT 3
In your first viewcontroller, declare your variable (in your case an array) like you normally would.
In your second viewcontroller, do this:
var yourVariable: YourVariableClass {
get {
return (self.tabBarController!.viewControllers![0] as! FirstViewControllerClass).yourVariable
}
set {
(self.tabBarController!.viewControllers![0] as! FirstViewControllerClass).yourVariable = newValue
}
}
This works because, in a tabbarcontroller all viewcontrollers behind the tab items are initialized. By doing this in your second viewcontroller you are actually getting/setting the variable from/in the first viewcontroller.

For Xcode 11 & Swift 5 + Storyboard + Dependency Injection Approach
Assuming you are using a storyboard this is a method I have devised.
Step 1:
Put an identifier on your tabBarController like I did in the image below.
Step 2:
In the scenedelegate.swift file (NOT appDelegate.swift), add the following code to the appropriate func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { method.
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).
self.window = self.window ?? UIWindow()//#JA- If this scene's self.window is nil then set a new UIWindow object to it.
//#Grab the storyboard and ensure that the tab bar controller is reinstantiated with the details below.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let tabBarController = storyboard.instantiateViewController(withIdentifier: "tabBarController") as! UITabBarController
for child in tabBarController.viewControllers ?? [] {
if let top = child as? StateControllerProtocol {
print("State Controller Passed To:")
print(child.title!)
top.setState(state: stateController)
}
}
self.window!.rootViewController = tabBarController //Set the rootViewController to our modified version with the StateController instances
self.window!.makeKeyAndVisible()
print("Finished scene setting code")
guard let _ = (scene as? UIWindowScene) else { return }
}
You will notice that the scenedelgate.swift file has a member variable; var window: UIWindow?. This used to be part of appDelegate but was changed in xCode 11 and Swift 5 so a lot of similar answers and tutorials will be out of date.
The part in the code that says storyboard.instantiateViewController(withIdentifier: you will want to add the name you used for the parameter. In my screenshot you will see I called it tabBarController.
To make this function work on any type of viewController without having to instantiate each one separately on an index, I've used a protocol strategy called StateControllerProtocol. We will be creating this next along with the StateController which will hold the global variables.
Step 3:
In stateController.swift or whatever you want to name this file, add the following code removing aspects that do not apply to your project.
import Foundation
struct tdfvars{
var lateBED:Double = 0.0
var acuteBED:Double = 0.0
var rbe:Double = 1.4
var t1half:Double = 1.5
var alphaBetaLate:Double = 3.0
var alphaBetaAcute:Double = 10.0
var totalDose:Double = 6000.00
var dosePerFraction:Double = 200.0
var numOfFractions:Double = 30
var totalTime:Double = 168
var ldrDose:Double = 8500.0
}
//#JA - Protocol that view controllers should have that defines that it should have a function to setState
protocol StateControllerProtocol {
func setState(state: StateController)
}
class StateController {
var tdfvariables:tdfvars = tdfvars()
}
The variables you want to share between views I recommend adding to the struct. I named mine tdfvariables but you will want to name this something relevant to your project. Note the protocol defined here as well. This is a protocol that will be added to each viewController as an extension that defines that there should be a function to set its stateController member variable (which we have not defined yet, but will in a later step).
Step 4:
In my case I have 2 views controlled by the tabBarController. StandardRegimenViewController and settingsViewController. This is the code you will want to add for your viewControllers.
import UIKit
class SettingsViewController: UIViewController {
var stateController: StateController?
override func viewDidLoad() {
super.viewDidLoad()
}
}
//#JA - This adds the stateController variable to the viewController
extension SettingsViewController: StateControllerProtocol {
func setState(state: StateController) {
self.stateController = state
}
}
The extension here adds the protocol to your class and adds the function as required by it that we defined earlier in the stateController.swift file. This is what will eventually get the stateController and it's struct values into your viewController.
Step 5:
Use the stateController to get access to your variables! You are done!
Here is some examples of how I did this in one of my controllers.
stateController?.tdfvariables.lateBED = 100
You can read the variables the same way! The advantage of this approach is you are NOT using Singletons and instead Dependency Injection for your viewControllers and anything else that may need access to your variables. Read more about dependency injection to see the benefits vs singletons to learn more.

I have a tabbed view controller in my application and I use the same array for multiple tab views. I accomplish this by declaring the array outside of any classes (in the lines between import UIKit and the class declaration) so that it is essentially a global variable that every view can access. Have you tried this?

You can override the tabBar(didSelect:) method and then index the array of ViewControllers on the UITabViewController, and cast the ViewController to the desired Custom ViewController. No need for shared mutable state and all the problems that come with it.
class SecondViewController: UIViewController {
var array: [Int] = []
override func viewDidLoad() {
super.viewDidLoad()
}
}
class TabViewController: UITabBarController {
override func tabBar(
_ tabBar: UITabBar,
didSelect item: UITabBarItem
) {
super.tabBar(tabBar, didSelect: item)
var secondTab = viewControllers?[1] as? SecondViewController
secondTab?.array = [1, 2, 3]
}
}

Related

Swift UISplitViewController how to go from triple to double columns

I'm having a lot of trouble figuring out how to structure a UISplitViewController.
I want:
A sidebar in the primary view (always)
I want the 1st sidebar navigation item (animals) to show triple (sidebar, animal list, animal detail)
I want the 2nd sidebar navigation item (profile) to show double (sidebar, profile view)
I see other apps doing this (GitHub for example), but I've really got no idea how they're managing it. Resources are hard to find, and most tutorials I've seen just show one or the other column styles.
I'm mostly looking for answers on how to architecture this well, but any code would also be massively appreciated!
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
window?.rootViewController = ViewController(style: .tripleColumn)
window?.makeKeyAndVisible()
}
Root view controller
class ViewController: UISplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
viewControllers = [
SidebarViewController(),
AnimalsViewController(),
AnimalDetailViewController()
]
// Example attempt at removing the secondary view
setViewController(ProfileViewController(), for: .supplementary)
setViewController(nil, for: .secondary)
hide(.secondary)
}
}
Desired behaviour
Animals
Profile
Cheers!
There is no "official" way to do it but it is possible. As far as I can tell, one of the best ways so solve it is to have two instances of UISplitViewController in your root view controller and juggle between them when needed. Here is my approach (approximately):
Disclaimer: This code was consulted with Apple engineers during the last WWDC22 on UIKit Labs. They have confirmed that it is very unfortunate that they currently do not offer a convenient way of doing it, and that this approach is probably the best way to do it. Feedback was filed and its ID passed to the engineers so hopefully we get an official API in the iOS 17 :D
rdar://FB10140263
Step 1. Initialise the UISplitViewControllers
private lazy var doubleColumnSVC: UISplitViewController = {
$0.primaryBackgroundStyle = .sidebar
// setup your SVC here
$0.setViewController(doubleColumnPrimaryNC, for: .primary)
return $0
}(UISplitViewController(style: .doubleColumn))
private lazy var tripleColumnSVC: UISplitViewController = {
$0.primaryBackgroundStyle = .sidebar
// setup your SVC here
$0.setViewController(tripleColumnPrimaryNC, for: .primary)
return $0
}(UISplitViewController(style: .tripleColumn))
Step 2. Initialise your sidebar VC and two separate UINavigationControllers
I have found it to be the most reliable solution for swapping sidebar VC. With a single UINavigationController instance there was a bug that the sidebar would randomly not appear. Two instances solve this problem while still keeping a single SidebarVC with proper focus state and already laid out content.
// Sidebar is shared and swapped between two split views
private lazy var sideBar = YourSideBarViewController()
private lazy var doubleColumnPrimaryNC = UINavigationController(
rootViewController: UIViewController()
)
private lazy var tripleColumnPrimaryNC = UINavigationController(
rootViewController: UIViewController()
)
Step 3. Make a property to store currently displayed SVC
It will come in handy in the next step when toggling between the two instances.
private var current: UISplitViewController?
Step 4. Implement Toggling between two styles when needed
This function should be called every time you want to navigate to a different screen from sidebar.
private func toggleStyleIfNeeded(_ style: UISplitViewController.Style) {
switch style {
case .doubleColumn:
// skip if the desired column style is already set up
if current === doubleColumnSVC { return }
// reassign current
current = doubleColumnSVC
// here add doubleColumnSVC as child view controller
// here add doubleColumnSVC.view as subview
// swap the sidebar
doubleColumnPrimaryNC.setViewControllers([sideBar], animated: false)
// here remove tripleColumnSVC from parent
// here remove tripleColumnSVC.view from superview
case .tripleColumn:
// skip if the desired column style is already set up
if current === tripleColumnSVC { return }
// reassign current
current = tripleColumnSVC
// here add tripleColumnSVC as child view controller
// here add tripleColumnSVC.view as subview
// swap the sidebar
tripleColumnPrimaryNC.setViewControllers([sideBar], animated: false)
// here remove doubleColumnSVC from parent
// here remove doubleColumnSVC.view from superview
default:
return
}
// If you are using UITabBarController for your compact style, assign it here
current?.setViewController(tabBar, for: .compact)
}
In lines that start with "here add" you will need to write your own code. I have simplified the code sample to make it shorter.
Step 5. Enjoy your SVC with dynamic columns!
Now you are basically ready to go! With this simple helper method on your root VC (or whichever one that is handling the navigation and managing the SVCs) you will have all the power that you need to achieve what you wanted, which is a UISplitViewController with dynamic number of columns!
func setViewController(
_ viewController: UIViewController,
for column: UISplitViewController.Column,
style: UISplitViewController.Style
) {
toggleStyleIfNeeded(style)
current?.setViewController(viewController, for: column)
}
We are using this approach in production for a few months now and it works great. The app supports iOS, iPadOS and Mac Catalyst. There are some things like customising the status bar style and getting consistent sidebar button experience a bit tricky to work perfectly but with some adjustments and help from the UISplitViewControllerDelegate everything is possible.
Good luck!
P.S. If anyone have walked this path before and is able to share suggestions, please do! I would love to learn more on how one could improve this dynamic split view experience both for users and developers.
To switch from 3 to 2 columns you must simply reinitialise the UISplitViewController with two columns (UISplitViewController(style: .doubleColumn) and reassign it to the window.rootViewController.
When reinitialising the UISplitViewController, you can either assign existing view controller objects, to maintain the current state, or initialise new ones. In case you assign existing view controller objects, it's probably handy to store these in variables after creating them for the first time.

Pass data to TabBarControllers after intialization?

How would I do something like this?
viewControllers!.forEach
{
$0.view
$0.m = self.m // error here
}
In each tabbarcontroller I defined m, yet this is not working.
I need it done through this as I initialize every tab by this.
Thanks.
The main problem is $0, the shorthand for the first parameter (i.e. a viewController in this case), is always immutable in a closure. There are a few other things to address to...
First of all you will need to subClass UIViewController to allow you to create/access the m property. At the most basic level this will be:
class MyVC: UIViewController {
var m: Int = 0 //giving a default value to save a .init in the example
}
The you will need to create MyVC view controllers, rather than standard UIViewControllers within your AppDelegate / SceneDelegate.
At which point you can adapt your original code to set the m property within each view controller:
if let count = tabBarController.viewControllers?.count {
for i in 0 ..< count {
if let vc = tabBarController.viewControllers?[i] as? MyVC {
vc.view // as per the original, but can't see any point in it
vc.m = m
}
}
}

Passing data back to controllers when struct model modified

I have a series of View Controllers which pass a struct model object down the chain.
If a user modifies the value of a property on the model, I update the view controller's model instance, and now I need to inform the parent view controllers that this object's value has changed.
Previously I would have used classes over structs for my model object and so I wouldn't have this issue as the object would have been directly written to.
But since structs are pass by value, I have to update the state on other view controllers. I have been using a singleton Manager object to handle state changes through a call to updateModel(). Is there a better way?
I have used something similar to this; keep a reference to the neighbouring view controller (with care to avoid a reference cycle) and a property observer on the struct property to update it when it changes.
This could also be updated prior to presenting a new view controller or before a segue, depending on your needs.
class myViewController: UIViewController {
// Your struct
var model: MyStruct? {
didSet {
if let pvc = previousVC {
pvc.model = model
}
}
}
// Keep a reference to the previous view controller on your stack
var previousVC: UIViewController?
override viewDidLoad() {
super.viewDidLoad()
self.model = MyStruct()
}
}

Making ViewController slimmer by moving TableView away

I am quite new into programming and facing some issues while trying to slim down my ViewController by moving creation of the tableView and associated views to the separate class and moving delegates and datasource from VC to separate one.
My constellation of current files and work is as follows:
After network connection, when data are received, I am calling inside a closure class to set view which will have embedded UITableView.
Fetched data are being saved into the CoreData stack
I instantiate view from another class
var detailView: DetailView! { return self.view as? DetailView }
Once I will download first part of the UI (separate call which works fine)
I am moving onto the part which is messy and surpass my abilities
I call a function createReposCard()
dispatchGroup.notify(queue: dispatchQueue) {
DispatchQueue.main.async {
// slide animation
UIView.animate(withDuration: 0.75, delay: 0.5, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [], animations: {
self.detailView.reposCard.center = CGPoint(x: self.detailView.reposCard.center.x, y: self.detailView.reposCard.center.y-UIScreen.main.bounds.height)
}, completion: nil)
self.detailView.createReposCard(for: self.repos)
self.detailView.detailsTableView.reloadData()
self.activityIndicator.stopAnimating()
self.activityIndicator.removeFromSuperview()
}
}
Code is incomplete to depict the problem only, but what it does one by one.
I am passing info about selected user (irrelevant info)
then I am making a reference to my DetailsViewController which still e.g. holds CoreData stack
In initializer I am instantiating detailsViewController and passing it onto the class which holds delegates (I am passing it to have there reference to the CoreData)
class DetailView: UIView {
var selectedUser: User?
var detailsViewController: DetailsViewController!
let detailsTableView: UITableView = {
let tableView = UITableView()
tableView.frame = CGRect.zero
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reposCell")
return tableView
}()
init(selectedUser:User, frame: CGRect) {
super.init(frame: frame)
self.selectedUser = selectedUser
detailsViewController = DetailsViewController()
let tableViewDelegates = TableViewDelegates(detailsViewController: detailsViewController)
detailsTableView.delegate = tableViewDelegates
detailsTableView.dataSource = tableViewDelegates
}
And finally code jumps into the depicted class where I am knocked down by "Unexpectedly found nil while implicitly unwrapping..."
public class TableViewDelegates: NSObject, UITableViewDataSource,UITableViewDelegate {
private let detailsViewController: DetailsViewController
init(detailsViewController: DetailsViewController){
self.detailsViewController = detailsViewController
super.init()
}
public func numberOfSections(in tableView: UITableView) -> Int {
return detailsViewController.fetchedResultsController.sections?.count ?? 1
}
...
Actually I don't know if my concept is good as I feel quite lost, but my intentions were as follows:
Move View creation to other class
Move TableView delegates to other class
Finally - move networking and CoreData to other class.
But as I see, simple data passing is overcoming my abilities.
I think this question can be divided into two parts:
1) Why is my variable nil when I unwrap it?
I don't think we have enough information to answer this accurately, but my overall approach would be like this:
Check what nil variable is being unwrapped;
Make sure this variable is being properly initialized;
Make sure that the object is not being incorrectly deinitialized;
If you're using Storyboard, use the inspectors to check if everything is set correctly.
There's a particular observation about step 2: you should check the order of execution of your methods to make sure that the variable is properly initialized. Why am I emphasizing this? Because there's a chance that some view (e.g., detailView) is initialized like an ordinary UIView, and then you try to access an element that is not part of a UIView object (e.g., a table view). In other words, check if you're setting the custom views before you try to access them.
2) How to structure the project in a more organized way?
This is a more interesting question, and I think that choosing a better approach will help you to avoid issues like what you're experiencing. I will divide this into some topics. Everything here is my personal opinion and doesn't necessarily reflect the best approach, especially because "best" is subjective here.
PersistenceManager class
First, passing a reference of a view controller to another class just to access CoreData doesn't seem like a good option. A better approach would be to have a PersistenceManager class, for example. You could use an object of this class to fetch and save data. You could pass this object instead of the view controller.
In some architectures (e.g., VIPER), it wouldn't be correct for the view controller to access the persistence directly, so it would be more appropriate to pass an array of already fetched objects. For example:
class TableViewController {
private let tableView: UITableView!
private var currentlyDisplayedUsers: [Users]?
func displayUsers(_ users: [Users]) {
self.currentlyDisplayedUsers = users
self.tableView.reloadData()
}
}
In the example above, the tableView would display currentlyDisplayedUsers, which would be updated by the method displayUsers, which would be called by someone else, like a Presenter.
Network class
Second, I think you should have a network class to download data from the internet. You would use instances of the Network class in the application logic. In other words, you would have something like
// This is in the application logic
// In MVC, the logic is in the Controller, while in VIPER the logic is in the Interactor
class ApplicationLogic {
let networkAPI: Network?
...
func fetchUserData() {
networkAPI?.fetchUsers() { data, error in
// Update the UI based on the response
// Using the previous example, you could call displayUsers here
}
}
}
TableView, TableViewDelegate, and TableViewDataSource
Finally, how to organize these guys. UITableViewDelegate is responsible for telling us about events in the table, while UITableViewDataSource is responsible for filling the table with data, which means that both are strongly related to the table view itself. That said, imho, both should be implemented in different swift files, but as extensions of the view controller that has a reference to the table view. Something like
// TableViewController+UITableViewDelegate.swift
extension TableViewController: UITableViewDelegate {
...
}
// TableViewController+UITableViewDataSource.swift
extension TableViewController: UITableViewDataSource {
...
}
Using this approach, the delegate and the data source would have access to the users array, mentioned earlier. Here is an example of how to implement a similar approach using VIPER.
So, I hope I could provide a basic idea on these topics. If you want to understand more about how to structure your code, I suggest researching iOS design patterns and architectural patterns. Some architectural design patterns that are famous in iOS development are MVC, MVP, MVVM, and VIPER.

How to decide what view to display based on global variable

I'm trying to implement an IOS swift app which starts off with a tab bar controller, and in one of the tab bar controllers is an item called "account".
When a user presses the account item, I want the app to decide (onclick event) whether the view that contains sign up/login is displayed or the profile view is displayed based on a global variable "loggedIn" (bool type).
(I've tried navigation controller but what I've understood from that is that it is a sequence of views which can't decide between views)
I want to know how can this be implemented, maybe some kind of "router" if you may that can switch between views...
If you didn't understand here's a picture of what I'm trying to implement
Basic Map of what I'm trying to explain
If you can suggest a more professional way of doing such design please don't hesitate to express your opinion.
I believe a good approach is updating the view controller when loggedIn status changes. If you don't already have, create a class inheriting from UITabBarController to manage your tabs. Here's the code:
class TabController: UITabBarController {}
In your storyboard, select your tab controller, go to the Identity Inspector and set TabController as the custom class. Now TabController will manage all the view controllers in your tab bar.
It's usually not a good approach to use global variables, so let's add loggedIn in the scope of TabController and listen for any of changes in it and update the corresponding view controller:
class TabController: UITabBarController {
var loggedIn = true {
didSet {
updateProfileTab()
}
}
}
Now, whenever you change loggedIn, that change will update the proper tab. Now let's write updateProfileTab():
class TabController: UITabBarController {
func updateProfileTab() {
let viewController: UIViewController
if loggedIn {
viewController = makeProfileViewController()
} else {
viewController = makeLoginViewController()
}
setViewController(viewController, at: 2)
}
func makeProfileViewController() -> ProfileViewController {
// create and return the profile view controller
}
func makeLoginViewController() -> LoginViewController {
// create and return the profile view controller
}
}
Naturally, you might want to write the body of both makeProfileViewController and makeLoginViewController methods. The last thing for TabController is to write setViewController(_:at:) method:
class TabController: UITabBarController {
...
func setViewController(_ viewController: UIViewController, at index: Int) {
let tabBarItem = viewControllers?[index].tabBarItem
viewController.tabBarItem = tabBarItem
viewControllers?[index] = viewController
}
...
}
Now, since TabController manages your tab bar, you can access it from within any of its children view controllers:
guard let tabController = tabBarController as? TabController else { return }
tabController.loggedIn = ...
Also, it's important to select the initial state. So, in the viewDidLoad from one of your tabbed view controllers, you should perform the above code. The first tab (the one that displays first) is probably the best place to do that. Hope this helps!
EDIT
To create your login and signup view controllers, the easiest way to go is by assigning ids in your storyboard. To do that, go to your storyboard, select the view controller, and in the Identity Inspector set a Storyboard ID, which you will use to instantiate the view controller:
func makeProfileViewController() -> ProfileViewController {
let controller = self.storyboard!.instantiateViewController(withIdentifier: "theStoryboardID")
return controller as! ProfileViewController
}
Note that I'm using force unwrap here (!). That's just for brevity. In a real case scenario you will want to use some if let or guard let statements to handle nil values.