Pass data to TabBarControllers after intialization? - swift

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
}
}
}

Related

Update second view controller in NSSplitViewController

I have a NSSplitViewController in which first viewcontroller displays a table and second viewcontroller should display a viewcontroller from a list of viewcontrollers based on the selection of table row.
I'm using tableViewSelectionDidChange() to get the selected row.
Now I have a bunch of viewcontrollers(storyboard ID given as row number) that I should add and remove to second viewcontroller in NSSplitViewController
How can I do that?
You are on the right path.
Within tableViewSelectionDidChange() you need to instantiate a new viewController from your storyboard using NSStoryboards instantiateController(withIdentifier: String) method. Then you can set this as your splitViews second view controller.
Then you need to create a splitViewItem. You can use the init method which takes a viewController for this (NSSplitViewItem(viewController:)).
Finally you have two possibilities to add the new viewController.
Either you use the add/removeSplitViewItem methods or you set the splitViewItems array directly.
My words in code:
guard let splitViewController = self.parent as? NSSplitViewController,
let viewController = self.storyboard?.instantiateController(withIdentifier: "yourIdentifier") as? NSViewController
else { return }
let item = NSSplitViewItem(viewController: viewController)
// Method one
splitViewController.removeSplitViewItem(splitViewController.splitViewItems[1])
splitViewController.addSplitViewItem(item)
// OR method two
var items = splitViewController.splitViewItems
items[1] = item
splitViewController.splitViewItems = items
Objective C representation of the first method.
NSStoryboard * mainStoryboard = [NSStoryboard storyboardWithName:#"Main" bundle:nil];
NSViewController * sourceViewController = [mainStoryboard instantiateControllerWithIdentifier:identrifier];
NSSplitViewItem * item = [NSSplitViewItem splitViewItemWithViewController:sourceViewController];
[self removeSplitViewItem:[self.splitViewItems objectAtIndex:1]];
[self addSplitViewItem:item];

Swift: get UI objects by ID?

in iOS, is it possible to assign a string ID to UI objects and then retrieve them in the code by that ID?
I am looking for something similar to the Android's findViewById(id)
you can use viewWithTag but tag is type of Int:
let superView = UIView()
let subView = UIView()
subView.tag = 100
superView.addSubview(subView)
let v = superView.viewWithTag(100)
if use xib or storyboard you can bind id like this:
use runtime you can bind obj to obj ,but seems not you want :
objc_setAssociatedObject(superView, "key", subView, .OBJC_ASSOCIATION_RETAIN)
let v = objc_getAssociatedObject(superView, "key")
update:
you can use an enum to get the view :
enum UIKey:String {
case AA = "aa"
func findView(byKey:String ,fromView:UIView) -> UIView {
let v :UIView!
switch self {
// get view from real tag value
case .AA: v = fromView.viewWithTag(1)
}
return v
}
}
then use :
let dict = ["aa":123]
dict.forEach { (key,value) in
let v = UIKey(rawValue: key)?.findView(key, fromView: self.view)
//set v with your value
}
As you are having several viewControllers in the storyboard you're probably looking for UIStoryboards storyboard.instantiateViewControllerWithIdentifier(identifier: String)
// Basic example
let viewController = yourStoryboard.instantiateViewControllerWithIdentifier("id") as! UIViewController
I needed to share one UITableViewCell descendant with a UIButton across multiple UITableViewControllers. So IBOutlet was not an option for me and I needed something similar to Android findByViewId
So I did it in the UITableViewCell descendant when configure it:
for view in self.contentView.subviews {
if let button = view as? UIButton {
button.setTitle(item.label, for: .normal)
}
}
If you have more complicated cell layout I think you may use some specific view properties to identify the required subview.
P.S. from my experience using the Tag property for such purposes is generally not a good idea.

Changing the value of a variable in another a class using a UIButton Swift 2

I have a View Controller (called BackgroundViewController) which has a few buttons, each of them set the color of the background of a different view, my main view. (just called ViewController, yes I started this project about a month ago, before I knew that I should name it something better). For that I set a class, SoundboardBrain, which I intend to use to hold a lot of the app's logic. Here's the class so far:
var backgroundName = String()
init(){
backgroundName = "Image"}
func changeBackgroundName(background: String){
backgroundName = background}
Now, BackgroundViewController is kind of like a settings pane, where the user could select one of the options and a bullet point appears by the one that he checked. Here's one of the of the buttons:
#IBAction func whiteButton(sender: AnyObject){
whiteBullet.hidden = false
imageBullet.hidden = true
}
//Here I call the changeBackground function I defined in SoundboardBrain
SoundboardBrain.changeBackgroundName("White")
//I then print the result of that and I still get "Image" NO MATTER WHAT!
So all I want to know is how to change a variable initialized in a class with a UIButton or another object of a ViewController.
You should keep the instance of SoundBrain somewhere in variable, or use a singleton. You could be initializing a new SoundBrain instance later.
Singleton is better for main app logic. Example:
class SoundboardBrain {
static let shared = SoundboardBrain()
var backgroundName = "Image"
func changeBackgroundName(background: String) {
backgroundName = background
}
}
SoundboardBrain.shared.backgroundName
// now the property is "Image"
// in UIButton
SoundboardBrain.shared.changeBackgroundName("something")
SoundboardBrain.shared.backgroundName
// now it's "something"
Example was made in Playground, but it doesn't matter.

Passing data between tab viewed controllers in 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]
}
}

How to get the previous viewcontroller that pushed my current view

The home page of my app has UIButtons, btnIncome and btnExpense. Pressing on this buttons pushes IncomeVC and ExpenseVC respectevely,which are two UIViewControllers with UITabBar added via xib. The tabBar have 4 items. Selecting on each tab item adds same four view controllers(which contains UITableViews) as the subview of IncomeVC and ExpenseVC,like for eg, DailyVC,WeeklyVC,MonthlyVC,YearlyVC.(ie,for Income ,there is daily,weekly etc and same for Expense) (I have done like that because the IncomeVC and ExpenseVC have a UIButton and a UIView which is common for all tabs).
So the problem is that, if click the btnIncome I have to populate those tableviews with the arrays related to Income and vice versa for Expense. How can I find from which viewController I selected the different tabs(I need to get it from the 4 Views I added as the subview of IncomeVC and ExpenseVC). Do I have to make 8 different views 4 each for Income and expense ?
Thanx.
In Swift 3,
if let navController = self.navigationController, navController.viewControllers.count >= 2 {
let viewController = navController.viewControllers[navController.viewControllers.count - 2]
}
You can get the previous viewController like following code,
NSLog(#"%#",[self.navigationController.viewControllers objectAtIndex:self.navigationController.viewControllers.count-2]);
This will displays the previous viewController name...
In Swift:
let n: Int! = self.navigationController?.viewControllers?.count
let myUIViewController = self.navigationController?.viewControllers[n-2] as! UIViewController
Swift 3
Here is a mashup of the previous answers that can be put into an extension:
extension UIViewController{
var previousViewController:UIViewController?{
if let controllersOnNavStack = self.navigationController?.viewControllers, controllersOnNavStack.count >= 2 {
let n = controllersOnNavStack.count
return controllersOnNavStack[n - 2]
}
return nil
}
}
Edit:
When fetching the previousViewController of a given view controller, call it VC1, in viewWillDisappear, VC1 is already popped of the Navigation Controller Stack. So in this scenario, the above code does not end up fetching the View controller directly above VC1(call it VC2), but the view controller above VC2 (if it exists).
To avoid this problem I just check if VC1 is still on the stack when previousViewController is requested. Here is the updated code:
extension UIViewController{
var previousViewController:UIViewController?{
if let controllersOnNavStack = self.navigationController?.viewControllers{
let n = controllersOnNavStack.count
//if self is still on Navigation stack
if controllersOnNavStack.last === self, n > 1{
return controllersOnNavStack[n - 2]
}else if n > 0{
return controllersOnNavStack[n - 1]
}
}
return nil
}
}
This code assumes that view controller you are sending the previousViewController message to will either be at the top of the navigation stack or not at all.
Swift 5.1 🔸
(Based on previous answers but simpler)
extension UINavigationController {
var previousViewController: UIViewController? {
viewControllers.count > 1 ? viewControllers[viewControllers.count - 2] : nil
}
}
If the reason for needing access to the previous view controller is to know what data to get, I would suggest that you instead give the new view controller the data before you push it on the stack. Or at least enough data so that the view controller know how to get the right data, e.g. a enum or constant or something.
This could be done with a custom initializer, or a property.
Take a look at this blog post for an example: "Passing Data Between View Controllers"
If you are using a storyboard, you can use prepareForSegue:sender to pass the right data. A good tutorial on that can be found here: Beginning Storyboards in iOS 5 Part 2
UIViewController *previousViewController = [[[self navigationController]viewControllers] objectAtIndex:([viewControllers indexOfObject:self]-1)];
Where self will be current view controller.
[viewControllers.count - 2] approach is buggy
If stated view controller is not last view controller of the navigation stack,[viewControllers.count - 2] returns incorrect result.
New crash safe solution:
Swift
extension UIViewController {
var previousViewController: UIViewController? {
guard
let viewControllers = navigationController?.viewControllers,
let index = viewControllers.firstIndex(of: self),
index > 0
else { return nil }
return viewControllers[index - 1]
}
}
Swift
extension UINavigationController {
func getPreviousViewController() -> UIViewController? {
let count = viewControllers.count
guard count > 1 else { return nil }
return viewControllers[count - 2]
}
}
You can certainly reuse the view controllers. I would suggest instead of pushing UIViewController (which contains UITabBar added from IB) when the button is pressed, you can push UITabBarController which contains 4 View Controllers (daily, weekly, monthly, and yearly).
You UITabBarController could have an enum property (Income and Expense) and this can be assigned when you push this UITabBarController based on the button pressed. From there, you can assign another enum property (Income and Expense) to the 4 UIViewControllers to show the correct type of data in each view controller.
Check out this post: How to identify previous view controller in navigation stack
Tony is right, you can get the previous VC from the viewControllers array but at index n-2.