How to prevent UIViewController from re-instantiating during segues? - swift

I have a UITableView inside of a UIViewController (lets call that view controller B) that I fill with objects one at a time. The objects are created in a previous UIViewController (view controller A), and added to the table's data source (which is also view controller B) in the prepare( for segue:_) function of view controller A. This adds that one object to the table, at which point the user can tap an "Add to Table" cell that brings them back to controller A, and the process is repeated. The problem is that each time I return to view controller B, the Table View only has the most recently added object. I believe this is because each segue is creating a new instance of the ViewController it is moving to. How can I prevent this, or find a better place to store the tableview data source that will not be re-instantiated and overwritten as an empty list each time?
View Controller A
class ViewControllerA : UIViewController {
var object = MyObject(...)
...
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let viewB = segue.destination as! ViewControllerB
viewB.dataSource.append(object) //
}
}
View Controller B
class ViewControllerB: UIViewController, UITableViewDataSource, UITableViewDelegate {
var dataSource = [MyObject]()
//DataSource functions fill table with dataSource array objects
...
}

The reason you lost your data is because that when view controller B is recreated every time when you trying to do the segue. So to keep the data, you must pass the data back from view controller B to view controller A. Then when you click on something else on view controller A, you need to append the new data to the existing one and then pass to controller B again.
To pass data back, you need to look at unwind segue. Here is a good example of the implementation.

You can pass the entire array back to ViewController A when you tap that "Add to Table" button. Then, append the new object to the entire list and assign it to the variable in ViewController B
A:
class ViewControllerA : UIViewController {
var dataSource = [MyObject]() // Now you have a dataSource variable in both viewControllers
var object = MyObject()
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let viewB = segue.destination as! ViewControllerB
dataSource.append(object) // Add the new object
viewB.dataSource = dataSource // Assign the newly-updated array
}
}
B:
class ViewControllerB: UIViewController, UITableViewDataSource, UITableViewDelegate {
var dataSource = [MyObject]()
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let viewA = segue.destination as! ViewControllerA
viewA.dataSource = dataSource
}
}

You are correct that this is happening because each time you segue back from ViewControllerB to ViewControllerA, ViewControllerB is deallocated from memory, along with its datasource. I would recommend passing a dataSource array from ViewControllerA when you instantiate ViewControllerB in prepareForSegue. Something like this...
class ViewControllerA : UIViewController {
var selectedItems: MyObject = []
var object = MyObject(...)
...
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let viewB = segue.destination as! ViewControllerB
viewB.dataSource = selectedItems
}
That way, ViewControllerA is keeping track of all selected items even after ViewControllerB is deallocated.

Related

How to update a label on the next screen?

Say I have two screens. Screen A has a picker and a button, which triggers a segue to screen B, which displays some content depending on the option selected by the picker.
How do I have the information as to what picker was selected in A, passed to B? So far, I have A doing:
#IBAction func pickThing(_ value: Int) {
self.thing = value;
}
Which seems to work; I believe that it is detecting the value and storing it. However, when I try adding #IBOutlet weak var thingLabel: WKInterfaceLabel! to match the label in B, I can only set the value of it when the app first loads.
If I put self.thingLabel.setText("test") in the awake() function, it sets the label to "test", so that works. But changing it to self.thingLabel.setText("thing \(self.thing)") doesn't work - it sets it to whatever self.thing is initialized as, but doesn't change it later. So awake() is not the right method to use. I've also tried putting it in willActivate and in pickThing, but neither of them did anything.
Is there some method that gets called when a screen is switched to? If not, how can I send data from one screen to the next?
For example on ViewController A use this function
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = segue.destination as? ViewControllerB {
vc.thing = self.thing
}
}
Or you can use closures in same methods and callback change from B ViewController on A
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = segue.destination as? ViewControllerB {
vc.clouser = { [weak self] thingB in
guard let `self` = self else { return }
self.thing = thingB
}
}
}
You need to make a variable in view controller B titled something like "parentVC," which would be of view controller A's class. In view controller A's class, you need to call prepare(for segue UIStoryboardSegue, sender: Any?). In this method, you can access the segue's destination property, which would be view controller B. From here you can set view controller B's "parentVC" property to "self," i.e. view controller A. Then in view controller B's class you can access properties of view controller A by using the "parentVC" variable. The code in view controller A's class would look something like this:
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.destination is ViewControllerB {
let viewControllerB = segue.destination as? ViewControllerB
viewControllerB.parentVC! = self
}
}

Difference Between 2 Ways of Declaring a ViewController Object

So I experimented with these 2 different ways of declaring a ViewController variable and it seemed to offer me the same results. However, I do feel there must be a difference between setting the destinationVC variable because if not, won't people use the more straightforward way of just declaring a new object?
[using segue.destination as! ViewControllerName]
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "changeCityName" {
let destinationVC = segue.destination as! ChangeCityViewController
destinationVC.delegate = self
}
}
[using ViewControllerName()]
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "changeCityName" {
let destinationVC = ChangeCityViewController()
destinationVC.delegate = self
}
}
In the prepareForSegue method, these two methods of creating a new VC differs greatly.
If you use segue.destination, you refer to the specific VC that the segue is going to, i.e. the one in your storyboard that the segue is connected to. If you create a new VC, then the VC you created won't be the same one as the segue is going to. i.e. you are dealing with a separate VC. Setting the delegate of the newly created VC won't do anything to the VC that is actually being presented.
If you are talking about the difference between using a segue to present a VC and this:
let vc = SomeViewController()
self.present(vc, animated: true)
Then the difference is less. If you use segues, then the views in the view controller will be read from the storyboard (NIB) file. If you create the VC by calling the initializer, you will have to handle adding the views in your view controller class.
Result may be visually same but its not true.
If you don't put any code inside prepare(for segue) still you will get same result(visually)
prepare(for segue) is called when UIViewControllers are connected through storyboard.
Since UIViewControllers are already connected in storyboard, so the destination UIViewController is called on your desired event.
In your first case using (segue.destination as! ViewControllerName) which is correct way of using segue.
Before going further one more thing is to be discussed about and that is
Why we are required to write code inside prepare(for segue) if its already connect through storyboard
1.From one button action you can connect several segues depending on your requirements, but each time button is pressed same prepare(for segue) method will be called, so to differentiate which UIViewController is to be called we do something like this
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if(segue.identifier == "FirstViewControllerIdentifier")
{
}else if(segue.identifier == "SecondViewControllerIdentifier"){
}else if(segue.identifier == "ThirdViewControllerIdentifier"){
}else{
// and so no
}
}
Now here we get object of destination controller(UIViewController) already being prepared.So we are not required to make a new object of destination controller
2.We can pass data to destination controller and also we can set delegate
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if(segue.identifier == "FirstViewControllerIdentifier")
{
// here we get object of first view controller and set delegate
let firstVC = segue.destination as! FirstViewController
firstVC.delegate = self
}else if(segue.identifier == "SecondViewControllerIdentifier"){
// here we get object of second view controller and pass some data to it
let secondVC = segue.destination as! SecondViewController
secondVC.someData = someData
}else if(segue.identifier == "ThirdViewControllerIdentifier"){
}else{
// and so no
}
}
Now in your second case using ViewControllerName() (the wrong code)
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if(segue.identifier == "FirstViewControllerIdentifier")
{
// here we create object of first view controller.This object is different from the destination view controller
//you create an object and set a delegate but after that you are not using that object and that object is totallu useless.
let firstVC = FirstViewController()
firstVC.delegate = self
// above code does not affect any thing but the contoller which is to be presented is destination view controller which is connected through storyboard
}
}
Hope you understand how to use segue and let me know if there is any problem

how to setup the delegate for UIPageViewController inside Container view that is embeded in a ViewController

I have a MainViewController, that has a container view inside it that embeds a UIPageViewController. MainViewController conforms the protocols UIPageViewControllerDelegate, UIPageViewControllerDataSource
and I want the embedded UIPageViewController to be delegated by the MainViewController.
Is that even possible?
Basically I am trying to have a similar attitude to having a collection view inside a view controller as an outlet from storyboard, and then to setup the delegates for it (collectionView.delegate = self, assuming self delegates it)
So this attitude with a UIPageViewController.
The obstacle I encounter is that there is no PageView as opposode to PageViewController (comparing to collection views, I have CollectionView as opposed to CollectionViewController.
Here are the steps you could use to produce this:
You can get a reference to your UIPageViewController by overriding func prepare(for segue: UIStoryboardSegue, sender: Any?) in MainViewController. This function is triggered when your embedded controller is made a child.
Given you gave your segue a name (let's use pageSegue for this example), you are able to do something like this in MainViewController:
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "pageSegue", let controller = segue.destination as? UIPageViewController {
controller.delegate = self
controller.dataSource = self
}
}
You can add a reference to your page view controller to your main view controller in the following way:
var walkthroughPageViewController: WalkthroughPageVC? {
return self.children.first as? WalkthroughPageVC
}
from here, you can just set walkthroughPageViewController.delegate = self
This method only works of course if you have one embedded view controller.
If you have more than one, Thomas' method above is better for grabbing the specific view controller you want.

Swift: How do you pass information to a view controller underneath the current one?

I have two view controllers, HomeViewController, and SecondViewController, which is pushed on top of HomeViewController at the top of the stack. I have a segue from HomeVC to SecondVC by means of 'Show'. I understand how to pass information from SecondVC to HomeVC - using the prepare for segue method using identifiers. But I do not have a segue from SecondVC to HomeVC, as it is embedded in a navigation controller. How would I pass information, for example a string, from SecondVC back to HomeVC upon popping the SecondVC from the navigation stack?
You need to pass a ViewModel from the HomeViewController to the SecondViewController, what you do is give your Segue an Identifier and then override prepareForSegue in your HomeViewController:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
if segue.identifier == "mySegueIdentifier" {
if let destination = segue.destinationViewController as? SecondViewController {
destination.aViewModel = aViewModel;
}
}
}
In your SecondViewController you need to declare the ViewModel:
class SecondViewController: UIViewController {
var aViewModel: AViewModel?
//rest of code
}
And create a ViewModel, use a struct type:
struct AViewModel
{
let AValueIWantToPassAround: String
let AnotherValueIWantToPassAround: Int
}

Show Label Value on 2nd View - Swift

Since i'm new and a newbie on Swift can someone tell me how I can get the label value on screen 1 to show on screen 2. Thanks
Edit: So I tried the way you told me but for some reason the Label text did not change in View 2. Any help? Thanks
I am passing textFiled data to destViewController to show how segue performs when passing data.
Note: If you want to pass string data to your destVC.You can assign your string like var someValue: String
mainStoryBoard:
MainVC:
#IBOutlet weak var textField: UITextField!
//or you can assign string like var someValue: : String
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let viewController = segue.destination as! destVC
viewController.dataText = textField.text // someValue.text
}
DestVC:
#IBOutlet var label: UILabel!
var dataText = String()
override func viewDidLoad()
label.text = dataText
}
Output:
You already have a segue between ViewController1 and ViewController2 it seems, so now you just need to pass some date from one to another. This can be done in the function prepare(for segue: UIStoryboardSegue, sender: AnyObject?) which is called when you transition from ViewController1 to ViewController2.
The UIStoryboardSegue class has a property called source of type UIViewController and another property called destination which is also a UIViewController.
So, if you define a property on your ViewController2, like so:
var labelValue: String
Then you can pass a value to it in your prepareForSegue defined on ViewController1 like so:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let viewController2 = segue.destination as? YourViewController2 else {
return
}
viewController2.labelValue = theValueFromViewController1
}
Here is tutorial telling you a bit more (in Objective C though)
And this is also a fine introduction.
Hope that helps you.
I'll consider you have ViewController class VC1 (screen 1) & ViewController class VC2 (screen 2). I see from the screenshot that, you're already using a segue to go from the VC1 to VC2 view.
Now, declare a variable in your VC2 class let's call it labelValue,
class VC2 {
var labelValue: String?
...
}
Whenever you use a storyboard segue to move from one viewcontroller's view to other viewcontroller's view a method named,
func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
will get called if it's overriden in the source viewController class (scene1 in your case). In this method you'd be able to access the destination view controller (scene 2) and it's properties. So for your case you can implement it like this,
class VC1 {
//I have assumed the below label will hold the value which you want to pass it to the scene 2.
var lblResult: UILabel!
......
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// You can access the VC2 instance from the segue
if let vc2 = segue.destinationViewController as? VC2 {
vc2.labelValue = lblResult.text
}
}
Now once you implement the prepareForSegue: method correctly as shown above, you'd be able to get the label's value from scene 1 to labelValue property of scene2.
Do learn more about segues here : More About Segues and Storyboards here.
================================
Edit:
Coming to your edited question, in your View2 viewController, you've declared labelNumber1 as String and not as UILabel, so whenever you pass the value to labelNumber1, the value will be containing in this variable and it will not be shown in the screen since it is just a normal string. So what you can do is, declare the labelNumber1 as UILabel
class View2: UIViewController {
#IBOutlet var labelNumber1: UILabel!
.....
}
and in your prepare for segue in ViewController1 make the following change,
class ViewController1: UIViewController {
.....
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// You can access the VC2 instance from the segue
if let view2 = segue.destinationViewController as? VC2 {
view2.labelNumber1.text = lblResult.text
}
}
However if you still want to keep labelNumber1 as String, and to verify whether the value has been passed from ViewController1 during segue, add below line to viewDidLoad() of View2 ViewController.
class View2: UIViewController {
.....
override func viewDidLoad() {
super.viewDidLoad()
print("labelNumber1: \(labelNumber1)")
}
}
And you can see the printed value of labelNumber1 in the console.
HTH :)