Use closure to pass data between two controllers - swift

my problem looks so simple, but since I am a beginner, I have problem to understand the concept of the closure to pass data between two controllers
for example I have a static table view controllers that has one cell and a title inside it
class FirstView: UITableViewController {
#IBOutlet weak var titleLabel: UILabel!
and I have an another view controller that contain a button inside it
class SecondViewController: UIViewController {
#IBAction func pressChangeButton(_ sender: UIButton) {
}
and there is segue1 between these two controllers, with identifier "segue1"
I want a to a simple task, I want to add a boolean closure that it will be true if the change button is pressed.
that is why I create a closure function the second view controller that has change button.
var change : ((Bool) -> Void)?
I just want, that the second view controllers tells the first one that change closure is now true (after pressing the change button) and the first view controllers simply change the title table inside it to whatever (I just want to see that how it can be done)
I don't know I have to use prepare sugue function?
Could anyone help me to understand this concept?

You can try
class FirstView: UITableViewController {
#IBOutlet weak var titleLabel: UILabel!
#IBAction func goToSecond(_ sender: UIButton) {
self.performSegue(withIdentifier: "segue1", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segue1" {
let des = segue.destination as! SecondViewController
des.change = { [weak self] (value) in
print(value)
self?.titleLabel.text = "SetValue"// set a value
}
}
}
}
class SecondViewController: UIViewController {
var change : ((Bool) -> Void)?
#IBAction func pressChangeButton(_ sender: UIButton) {
change?(true)
}
}

A closure is basically a piece of code that you can run. In Swift a closure is a first class citizen as it can be passed around as parameters and return type of functions. That being said, you can pass or set a closure as you normally would for other objects.
As per Sh_Kan's answer, just set SecondViewController's closure in prepare(for segue:sender:), always paying extra attention to retain cycles. You might also want to take a look at delegate design pattern in order to exchange data and messages between your controllers.

Related

Why doesn't performSegue() call shouldPerformSegue()

Quite new to programming in Swift and mobile development in general. I am trying to use performSegue() and control it without if-else statements. I made a google search how to use override func shouldPerformSegue() and tried to implement it in different ways but none of them solved my situation. Here I need to segue to Yellow or Green views if the switch is on using corresponding buttons. Even if I made return false, the function does not cancel the segue to happen. What is the reason of this behaviour and how can I fix it? Many thanks.
class ViewController: UIViewController {
#IBOutlet var segueSwitch: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func yellowButtonTapped(_ button: UIButton) {
performSegue(withIdentifier: "Yellow", sender: nil)
}
#IBAction func greenButtonTapped(_ button: UIButton) {
performSegue(withIdentifier: "Green", sender: nil)
}
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
// return segueSwitch.isOn
return false
}
}
shouldPerformSegue does not get called if you use performSegue(withIdentifier: in code. This function only get called if the segue is triggered from storyboard.
You can easily test this by adding 2 Buttons to a ViewController in storyboard one with the storyboard segue and one with an IBOutlet to a new ViewController. Then add a print statement to shouldPerformSegue. In the Button outlet call performSegue. Only the first button performing the segue from storyboard will print to the console.
And additionally you don´t need it. Any validation you would perform in shouldPerformSegue can be done in the ...ButtonTapped function:
#IBAction func yellowButtonTapped(_ button: UIButton) {
if validateSegue(){
performSegue(withIdentifier: "Yellow", sender: nil)
}
}
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
validateSegue()
}
func validateSegue() -> Bool{
......
}
This is deliberate and makes perfect sense. shouldPerformSegue lets you prevent an automatically triggered segue in case you don't want it performed. But if you didn't want a manual segue performed, all you had to do is nothing — don't say performSegue in the first place.
and control it without if-else statements
If-else is exactly how to control it.

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 :)

unwind segue not triggering

I have been learning swift and have made the foundation of most of my app. I have the following storyboard
app storyboard
Everything works fine. For example, I have an unwind segue on the add course view controller that triggers when you press save and you are returned to the 'your courses' view controller.
When you are on the my courses view controller, you can select a course and the topics are displayed, you can then select a topic and you are taken to an update score view controller, this all works fine.
However, my problem is this. I want to make it so that when you select save in the updatescore view controller, an unwind segue is triggered (the same as in the add course) and you are returned to the list of topics in the topics view controller.
However, I have followed many tutorials and obviously got it working before. (My action method for the unwind segue is in the correct topics view controller) but when i press save, the unwind segue is not returning me to the topics view controller.
Could anyone suggest a reason for this? I have spent a lot of time trying to find an answer and gone through many tutorials but have not managed to solve it.
I have also included a screen shot of the connections of the triggered segues for my save button to show that it is set up. Showing triggered segue for save button
i have the following code in the update score view controller
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if saveButton === sender {
print("save button selected")
}
}
But even this is not getting triggered when I click on save.
Many thanks
UPDATE:
After following Ronatorys advice My view controller for the update score is as follows but it is still not working:
import UIKit
class UpdateScoreTableViewController: UITableViewController {
#IBOutlet weak var topicGettingUpdated: UITextField!
#IBOutlet weak var newScore: UITextField!
#IBOutlet weak var saveButton: UIBarButtonItem!
var index:Int?
var Topics:[String]!
var TopicToUpdate:String?
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
guard let uiBarButtonItem = sender as? UIBarButtonItem else {
print("There is no UIBarButtonItem sender")
return
}
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.section == 0 && indexPath.row == 0 {
newScore.becomeFirstResponder()
}
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
}
But the prepare for segue is not even getting triggered.
Like Craig in the comments said, it's not that easy to find the problem. So I just build a simple app where you can follow the steps as guide and see if you forgot something to setup the functionality right. Hope it will help you. Note: Code is in Swift 3.0, but should be easy to adopt to 2.*
1. Storyboard with two View Controllers:
2. Declare the action method for the unwind segue in the FirstViewController.swift:
class FirstViewController: UIViewController {
// action method for the unwind segue
#IBAction func updateScore(_ segue: UIStoryboardSegue) {
print("Back in the FirstViewController")
}
}
3. Connect the Save button in the Storyboard with the action method (with ctrl + drag):
4. Connect your Save button with the SecondViewController.swift file, to use it for checking in your prepareSegue method (with ctrl + drag):
5. Add the prepare(for:sender:) method to your SecondViewController.swift:
class SecondViewController: UIViewController {
#IBOutlet weak var saveButtonPressed: UIBarButtonItem!
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// check safely with guard that your save button is the sender and you can use it
// if not print message
guard let uiBarButtonItem = sender as? UIBarButtonItem else {
print("There is no UIBarButtonItem sender")
return
}
// check if you selected the save button
if saveButtonPressed == uiBarButtonItem {
print("save button selected")
}
}
}
Result:
The sample app you can find here
I did not manage to get the unwind segue to work but instead used
navigationController!.popViewControllerAnimated(true)
as a work around and this works fine.

Need to free memory after Modal Segues, but I need both my Segues to pass data from A to B and B to A

I am making a game for iOS with SpriteKit.
I have 2 Viewcontrollers. One is the GameViewController and the other one is the MenuViewController. Let's call them A and B respectively.
When the player dies, a function is called in GameScene.swift that launches a modal "Lost" Segue to B. There, the player can restart the game or buy a life and a "Back" Segue is called to A.
I need to dismiss the additional Views that get created each time I call a segue.
Problem is: I need the "Lost" Segue to send data about the Score to View B and I need the "Back" Segue to send data to View A about wether or not the player used a life.
I have implemented all this. But now I need to find how to dismiss old views that keep eating the device's memory, thus leading to lag and crash.
I have googled for hours and hours. No solution was adapted to my situation.
The solutions I found either caused my app to bug, data not to be passed or views not to be generated.
I will not add code here since there is a LOT. But I am sure the answer is actually really easy, just not for a beginner like me.
I think a possible solution would be an unwind segue from B to A ?
But do unwind segues pass data along ?
Moreover, I found no answer I could understand on how to use an unwind segue.
I exhausted all my possibilities. Stack Exchange is my last chance.
You definitely should use an unwind segue to return to the previous viewController, otherwise as you have found your memory usage increases until your apps quits.
I created the following example from your description. It uses a standard segue to move from the GameViewController to the MenuViewController and it uses an unwind segue to move from the MenuViewController back to the GameViewController.
The GameViewController has a Player Dies UIButton, a UITextField for entering a score, and a UILabel for displaying the lives.
The MenuViewController has a UILabel for showing the score, a Buy a Life UIButton for adding lives, and a Restart UIButton for returning to the GameViewController.
Here's the code:
GameViewController.swift
import UIKit
class GameViewController: UIViewController {
#IBOutlet weak var scoreTextField: UITextField!
#IBOutlet weak var livesLabel: UILabel!
var lives = 3
func updateLivesLabel() {
livesLabel.text = "Lives: \(lives)"
}
override func viewDidLoad() {
super.viewDidLoad()
updateLivesLabel()
}
// This is the function that the unwind segue returns to.
// You can call it anything you want, but it has to be in
// the viewController you are returning to, it must be tagged
// with #IBAction and it must take a UIStoryboardSegue as its
// only parameter.
#IBAction func returnFromMenu(segue: UIStoryboardSegue) {
print("We're back in GameViewController")
// Update the lives label based upon the value passed in
// prepareForSegue from the MenuViewController.
updateLivesLabel()
}
#IBAction func goPlayerDies(sender: UIButton) {
lives--
self.performSegueWithIdentifier("Lost", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "Lost" {
let destinationVC = segue.destinationViewController as! MenuViewController
destinationVC.score = Int(scoreTextField.text ?? "") ?? 0
destinationVC.lives = lives
}
}
}
MenuViewController.swift
import UIKit
class MenuViewController: UIViewController {
#IBOutlet weak var scoreLabel: UILabel!
var score = 0
var lives = 0
override func viewDidLoad() {
super.viewDidLoad()
scoreLabel.text = "Score: \(score)"
}
#IBAction func buyLife(sender: UIButton) {
lives++
}
#IBAction func goRestart(sender: UIButton) {
self.performSegueWithIdentifier("Back", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "Back" {
let destinationVC = segue.destinationViewController as! GameViewController
destinationVC.lives = lives
}
}
}
This is how you wire up the forward segue to be called programmatically:
Control-drag from ViewController icon to the MenuViewController:
Select Present Modally from the pop-up:
Click on the segue arrow between the viewControllers and give it an identifier in the Attributes Inspector:
This is how you wire up the unwind segue to be called programmatically:
Control-drag from ViewController icon to Exit icon:
Choose returnFromMenu from pop-up:
Click on the Unwind Segue in the Document Outline and give it the identifier "Back" in the Attributes Inspector on the right:
Alternate Answer
Instead of using segues, you can present and dismiss viewControllers manually. The advantage for your app is that the MenuViewController will be allocated only once and will persist for the life of the app. This same viewController will be presented and dismissed repeatedly, but it will not be deallocated which I suspect is leading to your crashes.
The GameViewController will be the initialViewController that is created by the Storyboard. The MenuViewController will be loaded in viewDidLoad of the GameViewController.
To make this work, you need to add an identifier to the MenuViewController so that it can be instantiated by name. Click on the MenuViewController in the Storyboard and set its Storyboard ID in the Identity Inspector:
Here is the code. Note that all mention of segues is gone. Note how viewWillAppear is used to update the viewControllers.
GameViewController.swift
import UIKit
class GameViewController: UIViewController {
#IBOutlet weak var scoreTextField: UITextField!
#IBOutlet weak var livesLabel: UILabel!
var menuViewController: MenuViewController?
var lives = 3
func updateLivesLabel() {
livesLabel.text = "Lives: \(lives)"
}
override func viewDidLoad() {
super.viewDidLoad()
menuViewController = self.storyboard!.instantiateViewControllerWithIdentifier("MenuViewController") as? MenuViewController
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
updateLivesLabel()
}
#IBAction func goPlayerDies(sender: UIButton) {
lives--
menuViewController?.score = Int(scoreTextField.text ?? "") ?? 0
menuViewController?.lives = lives
self.presentViewController(menuViewController!, animated: true, completion: nil)
}
}
MenuViewController.swift
import UIKit
class MenuViewController: UIViewController {
#IBOutlet weak var scoreLabel: UILabel!
var score = 0
var lives = 0
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
scoreLabel.text = "Score: \(score)"
}
#IBAction func buyLife(sender: UIButton) {
lives++
}
#IBAction func goRestart(sender: UIButton) {
let destinationVC = self.presentingViewController as! GameViewController
destinationVC.lives = lives
self.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
}
}

SWIFT: No idea how to get back the selected value from a popover to the calling controller

I just going crazy on Swift Popover “return” values. I am new to Objectiv-C as well as SWIFT but I try to focus on SWIFT.
I checked out tutorials around Google and StackOverflow about how to manage iOS popovers, learned a lot but the last peace I couldn’t make it. It is great so see how easy it is made using Swift and Xcode 6, love it, but I could not figure out how to get back the selected value from my popover to my calling view controller.
So here is my problem:
(SIDENOTE: I am using SWIFT and do all using storyboard)
I have created a master ViewController with a button to select currencies. This button opens a “select currency” popover (linked to the CurrencyTableViewController (CTV) by CTRL-Dragging it to the CTV-Controller.
So far so good. The thing is, I have no idea how to get back the selected table row (currency) from the CTV-Table ;-( So I need the selected currency (table row) in the calling ViewController.
This is an excerpt from my ViewController (which is calling the popover)
class ViewController: UIViewController, UIPopoverPresentationControllerDelegate
[...]
// This button is calling the popover
#IBAction func buttonCurrency(sender: AnyObject) {
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let controller = segue.destinationViewController as? CurrencyTableViewController {
controller.popoverPresentationController?.delegate = self
return
}
}
[...]
Hopefully somebody can help me with that missing last mile how to get back the selected row value back to my ViewController.
Thanks in advance
Cheers
John
I made quick example, hope it helps:
// This is you popover's class
#objc protocol CurrencySelectedDelegate {
func currencySelected(currName: String)
}
class MyPopOverController: UIViewController {
weak var delegate: CurrencySelectedDelegate?
#IBAction func readyButtonPressed(sender: AnyObject) {
// Do what you want
delegate?.currencySelected("Euro/Dollar etc....")
// close popover
}
}
// ViewController
class ViewController: UIViewController, CurrencySelectedDelegate {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "mySegue" { // your identifier here
let controller = segue.destinationViewController as! MyPopOverController
controller.delegate = self
}
}
}
And remember just declare that currencySelected function in your ViewController.