I'm building an app to keep track of scores i have a struct
*/
var teamA_name: String!
var teamB_name: String!
var teamA_points: [Int] = []
var teamB_points: [Int] = []
/*
- Add points to the teams
*/
mutating func addPoints(teamA: Int, teamB: Int){
self.teamA_points.append(teamA)
self.teamB_points.append(teamB)
}
as you can see i have two int arrays that will hold the user points. I have a controller with two tableviews to show the array of points added by the user, i'm going to skip some of the code since i know is not needed for my problem, this is my Main ViewController where tables will show the points
class GameScoreViewController: UIViewController {
/*
- Properties
*/
var gameScore = GameScore()
override func viewDidLoad() {
super.viewDidLoad()
//- Setup delegates & datasources
teamA_tableview.delegate = self
teamA_tableview.dataSource = self
teamB_tableview.delegate = self
teamB_tableview.dataSource = self
// - Button configuration
addPointsButton.layer.cornerRadius = 5
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "toPopUp"{
let popUpView = segue.destination as! PopUpViewController
// this is where i call my popup view
}
}
}
}
now here is where my problem occurs, when i segue to my pop up and the user enters the score needed and taps done, the data doesn't append to the array and my tableview won't reload, i've tried many different ways, using callbacks, delegates, i tried userdefaults since is not very important data but nothing seems to work, and i'm stuck, this is my pop up view controller button action where it should happen, i left the textfield.text in the parameter for reference
#IBAction func addBtnPressed(_ sender: Any) {
// this is the func to append data to the array
gameScore.addPoints(teamA: Int(pointsTextField.text!)!, teamB: 0)
self.dismiss(animated: true, completion: nil)
//after dismissed it should reload table view or insert row with the user entered score
}
any help will be appreciated, thank you.
It seems that you declare a separate instance inside the popup
gameScore.addPoints(teamA: Int(pointsTextField.text!)!, teamB: 0)
You need to set a delegate to the real object in segue
let popUpView = segue.destination as! PopUpViewController
popUpView.delegate = self
And declare it inside the popup like
var delegate:GameScoreViewController?
Then use it
delegate?.addPoints(teamA: Int(pointsTextField.text!)!, teamB: 0)
Related
I need to pass a String and Array from my Third ViewController to my First ViewController directly using protocol/delegate, I have no problem doing it from VC 2 to VC 1 but I'm having a hard time with this. Also after clicking a button in my VC3 I need to go back to VC 1 and update the VC UI how would I do that? Would that have to be in viewdidload?
This in Swift UIKit and Storyboard
You need two protocols, and your firstVC and SecondVC have to conform those. When pushing new ViewController you need to give the delegate of that ViewController to self. On your third VC, when you click the button you need to call your delegate and pass your data to that delegate method, then repeat the same for other.
For FirstVC
protocol FirstProtocol: AnyObject {
func firstFunction(data: String)
}
class FirstVC: UIViewController, FirstProtocol {
weak var delegate: FirstProtocol?
#IBAction func buttonClicked(_ sender: Any) {
let secondVC = SecondVC()
secondVC.delegate = self
navigationController?.pushViewController(secondVC, animated: true)
}
func firstFunction(data: String) {
navigationController?.popToRootViewController(animated: true)
print(data)
}
}
You handle your navigation from your root. For better experience you can use something like coordinator pattern to handle it.
protocol SecondProtocol: AnyObject {
func secondFunction(data: String)
}
class SecondVC: UIViewController, SecondProtocol {
weak var delegate: FirstProtocol?
#objc func buttonClicked() {
let thirdVC = ThirdVC()
thirdVC.delegate = self
navigationController?.pushViewController(thirdVC, animated: true)
}
func secondFunction(data: String) {
delegate?.firstFunction(data: data)
}
}
Second VC is something that you just need to pass parameters.
class ThirdVC: UIViewController {
weak var delegate: SecondProtocol?
#objc func buttonClicked() {
delegate?.secondFunction(data: "data") // PASS YOUR ARRAY AND STRING HERE
}
}
What you need is unwind segue. Unwind segue will act like segue, only backward, popping, in this case, VC2. You can read here for more information.
Updating data code would be put in a function similar to prepareToSegue() but for unwind segue in your VC1.
Example of the function inside VC1:
#IBAction func unwindToDestination(_ unwindSegue: UIStoryboardSegue) {
switch unwindSegue.identifier {
case SegueIdentifier.yourSegueIdentifier:
let sourceVC = unwindSegue.source as! SourceVC
dataToPass = sourceVC.dataToPass
reloadData()
default:
break
}
}
Here is a different approach that accomplishes what you described by performing a Present Modally segue directly from View Controller 3 to View Controller 1, and sharing the string and array values by way of override func prepare(for segue....
In Main.storyboard, I set up 3 View Controllers, and have segues from 1 to 2, 2 to 3, and 3 to 1. These are Action Segues directly from the buttons on each VC, which is why you won't see self.performSegue used inside any of the View Controller files. Here is the picture:
In the first view controller, variables are initialized (with nil values) that will hold a String and an Array (of type Int in the example, but it could be anything):
import UIKit
class FirstViewController: UIViewController {
#IBOutlet weak var updatableTextLabel: UILabel!
var string: String?
var array: [Int]?
override func viewDidLoad() {
super.viewDidLoad()
// These will only not be nil if we came here from the third view controller after pressing the "Update First VC" button there.
// The values of these variables are set within the third View Controller's .prepare(for segue ...) method.
// As the segue is performed directly from VC 3 to VC 1, the second view controller is not involved at all, and no unwinding of segues is necessary.
if string != nil {
updatableTextLabel.text = string
}
if let a = array {
updatableTextLabel.text? += "\n\n\(a)"
}
}
}
The second view controller doesn't do anything except separate the first and third view controllers, so I didn't include its code.
The third view controller assigns the new values of the string and array inside prepare (this won't be done unless you press the middle button first, to demonstrate both possible outcomes in VC 1). This is where your string and array get passed directly from 3 to 1 (skipping 2 entirely).
import UIKit
class ThirdViewController: UIViewController {
var theString = "abcdefg"
var theArray = [1, 2, 3]
var passValuesToFirstVC = false
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func updateFirstVC(_ sender: UIButton) {
passValuesToFirstVC = true
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if passValuesToFirstVC && segue.identifier == "toFirst" {
// Cast segue.destination (which on its own has type UIViewController, the superclass to all your custom View Controllers) to the specific subclass that your first View Controller belongs to
let destinationVC = segue.destination as! FirstViewController
// When your first view controller loads, it will receive these values for the 'string' and 'array' variables. They replace the original 'nil' values these had in the FirstViewController definition.
destinationVC.string = theString
destinationVC.array = theArray
}
}
}
Note that there is an IBOutlet to the label on the first View Controller which contains the text to be updated.
After visiting the third view controller, pressing the "Update First VC Text" button, and then performing the segue back to the first, here is how it will look:
This doesn't address the part about protocols and delegates in your question (as I'm not sure how they're being used in your program, and other answers have already addressed that), but it illustrates the method of transferring variables directly from one View Controller to another without unwinding segues or using the UINavigationController.
Currently, I have three view controllers in my navigation stack.
In the first VC, I use a picker view to pick a time and then I use a button to pass "pickedTime var" and segue to the second VC. In the second VC, I successfully use the "pickedTime var" to show the picked time on the screen.
I ran into an issue after I tried to pass the "pickedTime var" again to the third VC. Although, the third screen loads, "pickedTime var" shows 0 on the screen instead of the pickedTime.
First VC code:
// Holds pickedTime from UIPickerView
var pickedTime = Int()
// Segue to second screen
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showSecondScreen" {
let controller = segue.destination as! SecondViewController
controller.pickedTime = pickedTime
}
}
Second VC code:
// Holds pickedTime passed from first VC
var pickedTime: Int()
// Show pickedTime in label
override func viewDidLoad() {
super.viewDidLoad()
pickedTimeLabel.text = "\(pickedTime)" // shows 60
}
// Segue to pop-up screen or third screen
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segueToPopUp" {
let controller = segue.destination as! PopUpViewController
...
} else if let controller = segue.destination as? ThirdViewController {
controller.pickedTime = pickedTime
}
}
Third VC code
// Holds pickedTime passed from second VC
var pickedTime: Int()
// Show pickedTime in label
override func viewDidLoad() {
super.viewDidLoad()
pickedTimeLabel.text = "\(pickedTime)" // shows 0
}
You may not need to inject data to VC but instead share a Controller instance between and update instance. When the controller comes in the screen can checkout the values it needs and renders.
On your Solution:
// Holds pickedTime passed from second VC
var pickedTime: Int()
// Show pickedTime in label
override func viewDidLoad() {
super.viewDidLoad()
pickedTimeLabel.text = "\(pickedTime)" // shows 0
}
Instead of this, you can create a new Controller called Time.swift where you save the picked time and do not need to save any variable but pas entire Object as a dependency.
Also keep in mind that you are creating a chain of dependencies which most often fail to because 'var pickedTime: Int()' will show 0 on first place
I've set up a simple Swift project to try and wrap my head around delegates & protocols. The goal is to pass data between two classes (SendingClass & ReceivingClass). Two buttons in the SendingClass are linked to the delegate which should trigger the Protocol conforming function in the ReceivingClass to execute. This doesn't work unfortunately, I suspect it has to do with where and how I am declaring the ReceivingClass as the delegate.
Appreciate your insights, i'm just starting out!
I've tried setting the delegate in various locations (presently within viewDidLoad, but cant get it to work).
let vc = SendingClass()
vc.statusDelegate = self
SendingClass.swift
import UIKit
protocol StatusDelegate {
func statusChanged(state: Bool, sender: String)
}
class SendingClass: UIViewController {
var statusDelegate : StatusDelegate?
#IBAction func button1Pressed(_ sender: UIButton) {
statusDelegate?.statusChanged(state: true, sender: "Button 1")
}
#IBAction func button2Pressed(_ sender: UIButton) {
statusDelegate?.statusChanged(state: false, sender: "Button 2")
}
}
ReceivingClass.swift
import Foundation
import UIKit
class ReceivingClass: UIViewController, StatusDelegate {
override func viewDidLoad() {
let vc = SendingClass()
vc.statusDelegate = self
}
func statusChanged(state: Bool, sender: String) {
print("Sender = \(sender) , State = \(state)")
}
}
Expected: the ReceivingClass protocol conforming function (func statusChanged) should execute each time the buttons are pressed within the SendingClass.
Actual: Nothing happens
I am using this..
// create extension in your receiving class
extension ReceivingClass: PopUpVCDelegate {
func statusChanged(state: Bool, sender: String) {
print("Sender = \(sender) , State = \(state)")
}
}
// on sending class, when you present your receiving class on any button click
eg.
let resultController = self.storyboard?.instantiateViewController(withIdentifier: "PopUpVCID") as? PopUpVC
resultController?.delegate = self
self.present(resultController!, animated: true, completion: nil)
//or if not have button add on viewdidload in receiving class
// here is full eg
How to get data from popup view controller to custom table view cell?
For protocol and delegate, you use it when u want to bring a value from 2nd VC (presented by 1st or pushed by 1st VC) to 1st VC, which is the original.
From your code, I dont see you presenting or pushing your 2nd VC. that's why it's not working. Hopefully I answered your doubt.
However if you still want to bring a value over from 1st VC to 2nd VC. In second VC, create a variable to receive it
var ReceivedData = String()
then from your first VC, when u are going to push it,
let vc = SendingClass()
vc.ReceivedData = "Whatever you want it to receive"
If you're using storyboard segues, maybe the view controller is instantiated from there so probably you have to use the prepareForSegue and get the destination view controller (which is already instantiated for you) in the ReceivingClass view controller:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
if let destination = segue.destination as? SendingClass {
destination.delegate = self
}
}
Also be careful with delegate patter: the delegate property should be declared as a weak property to avoid retain-cycle
weak var delegate: MyDelegate?
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
I have one view that is contain some data is reading values from server and when the user click on one button I opened another view as popup view then the user make selection to something from this view then when is clicking on OK button the user must return directly to the previous opened view only with update the text on the clicked button with the selected choice.
I don't know if something like that is possible or not in swift 3, I made everything only I don't know what is the way I can use it to make update only on the text of button without make update for all view after read this value from another view!
Main View:
override func viewDidLoad() {
super.viewDidLoad()
//read saved username
let prefs:UserDefaults = UserDefaults.standard
Savedusername = prefs.object(forKey: "SavedUsername")as! String
self.getUserData()
//after this I read and display all date from server and it's OK
// Do any additional setup after loading the view.
}
I used layout directly to go from first view to another view when click on button.
This is the code of OK clicked button , here I want to read the value of choice variable that is take value in the second view and passing this value to the first view only to update text on the first clicked button
#IBAction func okPressedButton(_ sender: Any) {
//here how I can passing data without using this line that is make update for all previous view
//self.performSegue(withIdentifier: "first_view", sender: self)
self.dismiss(animated: true, completion: nil )
}
Update:
the second view:
protocol MyProtocol {
func updateData(data: String)
}
class CalenderPopUpViewController: UIViewController {
#IBOutlet weak var calenderPopUp: UIView!
#IBOutlet weak var datePickerView: UIDatePicker!
var delegate:MyProtocol?
var selectedDate:String = ""
override func viewDidLoad() {
super.viewDidLoad()
calenderPopUp.layer.cornerRadius = 10
calenderPopUp.layer.masksToBounds = true
// Do any additional setup after loading the view.
}
#IBAction func selectDatePicker(_ sender: Any) {
var dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
var strDate = dateFormatter.string(from: datePickerView.date)
self.selectedDate = strDate
print(selectedDate)
}
#IBAction func okPressedButton(_ sender: Any) {
self.delegate?.updateData(data: self.selectedDate)
self.dismiss(animated: true, completion: nil )
}
#IBAction func cancelPressedButton(_ sender: Any) {
self.dismiss(animated: true, completion: nil )
}
}
the First class :
class UserInfoViewController: UIViewController{
.
.
.
.
.
.
.
func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "read_date_from_calender" {
(segue.destination as! CalenderPopUpViewController).delegate = self
}
}
override func viewDidAppear(_ animated: Bool) {
if(receiveddata != "")
{
print(receiveddata) // I tried to print received data here but without any result
}
}
}
extension UserInfoViewController: MyProtocol {
func updateData(data: String) {
self.receiveddata = data
}
}
If I'm understanding your question correctly you need to use a protocol
Write something like this at the top of the class of your modal view/view controllers:
protocol MyProtocol {
func newDataSelected(data: String)
}
Then somewhere in your presented view class declare a variable like this
var delegate: MyProtocol?
Then when you make a selection call the delegate to pass the data back to your presenting view controller:
self.delegate?.newDataSelected(data: "someData")
When you are presenting the view controller be sure to set the delegate:
func present() {
let modal = ModalViewController()
modal.delegate = self
self.present(modal, animated: true, completion: nil)
}
Finally make sure that you inherit from the protocol in your presenting view controller
extension PresentingViewController: MyProtocol {
func newDataSelected(data: String) {
// Do some stuff
}
}
There are a few ways to do that.
Define your own delegate. Define your own protocol with a sub function onUserDataChanged. Override in your MainView and set as delegate to second view. In second view, you will call self.delegate. onUserDataChanged(data)
Use NotificationCenter to notify the info changes.
Use global variable or UserDefault. In second view's button tap handler, save the info as the global variable defined. In MainView's viewWillAppear, you will read the info and set to button.