I am building in iOS 9 with Swift 2.0. I have my starting UIViewController that is my menu screen. It contains the following code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let id = segue.identifier where id == "GamePlayScene" {
self.gameVC = segue.destinationViewController as? GameViewController
self.gameVC!.delegate = self
if let s = sender as? GKTurnBasedMatch {
self.gameVC!.match = s
}
}
}
When segueing to my GameViewController, the following init runs before that prepareForSegue even gets called:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
GKLocalPlayer.localPlayer().registerListener(self) // I only want this once
}
In storyboard, my GameViewController has a "Menu" button that is connected to the exit widget for the view controller and it unwinds to the Menu as intended. But whenever I perform the segue again, the init gets called again so I now have multiple GameViewControllers. I think this slows my app down since I am using SKScenes. How do I perform a segue without it creating a new object every time?
func player(player: GKPlayer, receivedTurnEventForMatch match: GKTurnBasedMatch, didBecomeActive: Bool) {
if didBecomeActive {
// This event is what activated the app, so the user wants it right meow
GameKitHelper.sharedInstance.match = match
performSegueWithIdentifier("GamePlayScene", sender: match)
}
}
You can store your GameViewController in a singleton so that it only needs to be created once. Since the view controller is initialized only once it saves processing time. This is probably a good performance optimization for an app using two view controllers, such as your game, that need to frequently be switched back and forth between the two.
The way to do this is to create a new Swift class and have it accessible as a shared instance where the second view controller is stored in a private property. The second view controller is instantiated if it has not yet been initialized. Further retrievals of the view controller return the stored view controller thereby eliminating the need to initialize the view controller. The operations for creating the view controller, storing it, and returning it are handled by the getter of a publically accessible computed property.
Here is the code for the class:
Singleton.swift:
import Foundation
import UIKit
class Singleton {
static let sharedInstance = Singleton()
var gameViewController: GameViewController {
get {
if self.storedViewController == nil {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
self.storedViewController =
storyboard.instantiateViewControllerWithIdentifier("GameViewController") as? GameViewController
}
return self.storedViewController!
}
}
private var storedViewController: GameViewController?
}
To use this, it will be necessary to use the showViewController method of showing the game view controller instead of using a segue.
In the first view controller I have:
#IBAction func buttonWasPressed(sender: AnyObject) {
let vc = Singleton.sharedInstance.gameViewController
navigationController?.showViewController(vc, sender: self)
}
The second controller (GameViewController) will now only be created once and re-used every time the button is pressed on the first view controller.
GKLocalPlayer.localPlayer().registerListener(self) // I only want this once
The way to cause a line of code to run only once over the lifetime of the app is with dispatch_once.
Related
I have a view controller, lets call it vc1, which passes some data to another (vc2) using prepare for segue, and then calling performSegue.
Is there a way to pass some data back from vc2 to vc1 when vc2 is dismissed by swiping down?
Thanks,
Edit --
Apologies for the lack of information, very new to swift so unsure of the correct question to ask in this situation.
To elaborate, the root of the issue at the moment is that vc2 is not dismissed programatically. ie there is currently no function called, it is simply dismissed by the user swiping down.
Is there some function that I can include to capture this dismissal, and use it to send data back to vc1?
I would prefer not to add any buttons to vc2 if possible.
Apologies again, and I appreciate all the help given already!
Try This
class VCOne: UIViewController {
//Create a shared instance of VCOne
static var sharedInstance:VCOne?
//Let the data to be passed back to VCOne is of type string
var dataToBePassedBack:String?
override func viewDidLoad() {
super.viewDidLoad()
//set the sharedInstance to self
VCOne.sharedInstance = self
}
}
Class VCTwo:UIViewController{
//function in which you are dismissing your current VC you can use the shared
instance to pass the data back
func dismissVC(){
//before dismissing the VCTwo you can set the value for VCOne
VCOne.sharedInstance?.dataToBePassedBack = "data"
}
}
Using Protocol And Delegate You Do or Other Option is NSotificationcenter.
One way yo do it is to create another file that it the controller of everything and then have a delegate that always notifies the view controllers when new changes are available. I will walk it through.
protocol HeadControllerDelegate {
// Create a function that sends out the data to the delegates when it is called
// You can use your custom struct here to pass more data easly
func didReciveNewData(myData: String?)
}
struct HeadController {
// Create a shared instance so that the viewcontroller that conforms to the view as well as when we sends out the data the delegate is correct
static var shared = HeadController()
// Creates the delegate, every view can asign it to
public var delegate: HeadControllerDelegate?
// Add all your values here you want to pass back
var myValue: String? {
// The didSet gets called every time this value is set, and then is it time to call the delegate method
didSet {
// Calls the delegates didReciveMethod to notify the delegates that new data exsists
delegate?.didReciveNewData(myData: myValue)
}
}
}
Now in your viewcontroller class where you would like the data to be avaiable (as you said when you swipe down)
class ViewController: UIViewController {
// Here you create a property of the shared instance
let headController = HeadController.shared
override func viewDidLoad() {
super.viewDidLoad()
// Set yourself as the delegate for the headController delegate to recive data
headController.delegate = self
}
}
extension ViewController: HeadControllerDelegate {
// here will the data be recived
func didReciveNewData(myData: String?) {
// handle the data here, you have now got newData
print(myData)
}
}
In the class where you want to pass data you just do it like this. The beauty of this is that you can have multiple classes or structs that writes to the head controllers data (just make sure you do it thought the shared instance). It is also a good pracice according to we to use the delegate pattern.
class Sender {
var headController = HeadController.shared
func sendData(data: String) {
// Here you change the data of the headcontroller wich will send the data to all the delegates
headController.myValue = data
}
}
Hope this answer helps. If you have any questions please let me know.
UPDATE -- EASIER SOLUTION
Here is an easier solution but is less scalable as the previous one according to me.
In prepareForSegue simply pass over your current viewContorller as a field in the destination view controller. Then when viewDidDissapear in the new view controller you can simply pass back the data. Not to worry, I will show you!
In prepare for Segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let dc = segue.destination as? SecondViewController {
dc.viewController = self
}
}
And declare the secondViewContorller as following. The ViewDidDisappear method will be called when the view has dismissed, and therefore can you pass over the data to the view controller you have set before using the prepare for segue method.
class SecondViewController: UIViewController {
var viewController: UIViewController?
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidDisappear(_ animated: Bool) {
(viewController as? ViewController)?.value = 2
}
}
Then you could update the UI using a didSet, which simply will be called when the property is set, which will be done in the view did disappear method.
var value: Int = 0 {
didSet {
print(value)
text?.text = "\(value)"
}
}
Hope this helps!
Sorry, the question is a bit long. Please bear with me.
Basically, I'm trying to write a simple count up/count down ios app using swift. I have three main view controllers. One is an "Initial View Controller" (which is the root view controller) that contain only two buttons - one that presents modally to the actual counting page (second view controller) and another present modally to a tableViewController page (third view controller). so those are the three view controllers.
So, if the user chooses to save the counter they have been counting I want to append the data on that counter view controller to an array I have created to be displayed on the tableViewController. So I'm making the tableViewController a delegate of the Counter View Controller to append the data to the array.
And as to my understanding, you need to implement prepareSegue in tableViewController to connect the tableViewController to the Counter View Controller. However, because the segue to the Counter View Controller doesn't originate from the tableViewController and instead from the Initial View Controller, the prepareSegue function is not working, and thus the delegate doesn't work. So to simplify my question- How would you save(append) data from one View Controller to another View Controller when you segue from a different view controller?
I hope my question was clear. I'm completely new to software development and not sure if I'm making any sense. Thanks so much for the help!
If you have three controllers in the storyboard path, One -> Two -> Three, and you want One to know about data changes in Three, then you need to propagate the changes via Two.
A rough version might look like this:
protocol CountDelegate: class {
func updateCount()
}
class One: UIViewController, CountDelegate {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destination = segue.destination as? Two else { return }
destination.delegate = self
}
func updateCount() {
// whatever
}
}
class Two: UIViewController {
weak var delegate: CountDelegate?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destination = segue.destination as? Three else { return }
destination.delegate = self.delegate // just pass the delegate down
}
}
class Three: UIViewController {
weak var delegate: CountDelegate?
func doWork() {
self.delegate?.updateCount()
}
}
So the delegate is One, and both Two and Three (via Two) point back to it as the delegate.
Solution 1 (Using Notifications):
Each time the counter is updated in CounterViewController, the following code needs to be executed:
let notification = Notification.init(name: NSNotification.Name(rawValue: "UpdateCounter"), object: NSNumber.init(value: 1), userInfo: nil)
NotificationCenter.default.post(notification)
This code is implemented in the Initial View Controller (to observe for the changes in counter):
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(parseNotification(_:)), name: NSNotification.Name.init(rawValue: "UpdateCounter"), object: nil)
}
func parseNotification(_ notification: Notification?) {
if let number = notification?.object as? NSNumber {
/** here counter is a class variable in Initial ViewController **/
counter = number.intValue
}
}
And add the following code in prepareforSegue in the Initial View Controller
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
//Condition to check if TableViewController is being opened
tableViewController.counter = counter
}
Don't forget to remove the observer in the Initial View Controller:
deinit {
NotificationCenter.default.removeObserver(self)
}
Solution 2 (Using Global/Shared variables):
Create a global/shared variable such as
class Shared {
var value = 0
let instance = Shared()
}
In the CounterViewController, you set the value every time the counter is updated
Shared.instance.value = counterValue
You can read the value in TableViewController using the following code:
Shared.instance.value
Trying to pass data from one view controller MainScreenVC to Another RatesVC with protocol and extension, but that's not working, app crashing everytime . I'm clearly see that problem with code on second VC(because print showing correct data after action on first VC) but not sure where is error.
StoryBoard and 1st VC Example
Second VC
1st View controller
import UIKit
protocol transferNameOfCurrency {
func currencySelected(nameOfCurrency: String)
}
class MainScreenVC: UIViewController {
var transferCurrencyDelegate: transferNameOfCurrency?
var nameOfTheCurrency: String?
#IBAction func updateRates(_ sender: Any) {
nameOfTheCurrency = "EUR"
transferCurrencyDelegate?.currencySelected(nameOfCurrency:
nameOfTheCurrency)
print(nameOfTheCurrency)
}
}
2nd ViewController
import UIKit
class RatesVC: UIViewController {
var currencySelected: String?
override func viewDidLoad() {
super.viewDidLoad()
if let push = self.storyboard?.instantiateViewController(withIdentifier: "MainScreenVC") as? MainScreenVC
{
push.transferCurrencyDelegate = self
}
// Do any additional setup after loading the view.
}
}
extension RatesVC: transferNameOfCurrency {
func currencySelected(nameOfCurrency: String) {
currencySelected = nameOfCurrency
print(currencySelected)
}
}
The most obvious problem lies here:
if let push = self.storyboard?.instantiateViewController(withIdentifier: "MainScreenVC") as? MainScreenVC {
push.transferCurrencyDelegate = self
}
You have to realize that instantiateViewController creates a new view controller - it's not the reference to the view controller presented at the screen. In that code you just created a completely new view controller and then set its delegate to self, but otherwise nothing else.
Without knowing the context it is really hard to suggest anything - prepare(for:) segue might be the place where you want to set the delegate. Anyway, the problem is that you have to obtain a reference to the controller that is presented on the screen, the one that is supposed to be reacting to those events.
Moreover, from the memory management aspect, you should really consider making the delegate property a weak one to prevent memory leaks.
EDIT
So after seeing the minimal working example you provided at link, I think I can provide the solution on how to get that string to the SecondVC.
Your first view controller with comments:
import UIKit
class ViewController: UIViewController {
var newLine: String = "EUR"
#IBAction func push(_ sender: Any) {
// here the secondVC does not exist yet, calling delegate.transferWord() here would have no sense
// performSegue will create that secondVC, but now it does not exist, nor it is set up as the delegate
self.performSegue(withIdentifier: "ViewController", sender: navigationController)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let secondVC = segue.destination as? SecondVC, segue.identifier == "ViewController" {
// at this moment secondVC did not load its view yet, trying to access it would cause crash
// because transferWord tries to set label.text directly, we need to make sure that label
// is already set (for experiment you can try comment out next line)
secondVC.loadViewIfNeeded()
// but here secondVC exist, so lets call transferWord on it
secondVC.transferWord(word: newLine)
}
}
}
No need for delegates here, because your ViewController is the one pushing the SecondVC to the Navigation controller - that means that you can access it directly in prepare(for:), as you can see above.
Now the SecondVC is super simple (I omitted unnecessary code):
import UIKit
class SecondVC: UIViewController {
#IBOutlet weak var label: UILabel!
func transferWord(word: String) {
label.text = word
}
}
Storyboards can stay as they are.
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 know this question has been asked countless times already, and I've seen many variations including
func performSegue(withIdentifier identifier: String,
sender: Any?)
and all these other variations mentioned here: How to call a View Controller programmatically
but how would you change a view controller outside of a ViewController class? For example, a user is currently on ViewController_A, when a bluetooth device has been disconnected (out of range, weak signal, etc) the didDisconnectPeripheral method of CBCentral gets triggered. In that same method, I want to change current view to ViewController_B, however this method doesn't occur in a ViewController class, so methods like performSegue won't work.
One suggestion I've implemented in my AppDelegate that seems to work (used to grab the appropriate storyboard file for the iphone screen size / I hate AutoLayout with so much passion)
var storyboard: UIStoryboard = self.grabStoryboard()
display storyboard
self.window!.rootViewController = storyboard.instantiateInitialViewController()
self.window!.makeKeyAndVisible()
And then I tried to do the same in my non-ViewController class
var window: UIWindow?
var storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) //assume this is the same storyboard pulled in `AppDelegate`
self.window!.rootViewController = storyboard.instantiateViewController(withIdentifier: "ViewController_B")
self.window!.makeKeyAndVisible()
However I get an exception thrown saying fatal error: unexpectedly found nil while unwrapping an Optional value presumably from the window!
Any suggestions on what I can do, and what the correct design pattern is?
Try this:
protocol BTDeviceDelegate {
func deviceDidDisconnect()
func deviceDidConnect()
}
class YourClassWhichIsNotAViewController {
weak var deviceDelegate: BTDeviceDelegate?
func yourMethod() {
deviceDelegate?.deviceDidDisconnect()
}
}
class ViewController_A {
var deviceManager: YourClassWhichIsNotAViewController?
override func viewDidLoad() {
deviceManager = YourClassWhichIsNotAViewController()
deviceManager.delegate = self
}
}
extension ViewController_A: BTDeviceDelegate {
func deviceDidDisconnect() {
DispatchQueue.main.async {
// change the VC however you want here :)
// updated answer with 2 examples.
// The DispatchQueue.main.async is used here because you always want to do UI related stuff on the main queue
// and I am fairly certain that yourMethod is going to get called from a background queue because it is handling
// the status of your BT device which is usually done in the background...
// There are numerous ways to change your current VC so the decision is up to your liking / use-case.
// 1. If you are using a storyboard - create a segue from VC_A to VC_B with an identifier and use it in your code like this
performSegue(withIdentifier: "YourSegueIdentifierWhichYouveSpecifiedInYourSeguesAttibutesInspector", sender: nil)
// 2. Instantiate your VC_B from a XIB file which you've created in your project. You could think of a XIB file as a
// mini-storyboard made for one controller only. The nibName argument is the file's name.
let viewControllerB = ViewControllerB(nibName: "VC_B", bundle: nil)
// This presents the VC_B modally
present(viewControllerB, animated: true, completion: nil)
}
}
func deviceDidConnect() {}
}
YourClassWhichIsNotAViewController is the class which handles the bluetooth device status. Initiate it inside the VC_A and respond to the delegate methods appropriately. This should be the design pattern you are looking for.
I prefer dvdblk's solution, but I wasn't sure how to implement DispatchQueue.main.async (I'm still pretty new at Swift). So this is my roundabout, inefficient solution:
In my didDisconnectPeripheral I have a singleton with a boolean attribute that would signify whenever there would be a disconnect.
In my viewdidload of my ViewController I would run a scheduledTimer function that would periodically check the state of the boolean attribute. Subsequently, in my viewWillDisappear I invalidated the timer.