Search code examples
swiftuilabelparameter-passingviewcontrolleribaction

SWIFT - How to change a label from a calculated function output on another ViewController?


Swift 5:

I am trying to pass the result of a function, called in an embedded View, to the parent ViewController, so as to change a label at the bottom of the parent ViewController.

Other than descriptive text, my primary ViewController contains four elements.

  1. 3-component pickerView
  2. Embedded View that contains 3 separate 1-element pickerViews
  3. 1st Label that contains the 1st of 2 numbers calculated and output from a function
  4. 2nd Label that contains the 2nd of 2 numbers calculated and output from a function

In other words, there are 6 picker components visible on the screen (1 picker with 3 components and 3 1-component pickers). But the 3 single pickers are in an embedded view.

At this time, when I release the picker of any of the 3 components in the 3-component picker, the function is called, it correctly makes a set of calculations, based on the picker delegate selection and the 2 results of that function changes the two labels at the bottom of the screen accordingly. (i.e. 1 function passes 2 results.)

What I'm trying to do is trigger the same function calls, when any of the pickers in the embedded view are released and pass those two results to the appropriate two labels in the primary ViewController.

This is the code in the primary View Controller titled "SecondViewController" that works (I'm leaving out the data arrays, to save space):

    @IBOutlet weak var spendingPickerView: UIPickerView!
    @IBOutlet weak var taxRateLbl: UILabel!
    @IBOutlet weak var taxPaidLbl: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        spendingPickerView.delegate = self
        spendingPickerView.dataSource = self
        spendingPickerView.selectRow(5, inComponent: 2, animated: true)

    }
}

extension SecondViewController: UIPickerViewDataSource {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 3
        }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return 10
    }

    /*func pickerView(_ spendingPickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
        return NSAttributedString(string: digits[row], attributes: [NSAttributedString.Key.foregroundColor : UIColor.white])
    }*/
}

extension SecondViewController: UIPickerViewDelegate {

    /*func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        if pickerView.tag == 2{
        return adults[row]
        }
        if pickerView.tag == 3{
        return minors[row]
        }
        if pickerView.tag == 4{
        return region[row]
        }
        return ""
     }*/

    func pickerView(_ spendingPickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
        // let view = UIView(frame: CGRect(x: 0, y: 0, width: 82, height: 28))

        let digitsLbl = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 107))
        digitsLbl.text = digits[row]
        digitsLbl.textColor = .white
        digitsLbl.font = UIFont.boldSystemFont(ofSize: 24)

        return digitsLbl

    }

    func pickerView(_ spendingPickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        spendingPickerView.reloadComponent(1)

        // Calculation functions for output.
        taxPaidLbl.text = "$\(rateAndTaxCalc(retailSpend: retailDollar, adultIH: adultInHome, minorIH: minorInHhome, regionOfUSA: regionOfUSA).1)"

        taxRateLbl.text = "\(rateAndTaxCalc(retailSpend: retailDollar, adultIH: adultInHome, minorIH: minorInHhome, regionOfUSA: regionOfUSA).2)%"

    }

This is the code from the embedded View titled SecondPickerViewController from which I'm trying to pass data to "SecondViewController":

    @IBOutlet weak var adultPickerView: UIPickerView!
    @IBOutlet weak var minorPickerView: UIPickerView!
    @IBOutlet weak var regionPickerView: UIPickerView!


    override func viewDidLoad() {
        super.viewDidLoad()

        adultPickerView.delegate = self
        adultPickerView.dataSource = self
        minorPickerView.delegate = self
        minorPickerView.dataSource = self
        regionPickerView.delegate = self
        regionPickerView.dataSource = self

    }
}

extension SecondPickerViewController: UIPickerViewDataSource {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
            return 1
        }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {

        var wheel = 0
        if pickerView.tag == 2 {
            wheel = 4 }
        else if pickerView.tag == 3 {
            wheel = 11 }
        else if pickerView.tag == 4 {
            wheel = 3 }
        return wheel
    }
}

extension SecondPickerViewController: UIPickerViewDelegate {
    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {

        let returnLbl = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 107))

        if pickerView.tag == 2 {
            returnLbl.text = adults[row]
            returnLbl.textColor = .white
            returnLbl.textAlignment = .center
            returnLbl.font = UIFont.boldSystemFont(ofSize: 24)
        }
        else if pickerView.tag == 3 {
            returnLbl.text = minors[row]
            returnLbl.textColor = .white
            returnLbl.textAlignment = .center

            returnLbl.font = UIFont.boldSystemFont(ofSize: 24)
        }
            else if pickerView.tag == 4 {
            returnLbl.text = region[row]
            returnLbl.textColor = .white
            returnLbl.textAlignment = .center

            returnLbl.font = UIFont.boldSystemFont(ofSize: 24)
        }

        // This output needs to be sent to "SecondViewController". /////////////////
        // This output needs to be sent to "SecondViewController". /////////////////
        // This output needs to be sent to "SecondViewController". /////////////////
        taxPaidTmp = "$\(rateAndTaxCalc(retailSpend: retailDollar, adultIH: adultInHome, minorIH: minorInHhome, regionOfUSA: regionOfUSA).1)"

        taxRateTmp = "\(rateAndTaxCalc(retailSpend: retailDollar, adultIH: adultInHome, minorIH: minorInHhome, regionOfUSA: regionOfUSA).2)%"

        return returnLbl
    }
}

The function that is being called (rateAndTaxCalc) is in a separate file and it outputs the proper data, when called from "SecondViewController".

All 6 of the picker components in both views appear to be functioning properly. I just need to get the output of the two function calls for the 3 pickers on the embedded view, to be passed to the "SecondViewController", to change "taxRateLbl" and "taxPaidLbl".

Full disclosure: I am new to Swift (2 weeks), but I have programmed in 19 other programming languages in my life, going all the way back to FortranIV, COBOL, and 8080 machine language. So although I'm new to Swift, picking up a new programming language is typically no more difficult for me than a mechanic picking up a different shaped pair of pliers. I suppose that what I'm saying is that I could probably figure this issue out on my own, if I had the time, but time is an issue. I'm trying to get this done before normal life resumes post-virus. So any help will be appreciated. Thank you.


Solution

  • If I have understood you correctly, what you want to achieve is to call the same functions that the system would call for the large picker view, but for the small pickers.

    For instances like this, there are multiple ways that you can accomplish this, and I will list a few below:

    The Straightforward Way (The Bad Way)

    Since you know that FirstViewController is a parent of the SecondViewController, you can simply write (parent as? FirstViewController)?.myMethod() in the second view controller and that will deliver the method to your first view controller.

    However, you should almost NEVER write codes like this because it makes the assumption that SecondaryViewController is always a subcontroller of FirstViewController and making assumptions like this and having this fixed type casts will dramatically reduce the flexibility of your code.(You should know this already if you've coded for a long time, no matter which language)

    The Proper Way

    The proper way to pass any event in Swift, or the iOS system, is via one of the two methods: delegation or notification. The difference is that, while delegate can pass event one-to-one, using a notification can let you broadcast the event to multiple objects. As per the use case here, we'll stick to the delegation method since one-to-one is exactly what you need.

    To do this, you would first declare a protocol, say, SecondViewControllerDelegate and add a method like secondViewController(_ viewController: SecondViewController, pickerDidChange: UIPickerView). You can see that the style of this delegate method matches those found in the iOS API's, where the first parameter is always the object that's sending the call. Then, you'd add a property in SecondViewController called delegate and have it of type SecondViewControllerDelegate?, and then you make FirstViewController conform to this delegate and set itself as the delegate of the second view controller.

    In code:

    // the `class` here means only classes(not structs or enums) can conform to it
    // so we later have a weak reference to it
    protocol SecondViewControllerDelegate: class {
        func secondViewController(_ viewController: SecondViewController, pickerDidChange: UIPickerView)
    }
    
    
    class FirstViewController: UIViewController, SecondViewControllerDelegate {
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            mySecondViewController.delegate = self
        }
    
        func secondViewController(_ viewController: SecondViewController, pickerDidChange: UIPickerView) {
            // Here SecondViewController tell me about the change
        }
    }
    
    class SecondViewController: UIViewController {
    
        // use weak to avoid reference cycle and memory leaks
        weak var delegate: SecondViewControllerDelegate?
    
        func pickerChangedSomehow() {
            // when your picker changes, call your delegate
            delegate?.secondViewController(self, pickerDidChange: myPicker)
        }
    }
    

    Bonus: The Clever Way

    Since in the first view controller performs the function on UIPickerDelegate event call, and so does the second view controller, the clever way here is to make your first view controller the delegate for ALL FOUR picker views(if you didn't know, an object, can be the delegate of many other objects, and that's why the sender of the delegate is passed as the first parameter), so no matter which picker view is changed, you'd always get the delegate call in your first view controller, making everything much easier.