I have a few controllers. for some reason, I'm using UISegmentedControl instead of tab bar.
each few controllers download data from the servers. my problem is, if I move to next view controller and go back to the previous view controllers, I need to redownload again.
how to change view controller with UISegmentedControl without destroy the previous controller, so I don't need to redownload again. each time I move to different viewcontroller
here's my code
class ContentViewController: UIViewController {
private let homeViewController: HomeViewController!
private let aboutViewController: AboutViewController!
private let liveTVViewController: LiveTVViewController!
private let programsViewController: ProgramsViewController!
private var currentViewController: UIViewController!
var userDeviceType: Int!
override func viewDidLoad() {
super.viewDidLoad()
let viewController = viewControllerForSegmentIndex(0)
self.addChildViewController(viewController)
viewController.view.frame = self.view.bounds
self.view.addSubview(viewController.view)
currentViewController = viewController
NSNotificationCenter.defaultCenter().addObserver(self, selector: "segmentChanged:", name: "SegmentChangedNotification", object: nil)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func segmentChanged(notification: NSNotification) {
let userInfo = notification.userInfo as [String: AnyObject]
let selectedIndex = userInfo["selectedIndex"] as Int
let viewController = viewControllerForSegmentIndex(selectedIndex)
self.addChildViewController(viewController)
self.transitionFromViewController(currentViewController,
toViewController: viewController,
duration: 0.0,
options: UIViewAnimationOptions.CurveEaseIn,
animations: { () -> Void in
self.currentViewController.view.removeFromSuperview()
viewController.view.frame = self.view.bounds
self.view.addSubview(viewController.view)
}) { (finished: Bool) -> Void in
viewController.didMoveToParentViewController(self)
self.currentViewController.removeFromParentViewController()
self.currentViewController = viewController
}
}
func viewControllerForSegmentIndex(index: Int) -> UIViewController {
var viewController: UIViewController
if index == 0 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("HomePage") as HomeViewController
(viewController as HomeViewController).userDeviceType = userDeviceType
} else if index == 1 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("ProgramsPage") as ProgramsViewController
} else if index == 2 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("LiveTVPage") as LiveTVViewController
} else if index == 3 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("AboutPage") as AboutViewController
} else {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("HomePage") as HomeViewController
}
return viewController
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
thank you very much and sorry for my bad English
You should adopt the MVC design pattern. This will allow you to store the data you need downloaded in the Model. Then, when a certain view controller is loaded, you simply ask the model if that data exists. If it does, you'll get it back. Otherwise, you can download it as normal.
To further explain:
The Model-View-Controller (MVC) design pattern assigns objects in an application one of three roles: model, view, or controller. The pattern defines not only the roles objects play in the application, it defines the way objects communicate with each other.
Model objects encapsulate the data specific to an application and define the logic and computation that manipulate and process that data. For example, a model object might represent a character in a game or a contact in an address book. A model object can have to-one and to-many relationships with other model objects, and so sometimes the model layer of an application effectively is one or more object graphs. Much of the data that is part of the persistent state of the application (whether that persistent state is stored in files or databases) should reside in the model objects after the data is loaded into the application. Because model objects represent knowledge and expertise related to a specific problem domain, they can be reused in similar problem domains. Ideally, a model object should have no explicit connection to the view objects that present its data and allow users to edit that data—it should not be concerned with user-interface and presentation issues.
User actions in the view layer that create or modify data are communicated through a controller object and result in the creation or updating of a model object. When a model object changes (for example, new data is received over a network connection), it notifies a controller object, which updates the appropriate view objects
The above quotes are from the link I mentioned in the first paragraph.
You are instantiate new view controllers in viewControllerForSegmentIndex(index: Int) by mistake.
What you should do to avoid instantiate each view controller every time you switch back to it is to modify the properties as:
private let homeViewController: HomeViewController = self.storyboard!.instantiateViewControllerWithIdentifier("HomePage") as HomeViewController
private let aboutViewController: AboutViewController = self.storyboard!.instantiateViewControllerWithIdentifier("AboutPage") as AboutViewController
private let liveTVViewController: LiveTVViewController = self.storyboard!.instantiateViewControllerWithIdentifier("LiveTVPage") as LiveTVViewController
private let programsViewController: ProgramsViewController = self.storyboard!.instantiateViewControllerWithIdentifier("ProgramsPage") as ProgramsViewController
And change viewControllerForSegmentIndex(index: Int) to:
func viewControllerForSegmentIndex(index: Int) -> UIViewController {
var viewController: UIViewController
switch index {
case 0:
return homeViewController
case 1:
return programsViewController
case 2:
return liveTVViewController
case 3:
return aboutViewController
default:
return homeViewController
}
}
Related
In my app, the main ViewController is getting data from a sensor 60 times a second. In order to display the data, I have two ContainerViews, one that displays the raw data and another that displays it in a graph.
How do I either constantly send data from my mainVC to the ContainerView or let the ContiainerViews access variables in my mainVC?
There are lots of ways to slice this.
I would advise against collecting that data from your sensors in a view controller. That's not really a view controller's job. It gets worse when there are multiple objects who need that sensor data.
Probably the cleanest design would be to create a separate object (I'll call it a SensorManager) that collects your sensor data and passes it to anybody who cares.
You could have the SensorManager use the NotificationCenter to broadcast notifications, and then have all interested objects add observers for the notifications that they care about. That gives you very loose coupling between the SensorManager and the objects that get notified about sensor data. The downside is that the code is harder to debug.
Alternately, could set up your SensorManager to have an array of objects that it notifies. I would define a protocol that has one or more methods that get called with sensor data, and have the SensorManager maintain an array of client objects that conform to that protocol. When the SensorManager has new sensor data, it would loop through the array of client objects and call the appropriate method on each to tell each one about the new data. This second option is sort of like the delegate design pattern, but is a one-to-many, where the delegate pattern is a one-to-one passing of info.
If you are wedded to the idea of collecting the sensor data in your main view controller and you create your child view controllers using embed segues then you could write a prepareForSegue() method in your main view controller that looks for destination view controllers that conform to a protocol. Let's call it SensorDataListener. The main view controller could save those objects in an array and notify the objects about new sensor data using the methods in the protocol. (This last approach is similar to the approach of creating a SensorManager object, but instead it would be the main view controller serving that role.)
//At the top of your class:
protocol SensorDataListener {
func newSensorData(_ SensorData)
}
var sensorClients = [SensorDataListener]()
//...
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let dest = segue.desination as SensorDataListener {
sensorClients.append(dest)
}
}
And then when you receive new data
sensorClients.forEach { sensorClient in
sensorClient.newSensorData(sensorData)
}
Have both of your child view controllers as variables in main view controller and hook them up from self.childViewControllers of main view controller in viewDidLoad like so.
class ViewController: UIViewController {
var firstViewController: FirstViewController!
var secondViewController: SecondViewController!
override func viewDidLoad() {
super.viewDidLoad()
for vc in self.childViewControllers {
if let firstViewController = vc as? FirstViewController {
self.firstViewController = firstViewController
}
if let secondViewController = vc as? SecondViewController {
self.secondViewController = secondViewController
}
}
}
func sensorDataUpdated(data: Any) {
self.firstViewController.data = data
self.secondViewController.data = data
}
}
And here's an example of how one of your inner view controllers would work, the logic is the same for both of them:
class FirstViewController: UIViewController {
var data: Any? {
didSet {
self.updateUI();
}
}
func updateUI() {
guard let data = self.data else { return }
// Perform UI updates
}
}
Say I have multiple view controllers in my Swift app and I want to be able to pass data between them. If I'm several levels down in a view controller stack, how do I pass data to another view controller? Or between tabs in a tab bar view controller?
(Note, this question is a "ringer".) It gets asked so much that I decided to write a tutorial on the subject. See my answer below.
Your question is very broad. To suggest there is one simple catch-all solution to every scenario is a little naïve. So, let's go through some of these scenarios.
The most common scenario asked about on Stack Overflow in my experience is the simple passing information from one view controller to the next.
If we're using storyboard, our first view controller can override prepareForSegue, which is exactly what it's there for. A UIStoryboardSegue object is passed in when this method is called, and it contains a reference to our destination view controller. Here, we can set the values we want to pass.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "MySegueID" {
if let destination = segue.destination as? SecondController {
destination.myInformation = self.myInformation
}
}
}
Alternatively, if we're not using storyboards, then we're loading our view controller from a nib. Our code is slightly simpler then.
func showNextController() {
let destination = SecondController(nibName: "SecondController", bundle: nil)
destination.myInformation = self.myInformation
show(destination, sender: self)
}
In both cases, myInformation is a property on each view controller holding whatever data needs to be passed from one view controller to the next. They obviously don't have to have the same name on each controller.
We might also want to share information between tabs in a UITabBarController.
In this case, it's actually potentially even simpler.
First, let's create a subclass of UITabBarController, and give it properties for whatever information we want to share between the various tabs:
class MyCustomTabController: UITabBarController {
var myInformation: [String: AnyObject]?
}
Now, if we're building our app from the storyboard, we simply change our tab bar controller's class from the default UITabBarController to MyCustomTabController. If we're not using a storyboard, we simply instantiate an instance of this custom class rather than the default UITabBarController class and add our view controller to this.
Now, all of our view controllers within the tab bar controller can access this property as such:
if let tbc = self.tabBarController as? MyCustomTabController {
// do something with tbc.myInformation
}
And by subclassing UINavigationController in the same way, we can take the same approach to share data across an entire navigation stack:
if let nc = self.navigationController as? MyCustomNavController {
// do something with nc.myInformation
}
There are several other scenarios. By no means does this answer cover all of them.
This question comes up all the time.
One suggestion is to create a data container singleton: An object that gets created once and only once in the life of your application, and persists for the life of your app.
This approach is well suited for a situation when you have global app data that needs to be available/modifiable across different classes in your app.
Other approaches like setting up one-way or 2-way links between view controllers are better suited to situations where you are passing information/messages directly between view controllers.
(See nhgrif's answer, below, for other alternatives.)
With a data container singleton, you add a property to your class that stores a reference to your singleton, and then use that property any time you need access.
You can set up your singleton so that it saves it's contents to disk so that your app state persists between launches.
I created a demo project on GitHub demonstrating how you can do this. Here is the link:
SwiftDataContainerSingleton project on GitHub
Here is the README from that project:
SwiftDataContainerSingleton
A demonstration of using a data container singleton to save application state and share it between objects.
The DataContainerSingleton class is the actual singleton.
It uses a static constant sharedDataContainer to save a reference to the singleton.
To access the singleton, use the syntax
DataContainerSingleton.sharedDataContainer
The sample project defines 3 properties in the data container:
var someString: String?
var someOtherString: String?
var someInt: Int?
To load the someInt property from the data container, you'd use code like this:
let theInt = DataContainerSingleton.sharedDataContainer.someInt
To save a value to someInt, you'd use the syntax:
DataContainerSingleton.sharedDataContainer.someInt = 3
The DataContainerSingleton's init method adds an observer for the UIApplicationDidEnterBackgroundNotification. That code looks like this:
goToBackgroundObserver = NSNotificationCenter.defaultCenter().addObserverForName(
UIApplicationDidEnterBackgroundNotification,
object: nil,
queue: nil)
{
(note: NSNotification!) -> Void in
let defaults = NSUserDefaults.standardUserDefaults()
//-----------------------------------------------------------------------------
//This code saves the singleton's properties to NSUserDefaults.
//edit this code to save your custom properties
defaults.setObject( self.someString, forKey: DefaultsKeys.someString)
defaults.setObject( self.someOtherString, forKey: DefaultsKeys.someOtherString)
defaults.setObject( self.someInt, forKey: DefaultsKeys.someInt)
//-----------------------------------------------------------------------------
//Tell NSUserDefaults to save to disk now.
defaults.synchronize()
}
In the observer code it saves the data container's properties to NSUserDefaults. You can also use NSCoding, Core Data, or various other methods for saving state data.
The DataContainerSingleton's init method also tries to load saved values for it's properties.
That portion of the init method looks like this:
let defaults = NSUserDefaults.standardUserDefaults()
//-----------------------------------------------------------------------------
//This code reads the singleton's properties from NSUserDefaults.
//edit this code to load your custom properties
someString = defaults.objectForKey(DefaultsKeys.someString) as! String?
someOtherString = defaults.objectForKey(DefaultsKeys.someOtherString) as! String?
someInt = defaults.objectForKey(DefaultsKeys.someInt) as! Int?
//-----------------------------------------------------------------------------
The keys for loading and saving values into NSUserDefaults are stored as string constants that are part of a struct DefaultsKeys, defined like this:
struct DefaultsKeys
{
static let someString = "someString"
static let someOtherString = "someOtherString"
static let someInt = "someInt"
}
You reference one of these constants like this:
DefaultsKeys.someInt
Using the data container singleton:
This sample application makes trival use of the data container singleton.
There are two view controllers. The first is a custom subclass of UIViewController ViewController, and the second one is a custom subclass of UIViewController SecondVC.
Both view controllers have a text field on them, and both load a value from the data container singlelton's someInt property into the text field in their viewWillAppear method, and both save the current value from the text field back into the `someInt' of the data container.
The code to load the value into the text field is in the viewWillAppear: method:
override func viewWillAppear(animated: Bool)
{
//Load the value "someInt" from our shared ata container singleton
let value = DataContainerSingleton.sharedDataContainer.someInt ?? 0
//Install the value into the text field.
textField.text = "\(value)"
}
The code to save the user-edited value back to the data container is in the view controllers' textFieldShouldEndEditing methods:
func textFieldShouldEndEditing(textField: UITextField) -> Bool
{
//Save the changed value back to our data container singleton
DataContainerSingleton.sharedDataContainer.someInt = textField.text!.toInt()
return true
}
You should load values into your user interface in viewWillAppear rather than viewDidLoad so that your UI updates each time the view controller is displayed.
Another alternative is to use the notification center (NSNotificationCenter) and post notifications. That is a very loose coupling. The sender of a notification doesn't need to know or care who's listening. It just posts a notification and forgets about it.
Notifications are good for one-to-many message passing, since there can be an arbitrary number of observers listening for a given message.
Swift 4
There are so many approaches for data passing in swift. Here I am adding some of the best approaches of it.
1) Using StoryBoard Segue
Storyboard segues are very much useful for passing data in between Source and Destination View Controllers and vice versa also.
// If you want to pass data from ViewControllerB to ViewControllerA while user tap on back button of ViewControllerB.
#IBAction func unWindSeague (_ sender : UIStoryboardSegue) {
if sender.source is ViewControllerB {
if let _ = sender.source as? ViewControllerB {
self.textLabel.text = "Came from B = B->A , B exited"
}
}
}
// If you want to send data from ViewControllerA to ViewControllerB
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.destination is ViewControllerB {
if let vc = segue.destination as? ViewControllerB {
vc.dataStr = "Comming from A View Controller"
}
}
}
2) Using Delegate Methods
ViewControllerD
//Make the Delegate protocol in Child View Controller (Make the protocol in Class from You want to Send Data)
protocol SendDataFromDelegate {
func sendData(data : String)
}
import UIKit
class ViewControllerD: UIViewController {
#IBOutlet weak var textLabelD: UILabel!
var delegate : SendDataFromDelegate? //Create Delegate Variable for Registering it to pass the data
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
textLabelD.text = "Child View Controller"
}
#IBAction func btnDismissTapped (_ sender : UIButton) {
textLabelD.text = "Data Sent Successfully to View Controller C using Delegate Approach"
self.delegate?.sendData(data:textLabelD.text! )
_ = self.dismiss(animated: true, completion:nil)
}
}
ViewControllerC
import UIKit
class ViewControllerC: UIViewController , SendDataFromDelegate {
#IBOutlet weak var textLabelC: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func btnPushToViewControllerDTapped( _ sender : UIButton) {
if let vcD = self.storyboard?.instantiateViewController(withIdentifier: "ViewControllerD") as? ViewControllerD {
vcD.delegate = self // Registring Delegate (When View Conteoller D gets Dismiss It can call sendData method
// vcD.textLabelD.text = "This is Data Passing by Referenceing View Controller D Text Label." //Data Passing Between View Controllers using Data Passing
self.present(vcD, animated: true, completion: nil)
}
}
//This Method will called when when viewcontrollerD will dismiss. (You can also say it is a implementation of Protocol Method)
func sendData(data: String) {
self.textLabelC.text = data
}
}
Instead of creating a data controller singelton I would suggest to create a data controller instance and pass it around. To support dependency injection I would first create a DataController protocol:
protocol DataController {
var someInt : Int {get set}
var someString : String {get set}
}
Then I would create a SpecificDataController (or whatever name would currently be appropriate) class:
class SpecificDataController : DataController {
var someInt : Int = 5
var someString : String = "Hello data"
}
The ViewController class should then have a field to hold the dataController. Notice that the type of dataController is the protocol DataController. This way it's easy to switch out data controller implementations:
class ViewController : UIViewController {
var dataController : DataController?
...
}
In AppDelegate we can set the viewController's dataController:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if let viewController = self.window?.rootViewController as? ViewController {
viewController.dataController = SpecificDataController()
}
return true
}
When we move to a different viewController we can pass the dataController on in:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
...
}
Now when we wish to switch out the data controller for a different task we can do this in the AppDelegate and do not have to change any other code that uses the data controller.
This is of course overkill if we simply want to pass around a single value. In this case it's best to go with nhgrif's answer.
With this approach we can separate view form the logic part.
As #nhgrif pointed out in his excellent answer, there are lots of different ways that VCs (view controllers) and other objects can communicate with each other.
The data singleton I outlined in my first answer is really more about sharing and saving global state than about communicating directly.
nhrif's answer lets you send information directly from the source to the destination VC. As I mentioned in reply, it's also possible to send messages back from the destination to the source.
In fact, you can set up an active one-way or 2-way channel between different view controllers. If the view controllers are linked via a storyboard segue, the time to set up the links is in the prepareFor Segue method.
I have a sample project on Github that uses a parent view controller to host 2 different table views as children. The child view controllers are linked using embed segues, and the parent view controller wires up 2-way links with each view controller in the prepareForSegue method.
You can find that project on github (link). I wrote it in Objective-C, however, and haven't converted it to Swift, so if you're not comfortable in Objective-C it might be a little hard to follow
SWIFT 3:
If you have a storyboard with identified segues use:
func prepare(for segue: UIStoryboardSegue, sender: Any?)
Although if you do everything programmatically including navigation between different UIViewControllers then use the method:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)
Note: to use the second way you need to make your UINavigationController, you are pushing UIViewControllers on, a delegate and it needs to conform to the protocol UINavigationControllerDelegate:
class MyNavigationController: UINavigationController, UINavigationControllerDelegate {
override func viewDidLoad() {
self.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
// do what ever you need before going to the next UIViewController or back
//this method will be always called when you are pushing or popping the ViewController
}
}
It depends when you want to get data.
If you want to get data whenever you want, can use a singleton pattern. The pattern class is active during the app runtime. Here is an example of the singleton pattern.
class AppSession: NSObject {
static let shared = SessionManager()
var username = "Duncan"
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print(AppSession.shared.username)
}
}
If you want to get data after any action, can use NotificationCenter.
extension Notification.Name {
static let loggedOut = Notification.Name("loggedOut")
}
#IBAction func logoutAction(_ sender: Any) {
NotificationCenter.default.post(name: .loggedOut, object: nil)
}
NotificationCenter.default.addObserver(forName: .loggedOut, object: nil, queue: OperationQueue.main) { (notify) in
print("User logged out")
}
The way that I would do it would be instead of passing data between view controllers, I would just declare a variable globally. You can even do this with a function!
For example:
var a = "a"
func abc() {
print("abc")
}
class ViewController: UIViewController {
}
I was wondering, when passing data using prepareForSegue, can you pass data to a View Controller later in the app? For example on the first ViewController I have the user enter their name. It's not until the very end, so a few views later, do I need to display their name. Is there a way to pass their name without having to go to the end view right away?
Use a Coordinator.
It's really easy to decouple your ViewControllers:
instead of using segues give every ViewController a delegate
create a coordinator object (this object knows your screen flow, not your screens)
the coordinator creates the ViewControllers (it can use UIStoryboard instantiateViewController(withIdentifier:) so ViewController A does not have to know that ViewController B exists
instead of calling performSegue you just call your delegate and pass in the data
Benefits
Simple to use
Easy to reorder screens in a flow
Highly decoupled (easier testing)
Very nice for A/B testing
Scales a lot (you can have multiple coordinators, one for each flow)
Sample
Let's say you have 3 VCs, the first one asks for your name, the second for your age and the third displays the data. It would make no sense that AgeViewController knew that NameViewController existed, later on you may want to change their order or even merge them.
Name View Controller
protocol NameViewControllerDelegate: class {
func didInput(name: String)
}
class NameViewController: UIViewController {
weak var delegate: NameViewControllerDelegate?
#IBOutlet var nameTextField: UITextField!
//Unimportant stuff ommited
#IBAction func submitName(sender: Any) {
guard let name = nameTextField.text else {
// Do something, it's up to you what
return
}
delegate?.didInput(name: name)
}
}
Age View Controller
protocol AgeViewControllerDelegate: class {
func didInput(age: Int)
}
class AgeViewController: UIViewController {
weak var delegate: AgeViewControllerDelegate?
#IBOutlet var ageTextField: UITextField!
//Unimportant stuff ommited
#IBAction func submitAge(sender: Any) {
guard let ageString = ageTextField.text,
let age = Int(ageString) else {
// Do something, it's up to you what
return
}
delegate?.didInput(age: age)
}
}
Displayer View Controller
class DisplayerViewController: UIViewController {
var age: Int?
var name: String?
}
Coordinator
class Coordinator {
var age: Int?
var name: String?
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
fileprivate lazy var storyboard: UIStoryboard = {
return UIStoryboard(name: "MyStoryboard", bundle: nil)
}()
//This works if you name your screns after their classes
fileprivate func viewController<T: UIViewController>(withType type: T.Type) -> T {
return storyboard.instantiateViewController(withIdentifier: String(describing: type(of: type))) as! T
}
func start() -> UIViewController {
let viewController = self.viewController(withType: NameViewController.self)
viewController.delegate = self
navigationController.viewControllers = [viewController]
return viewController
}
}
Coordinator + Name View Controller Delegate
extension Coordinator: NameViewControllerDelegate {
func didInput(name: String){
self.name = name
let viewController = self.viewController(withType: AgeViewController.self)
viewController.delegate = self
navigationController.pushViewController(viewController, animated: true)
}
}
Coordinator + Age View Controller Delegate
extension Coordinator: AgeViewControllerDelegate {
func didInput(age: Int) {
self.age = age
let viewController = self.viewController(withType: DisplayerViewController.self)
viewController.age = age
viewController.name = name
navigationController.pushViewController(viewController, animated: true)
}
}
Not really. You can pass view by view the item but it's not a proper way of doing things.
I suggest you to have a Static Manager or this kind of stuff to store the information globally in your app to retrieve it later
All the solution are pretty good. Possible you can try the below model also
1. DataModel class
1.1 Should be singleton class
1.2 Declare value
Step 1 : ViewCOntroller-one
1 Create the Sharedinstance of singleton class
1.1 Assign the value
Step 3 :ViewController-two
1 Create the Sharedinstance of singleton class
1.1 Get the value
Let's consider the following case:
I have a tab bar application where tapping each tab bar item takes user to other view that is handled by different view controller(typical pattern).
In one of my controllers I have method that downloads the important data and I want them to be global for whole application. What design pattern should I use?
One way to do that is to store this data using persistence such as core data, but is it the only way to make data visible for all view controllers? Maybe app delegate is able to perform such actions?
How in general you solve such situation where you have some data or variable which should be visible for all view controllers in your project?
Note that I'm not asking about persisting data across launches of the app, I just wonder how to make some data global in terms of the whole project.
Dont (emphasize DON'T) use following:
Singletons
AppDelegate (just another Singleton)
NSUserDefaults
Rather Don't:
Core Data
Do:
pass in either during instantiation or via properties
Why?
The DON'Ts messes up your memory
the Rather Don't messes with several principals of SOLID.
How would you do it correctly:
Create a base view controller that has a property that takes your data, make all your view controller inherit from it.
subclass UITabBarController
if a new view controller is selected, set the data to the view controller
the implementation is a bit tricky, this is from a real world app
class ContentTabBarController : UITabBarController {
private var kvoSelectedViewControllerContext: UInt8 = 1
required init(coder aDecoder: NSCoder) {
self.addObserver(self, forKeyPath: "selectedViewController", options: .New | .Old | .Initial , context: &kvoSelectedViewControllerContext)
}
deinit{
self.removeObserver(self, forKeyPath: "selectedViewController")
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if context == &kvoSelectedViewControllerContext {
var targetVC : UIViewController?
if let viewController = change["new"] as? UIViewController{
if let oldViewController = change["old"] as? UIViewController{
if viewController != oldViewController {
targetVC = viewController
}
}
} else {
targetVC = self.viewControllers![0] as? UIViewController
}
self.configureTargetViewController(targetVC)
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.translucent = false
}
func configureTargetViewController(viewController: UIViewController?){
//setup data
}
}
How does the tab bar controller get the data.
Well, that is up to you. It could fetch core data, it could actually pass a fetching manager as data. It could read from disc or network. But for a sane design it should not do it itself but use another class for it. and an instance of this fetcher class should be set from outside the tab bar controller, i.e. from the App Delegate.
One easy way would be to make a struct and make it hold variables. Then, you can edit it anytime you would want to. For example:
struct Variables {
static var number = 4
}
Then you can edit the data inside Variables in any view controller you want by doing this code.
Variables.number = 6 //or any other number you want
A cleaner and efficient, although not necessarily different, way to do this is to create a singleton class, e.g. AppData, which you can access in a variety of ways, and which would be available to all your other classes. It has the benefit of separating your app-specific stuff from the app delegate stuff. You might define the class this way:
#interface AppData : NSObject
// Perhaps you'll declare some class methods here & objects...
#end
you can define ivars for the AppData class, and then manage a singleton instance of AppData. Use a class method, e.g. +sharedInstance, to get a handle to the singleton on which you could then call mehods. For example,
[[AppData sharedInstance] someMethod:myArgument];
Your implementation of +sharedInstance can be where you manage the actual creation of the singleton, which the method ultimately returns.
Try this simple method,
1) Create a variable in appdelegate.swift that could be visible to all viewcontroller.
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
...
...
var Check:String!="" // to pass string
...
...
}
2) Create appdelegate instance in any viewcontroller
viewcontroller1.swift
import UIKit
class ViewController: UIViewController {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
...
...
override func viewDidLoad() {
super.viewDidLoad()
...
var tmp = "\(appDelegate.Check)"
appDelegate.Check="Modified"
}
}
Viewcontroller2.swift
import UIKit
class ViewController: UIViewController {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
...
...
override func viewDidLoad() {
super.viewDidLoad()
...
var getdata = "\(appDelegate.Check)"
println("Check data : \(getdata)") // Output : Check data : Modified
}
}
Say I have multiple view controllers in my Swift app and I want to be able to pass data between them. If I'm several levels down in a view controller stack, how do I pass data to another view controller? Or between tabs in a tab bar view controller?
(Note, this question is a "ringer".) It gets asked so much that I decided to write a tutorial on the subject. See my answer below.
Your question is very broad. To suggest there is one simple catch-all solution to every scenario is a little naïve. So, let's go through some of these scenarios.
The most common scenario asked about on Stack Overflow in my experience is the simple passing information from one view controller to the next.
If we're using storyboard, our first view controller can override prepareForSegue, which is exactly what it's there for. A UIStoryboardSegue object is passed in when this method is called, and it contains a reference to our destination view controller. Here, we can set the values we want to pass.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "MySegueID" {
if let destination = segue.destination as? SecondController {
destination.myInformation = self.myInformation
}
}
}
Alternatively, if we're not using storyboards, then we're loading our view controller from a nib. Our code is slightly simpler then.
func showNextController() {
let destination = SecondController(nibName: "SecondController", bundle: nil)
destination.myInformation = self.myInformation
show(destination, sender: self)
}
In both cases, myInformation is a property on each view controller holding whatever data needs to be passed from one view controller to the next. They obviously don't have to have the same name on each controller.
We might also want to share information between tabs in a UITabBarController.
In this case, it's actually potentially even simpler.
First, let's create a subclass of UITabBarController, and give it properties for whatever information we want to share between the various tabs:
class MyCustomTabController: UITabBarController {
var myInformation: [String: AnyObject]?
}
Now, if we're building our app from the storyboard, we simply change our tab bar controller's class from the default UITabBarController to MyCustomTabController. If we're not using a storyboard, we simply instantiate an instance of this custom class rather than the default UITabBarController class and add our view controller to this.
Now, all of our view controllers within the tab bar controller can access this property as such:
if let tbc = self.tabBarController as? MyCustomTabController {
// do something with tbc.myInformation
}
And by subclassing UINavigationController in the same way, we can take the same approach to share data across an entire navigation stack:
if let nc = self.navigationController as? MyCustomNavController {
// do something with nc.myInformation
}
There are several other scenarios. By no means does this answer cover all of them.
This question comes up all the time.
One suggestion is to create a data container singleton: An object that gets created once and only once in the life of your application, and persists for the life of your app.
This approach is well suited for a situation when you have global app data that needs to be available/modifiable across different classes in your app.
Other approaches like setting up one-way or 2-way links between view controllers are better suited to situations where you are passing information/messages directly between view controllers.
(See nhgrif's answer, below, for other alternatives.)
With a data container singleton, you add a property to your class that stores a reference to your singleton, and then use that property any time you need access.
You can set up your singleton so that it saves it's contents to disk so that your app state persists between launches.
I created a demo project on GitHub demonstrating how you can do this. Here is the link:
SwiftDataContainerSingleton project on GitHub
Here is the README from that project:
SwiftDataContainerSingleton
A demonstration of using a data container singleton to save application state and share it between objects.
The DataContainerSingleton class is the actual singleton.
It uses a static constant sharedDataContainer to save a reference to the singleton.
To access the singleton, use the syntax
DataContainerSingleton.sharedDataContainer
The sample project defines 3 properties in the data container:
var someString: String?
var someOtherString: String?
var someInt: Int?
To load the someInt property from the data container, you'd use code like this:
let theInt = DataContainerSingleton.sharedDataContainer.someInt
To save a value to someInt, you'd use the syntax:
DataContainerSingleton.sharedDataContainer.someInt = 3
The DataContainerSingleton's init method adds an observer for the UIApplicationDidEnterBackgroundNotification. That code looks like this:
goToBackgroundObserver = NSNotificationCenter.defaultCenter().addObserverForName(
UIApplicationDidEnterBackgroundNotification,
object: nil,
queue: nil)
{
(note: NSNotification!) -> Void in
let defaults = NSUserDefaults.standardUserDefaults()
//-----------------------------------------------------------------------------
//This code saves the singleton's properties to NSUserDefaults.
//edit this code to save your custom properties
defaults.setObject( self.someString, forKey: DefaultsKeys.someString)
defaults.setObject( self.someOtherString, forKey: DefaultsKeys.someOtherString)
defaults.setObject( self.someInt, forKey: DefaultsKeys.someInt)
//-----------------------------------------------------------------------------
//Tell NSUserDefaults to save to disk now.
defaults.synchronize()
}
In the observer code it saves the data container's properties to NSUserDefaults. You can also use NSCoding, Core Data, or various other methods for saving state data.
The DataContainerSingleton's init method also tries to load saved values for it's properties.
That portion of the init method looks like this:
let defaults = NSUserDefaults.standardUserDefaults()
//-----------------------------------------------------------------------------
//This code reads the singleton's properties from NSUserDefaults.
//edit this code to load your custom properties
someString = defaults.objectForKey(DefaultsKeys.someString) as! String?
someOtherString = defaults.objectForKey(DefaultsKeys.someOtherString) as! String?
someInt = defaults.objectForKey(DefaultsKeys.someInt) as! Int?
//-----------------------------------------------------------------------------
The keys for loading and saving values into NSUserDefaults are stored as string constants that are part of a struct DefaultsKeys, defined like this:
struct DefaultsKeys
{
static let someString = "someString"
static let someOtherString = "someOtherString"
static let someInt = "someInt"
}
You reference one of these constants like this:
DefaultsKeys.someInt
Using the data container singleton:
This sample application makes trival use of the data container singleton.
There are two view controllers. The first is a custom subclass of UIViewController ViewController, and the second one is a custom subclass of UIViewController SecondVC.
Both view controllers have a text field on them, and both load a value from the data container singlelton's someInt property into the text field in their viewWillAppear method, and both save the current value from the text field back into the `someInt' of the data container.
The code to load the value into the text field is in the viewWillAppear: method:
override func viewWillAppear(animated: Bool)
{
//Load the value "someInt" from our shared ata container singleton
let value = DataContainerSingleton.sharedDataContainer.someInt ?? 0
//Install the value into the text field.
textField.text = "\(value)"
}
The code to save the user-edited value back to the data container is in the view controllers' textFieldShouldEndEditing methods:
func textFieldShouldEndEditing(textField: UITextField) -> Bool
{
//Save the changed value back to our data container singleton
DataContainerSingleton.sharedDataContainer.someInt = textField.text!.toInt()
return true
}
You should load values into your user interface in viewWillAppear rather than viewDidLoad so that your UI updates each time the view controller is displayed.
Another alternative is to use the notification center (NSNotificationCenter) and post notifications. That is a very loose coupling. The sender of a notification doesn't need to know or care who's listening. It just posts a notification and forgets about it.
Notifications are good for one-to-many message passing, since there can be an arbitrary number of observers listening for a given message.
Swift 4
There are so many approaches for data passing in swift. Here I am adding some of the best approaches of it.
1) Using StoryBoard Segue
Storyboard segues are very much useful for passing data in between Source and Destination View Controllers and vice versa also.
// If you want to pass data from ViewControllerB to ViewControllerA while user tap on back button of ViewControllerB.
#IBAction func unWindSeague (_ sender : UIStoryboardSegue) {
if sender.source is ViewControllerB {
if let _ = sender.source as? ViewControllerB {
self.textLabel.text = "Came from B = B->A , B exited"
}
}
}
// If you want to send data from ViewControllerA to ViewControllerB
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.destination is ViewControllerB {
if let vc = segue.destination as? ViewControllerB {
vc.dataStr = "Comming from A View Controller"
}
}
}
2) Using Delegate Methods
ViewControllerD
//Make the Delegate protocol in Child View Controller (Make the protocol in Class from You want to Send Data)
protocol SendDataFromDelegate {
func sendData(data : String)
}
import UIKit
class ViewControllerD: UIViewController {
#IBOutlet weak var textLabelD: UILabel!
var delegate : SendDataFromDelegate? //Create Delegate Variable for Registering it to pass the data
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
textLabelD.text = "Child View Controller"
}
#IBAction func btnDismissTapped (_ sender : UIButton) {
textLabelD.text = "Data Sent Successfully to View Controller C using Delegate Approach"
self.delegate?.sendData(data:textLabelD.text! )
_ = self.dismiss(animated: true, completion:nil)
}
}
ViewControllerC
import UIKit
class ViewControllerC: UIViewController , SendDataFromDelegate {
#IBOutlet weak var textLabelC: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func btnPushToViewControllerDTapped( _ sender : UIButton) {
if let vcD = self.storyboard?.instantiateViewController(withIdentifier: "ViewControllerD") as? ViewControllerD {
vcD.delegate = self // Registring Delegate (When View Conteoller D gets Dismiss It can call sendData method
// vcD.textLabelD.text = "This is Data Passing by Referenceing View Controller D Text Label." //Data Passing Between View Controllers using Data Passing
self.present(vcD, animated: true, completion: nil)
}
}
//This Method will called when when viewcontrollerD will dismiss. (You can also say it is a implementation of Protocol Method)
func sendData(data: String) {
self.textLabelC.text = data
}
}
Instead of creating a data controller singelton I would suggest to create a data controller instance and pass it around. To support dependency injection I would first create a DataController protocol:
protocol DataController {
var someInt : Int {get set}
var someString : String {get set}
}
Then I would create a SpecificDataController (or whatever name would currently be appropriate) class:
class SpecificDataController : DataController {
var someInt : Int = 5
var someString : String = "Hello data"
}
The ViewController class should then have a field to hold the dataController. Notice that the type of dataController is the protocol DataController. This way it's easy to switch out data controller implementations:
class ViewController : UIViewController {
var dataController : DataController?
...
}
In AppDelegate we can set the viewController's dataController:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if let viewController = self.window?.rootViewController as? ViewController {
viewController.dataController = SpecificDataController()
}
return true
}
When we move to a different viewController we can pass the dataController on in:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
...
}
Now when we wish to switch out the data controller for a different task we can do this in the AppDelegate and do not have to change any other code that uses the data controller.
This is of course overkill if we simply want to pass around a single value. In this case it's best to go with nhgrif's answer.
With this approach we can separate view form the logic part.
As #nhgrif pointed out in his excellent answer, there are lots of different ways that VCs (view controllers) and other objects can communicate with each other.
The data singleton I outlined in my first answer is really more about sharing and saving global state than about communicating directly.
nhrif's answer lets you send information directly from the source to the destination VC. As I mentioned in reply, it's also possible to send messages back from the destination to the source.
In fact, you can set up an active one-way or 2-way channel between different view controllers. If the view controllers are linked via a storyboard segue, the time to set up the links is in the prepareFor Segue method.
I have a sample project on Github that uses a parent view controller to host 2 different table views as children. The child view controllers are linked using embed segues, and the parent view controller wires up 2-way links with each view controller in the prepareForSegue method.
You can find that project on github (link). I wrote it in Objective-C, however, and haven't converted it to Swift, so if you're not comfortable in Objective-C it might be a little hard to follow
SWIFT 3:
If you have a storyboard with identified segues use:
func prepare(for segue: UIStoryboardSegue, sender: Any?)
Although if you do everything programmatically including navigation between different UIViewControllers then use the method:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)
Note: to use the second way you need to make your UINavigationController, you are pushing UIViewControllers on, a delegate and it needs to conform to the protocol UINavigationControllerDelegate:
class MyNavigationController: UINavigationController, UINavigationControllerDelegate {
override func viewDidLoad() {
self.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
// do what ever you need before going to the next UIViewController or back
//this method will be always called when you are pushing or popping the ViewController
}
}
It depends when you want to get data.
If you want to get data whenever you want, can use a singleton pattern. The pattern class is active during the app runtime. Here is an example of the singleton pattern.
class AppSession: NSObject {
static let shared = SessionManager()
var username = "Duncan"
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print(AppSession.shared.username)
}
}
If you want to get data after any action, can use NotificationCenter.
extension Notification.Name {
static let loggedOut = Notification.Name("loggedOut")
}
#IBAction func logoutAction(_ sender: Any) {
NotificationCenter.default.post(name: .loggedOut, object: nil)
}
NotificationCenter.default.addObserver(forName: .loggedOut, object: nil, queue: OperationQueue.main) { (notify) in
print("User logged out")
}
The way that I would do it would be instead of passing data between view controllers, I would just declare a variable globally. You can even do this with a function!
For example:
var a = "a"
func abc() {
print("abc")
}
class ViewController: UIViewController {
}