Storing data between ViewControllers in Swift using Nav Bar - swift

I'm not sure I set this up correctly: you can navigate between calculator view controller to settings view controller and back using the nav bar, however...upon setting either the picker or the segmented control, the data isn't passed back to the calculator view controller via the code below. I think this is because I don't have a corresponding segue on my storyboard pointing back to the calculator view controller. I would do that, but why doesn't the back button show on the story board on settings view controller? If it did I could just control drag back to calculator view controller? I must be missing something?
Also I don't just want the picker/segmented control data to be used once, i.e. I don't want to reset when the user goes back to settings viewcontroller, I want these choices saved as long as the app is open. How do I do that?
The only other existing answers I could find were in Objective C
code:
class settingsViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {
let titleData = TitleData()
var selectedCounty = String?("Allegany")
var priceMax = Float(1000000)
var priceMin = Float(0)
var payoffMax = Float(1000000)
var payoffMin = Float(0)
#IBOutlet weak var countyPicker: UIPickerView!
#IBOutlet weak var segmentedControl: UISegmentedControl!
override func viewDidLoad() {
super.viewDidLoad()
countyPicker.delegate = self
countyPicker.dataSource = self
}
#IBAction func indexChanged(sender: UISegmentedControl) {
switch segmentedControl.selectedSegmentIndex
{
case 0:
priceMax = 500000.0
priceMin = 0.0
case 1:
priceMax = 1000000.0
priceMin = 500000.0
case 2:
priceMax = 2000000.0
priceMin = 1000000.0
case 3:
priceMax = 5000000.0
priceMin = 2000000.0
default:
break
}
}
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return titleData.mdCounties.count
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
selectedCounty = titleData.mdCounties[row].name
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
return titleData.mdCounties[row].name!
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var destViewController: FirstViewController = segue.destinationViewController as! FirstViewController
destViewController.selectedCounty = selectedCounty
destViewController.newPriceMax = priceMax
destViewController.newPriceMin = priceMin
destViewController.newPayoffMax = payoffMax
destViewController.newPayoffMin = payoffMin
}
}

This is a FAQ on stack overflow. It can be generalized as "how do I pass data between ViewControllers." It isn't specific to Swift. The techniques are identical in Objective-C and swift. The only thing that changes is the language you use to implement it.
The most global, flexible way to do this is to create a data container singleton that holds your app state. Set up your singleton to save app state to a file (user defaults, or to a file in your app's documents directory.
Any time you want to save app data, save it to the singleton. Any time you want to read app data, read it from the singleton.
If you have a more specific case where you want to pass data between one view controller and another one that you segue to, you can pass a data object to the destination view controller in prepareForSegue. Then you can change the data object in the second view controller, and when you return from the segue, the data will be changed in the data object.

Related

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.
3-component pickerView
Embedded View that contains 3 separate 1-element pickerViews
1st Label that contains the 1st of 2 numbers calculated and output from a function
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.
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.

xcode cannot assign value of type.. to NSSet

I am working on CoreData with Swift 3 and I am almost done. I did the CoreData entities and now I am trying to save data from a form on the app and I got this error:
import UIKit
import CoreData
class ItemDetailsVC: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
#IBOutlet weak var storePicker: UIPickerView!
#IBOutlet weak var titleField: CustomTextField!
#IBOutlet weak var priceField: CustomTextField!
#IBOutlet weak var detailsField: CustomTextField!
var stores = [Store]()
override func viewDidLoad() {
super.viewDidLoad()
storePicker.delegate = self
storePicker.dataSource = self
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
let store = stores[row]
return store.name
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return stores.count
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
// update
}
func getStores() {
let fetchRequest: NSFetchRequest<Store> = Store.fetchRequest()
do {
self.stores = try context.fetch(fetchRequest)
self.storePicker.reloadAllComponents()
} catch {
// some code
}
}
#IBAction func savePressed(_ sender: UIButton) {
let item = Item(context: context)
if let title = titleField.text {
item.title = title
}
if let price = priceField.text {
item.price = (price as NSString).doubleValue
}
if let details = detailsField.text {
item.details = details
}
item.toStore = stores[storePicker.selectedRow(inComponent: 0)] error: cannot assign value of type 'Store' to value of type NSSet?
}
I am not asking anyone to solve the error, I just wanna know what the error is telling me to do. Error is on the item.toStore = stores[storePicker... line
When you set a relationship on CoreData model to To Many it starts behaving like an Array. And like so you need to append the data to that array instead of assigning an class to it. You can do so with:
.addingObjects(from: [Any])
I am not asking anyone to solve the error, I just wanna know what the
error is telling me to do.
From what I have understood is that toStore is a to many relationship from Item to Store entity in CoreData.
Now since you need to know about the error only ,if I am correct and the relationship is exactly above then it means that Item entity has a set of stores associated with it not a single Store , hence you are getting this error
error: cannot assign value of type 'Store' to value of type NSSet?
The problem is your entity has one-to-many relationship, if you want to fix this issue try to change it "to one". I know you don't want the answer but here is the hint on how you can fix this. If you go to your *.xcdatamodeld file and locate your Item entity you can see the attributes relationships...find appropriate relationship and modify it from "to-many" to "to-one"

Xcode Swift 3 Picker: Putting the DataSource in ViewDidLoad

I am trying to set up a picker that would display different results based on certain conditions. The only issue is that I need to put the dataSource (picker data that will display) outside the viewDidLoad. I need the viewDidLoad to load the appropriate data if the condition was met. However, with code dealing with pickers that I've seen, it seems the dataSource array is placed outside the viewDidLoad (I presume so the delegate methods can use that value). I am just wondering if I can place this dataSource into viewDidLoad so that it would recognize it, or if there was a way to run a Switch or If statement outside the viewDidLoad and in the class.
import UIKit
import Parse
class VC: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {
var pickerData = [String]()
#IBOutlet weak var namePicker: UIPickerView!
override func viewDidLoad() {
super.viewDidLoad()
namePicker.dataSource = self
namePicker.delegate = self
//here I want the picker to show something different depending on the value of nameOfPerson. I changed the order of the array because the picker selects the first name of the array as the default.
switch nameOfPerson
{
case "Carly":
let pickerData = ["Carly","Jeff","Bob","Anita"]
case "Jeff":
let pickerData = ["Jeff","Carly","Bob","Anita"]
case "Bob":
let pickerData = ["Bob","Carly","Jeff","Anita"]
case "Anita":
let pickerData = ["Anita","Carly","Jeff","Bob"]
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return pickerData.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return pickerData[row]
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int)
{
}
}
I am just wondering if I can place this dataSource into viewDidLoad so that it would recognize it, or if there was a way to run a Switch or If statement outside the viewDidLoad and in the class
No. The data source methods must be methods, i.e. functions at the top level of the class, in order for Objective-C to see them. Therefore the data itself must be visible to the data source methods, and the only coherent way to share data between viewDidLoad and the data source methods is as a property, like your var pickerData.
What's wrong with your code is the let:
case "Carly":
let pickerData = ["Carly","Jeff","Bob","Anita"]
case "Jeff":
let pickerData = ["Jeff","Carly","Bob","Anita"]
case "Bob":
let pickerData = ["Bob","Carly","Jeff","Anita"]
case "Anita":
let pickerData = ["Anita","Carly","Jeff","Bob"]
}
Delete the term let from each of those cases! Otherwise you are just setting a temporary local variable pickerData, which is not what you want to do. You want to set the property that you declared like this:
var pickerData = [String]()
....and your code is not doing that! What you want is this:
case "Carly":
self.pickerData = ["Carly","Jeff","Bob","Anita"]
case "Jeff":
self.pickerData = ["Jeff","Carly","Bob","Anita"]
case "Bob":
self.pickerData = ["Bob","Carly","Jeff","Anita"]
case "Anita":
self.pickerData = ["Anita","Carly","Jeff","Bob"]
}
(Plus you need a default case, of course.)
Then, at the end, just to be on the safe side, reload the data:
self.namePicker.reloadAllComponents()
Datasource needs to be set while the view loads, though you can load it anywhere, where you need it.
Inside viewDidLoad
picker.datasource = self
Anywhere, you can set the actual data;
picker.dataArray = "example, anything"
I'm not sure I understand you completely. Please let me know if you were not looking for this.

How can I link a to a second view controller using a button determined by a variable?

I have created a UIPickerView which has an array of services inside. I have used the didSelectRow to display my selected service below the UIPickerView. That's all fine and works perfectly.
I want the service which as been selected to take me to another view controller named that service. For example, Coffee shops as been selected in the Picker, "coffee shops" is displayed below the picker and you click a button which will bring you to a page which displays the given information about coffee shops.
#Pbush25 thanks for the quick reply. Please would you take a look at my code and tell me where I am going wrong as I still can't get it to work.
import UIKit
class ViewController: UIViewController, UIPickerViewDelegate {
#IBOutlet weak var SelectedService: UILabel!
#IBAction func Find(sender: AnyObject) {
}
#IBOutlet weak var ItemLabel: UILabel!
var services = ["Cafe","Coffee Shops","Bar","Takeaway","Sunday Roast","Shoe Mender","Craft Shops","Electrical"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int{
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int{
return services.count
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String!{
return services[row]
}
}
As #JAL said, there is a method for the UIPickerViewDelegate that I belive is didSelectRow or something to that extent, which will give you the index path of the row that was selected. When your button is pressed to go to the next screen, all you have to do in your prepareForSegue method is get the element at that row and use a switch statement to determine which new view controller to activate the segue on based on what value you receive.
EDIT: Added code for clarity.
//save the picked data to a variable
func pickerView(pickerView: UIPickerView!, didSelectRow row: Int, inComponent component: Int)
{
selection = "\(pickerViewData[row])"
}
//use this variable to decide which segue to take
func prepareForSegue(sender: UIStoryboardSegue){
switch selection {
case: "Coffee Shops"
performSegueWithIdentifier("goToCoffeeShops", sender: self)
case: "Some other selection"
performSegueWithIdentifier("someOtherSegue", sender: self)
}
}

How can i navigate to another view controller dependent on a variable

I am trying to link to another view controller by pressing a button which i have named 'find'. The issue is with the pickerView. i want the 'service' selected from the pickerView to determine which page you end up on. For example, you select 'bars' in the pickerView, you click the 'find' button and it takes you to the bars view controller page.
Here is my code which controls the picker and the find button.
import UIKit
class ViewController: UIViewController, UIPickerViewDelegate {
#IBOutlet weak var SelectedService: UILabel!
#IBAction func Find(sender: AnyObject) {
}
#IBOutlet weak var ItemLabel: UILabel!
var services = ["Cafe","Coffee Shops","Bar","Takeaway","Sunday Roast","Shoe Mender","Craft Shops","Electrical"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int{
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int{
return services.count
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String!{
return services[row]
}
}
I am new to coding with swift so please could you explain your answer in a simple way.
Thank you very much
Shaun
In #IBAction func Find() you could have switch statement where you check which item from UIPickerView was selected, and based on that you can present desired view controller like this:
let cafeViewController: CafeViewController = CafeViewController()
self.presentViewController(cafeViewController, animated: true, completion: nil)
But are you sure you need it's own view controller for every item? They'll probably look exactly the same, only presented data will be different... so you could use just one view controller, send picked item in func prepareForSegue() and according on that prepare data in viewDidLoad() in that view controller.
Or at least try to reuse as much as you can creating some base class for your view controller and then create subclass for each service.