Save Label Text and Button States

Hello,


I'm for some help with using User Defaults to store text saved in my progressLabel and button states. The way this works is that each time the user taps a button, the button toggles "on" and "off" and if "on" a percentage is calculated and displayed in the progressLable as "___ % Complete!". I'd like to save this information so that the user can continually go back in and tap additional buttons as they progress, eventually reaching "100% Complete!".


Here is my code:

class ViewController: UIViewController {

@IBOutlet weak var progressLabel: UILabel!


var countClickedButtons: Int = 0 {

didSet {

let percent = Int(100.0 * Double(countClickedButtons) / 14.0)

progressLabel.text = String(percent) + " % Complete!"

}

}

override func viewDidLoad() {

super.viewDidLoad()

}


/


@IBAction func buttonPressed (_ sender: PRToggleButton) {

if !sender.isSelected {

countClickedButtons += 1

}else{

countClickedButtons -= 1

}

sender.isSelected = !sender.isSelected

}


@IBAction func saveButtonStates(_ sender: Any) {

UserDefaults.standard.set((sender as AnyObject).isSelected, forKey: "isSaved")

//I tried the above and several other ways of using UserDefaults from video tutorials, but so far all I get are crashes. With the above, nothing happens.

}


@IBAction func closeView(_ sender: Any) {

self.dismiss(animated: true, completion: nil)

}

}

Replies

I assume you use Swift 4.


You have a wrong


UserDefaults.standard.set((sender as AnyObject).isSelected, forKey: "isSaved")


Doc for set states :

Sets the value of the specified default key.

The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.


So you cannot save as Bool, but you can as String

        UserDefaults.standard.set(String(sender.isSelected), forKey: "isSaved")

That will save "true" or "false"


get it back with

     if let value = UserDefaults.standard.string(forKey: "isSaved") {
          let selectedState = Bool(value) 
    }


Take care: if you have several buttons, you need to save the button title as well, to differentiale between buttons.

Then you should save a dictionary : [title: state]

        UserDefaults.standard.set([sender.title : String(sender.isSelected)], forKey: "isSaved")

The class reference of `UserDefaults` says that there is an overload of `set(_:forKey:)` for Bool:

func set(Bool, forKey: String)


And you can get the Boolean value with:

func bool(forKey: String)

I guess `sender` is not your `PRToggleButton` when the action `saveButtonStates(_:)` is called.


You may need to add another IBOutlet for the button:

class ViewController: UIViewController {
   
    @IBOutlet weak var progressLabel: UILabel!
    @IBOutlet weak var toggleButton: PRToggleButton! //<- This outlet needs to be properly connected.
   
    //...
   
    @IBAction func saveButtonStates(_ sender: Any) {
        UserDefaults.standard.set(toggleButton.isSelected, forKey: "isSaved") //<- Do not use sender in this method.
        //...
    }
   
    //...
}

I apprecaite your help, but I think there may be some confusion regarding what's happening with this aspect of my app. So I have 14 buttons and a label. Each time a button is tapped it changes from its original state to blue, and also calculates a percentage which is displayed in the label. For example, if the user taps 7 of the 14 buttons, those 7 buttons turn blue, and the label changes to show "50% Complete!". What I'd like to happen is when the saveButtonStates button is tapped, the current state of all buttons and the current text displayed in the label is saved. The user can then click the closeView done button, and the next time the user opens the app, the saved button states are loaded as well as the saved label showing "50% Complete!". Also, all of my buttons that generate the percentage are connected tot he buttonPressed function.


I'm not sure if this helps to clarify, but I've tried implementing the suggestions above, but I haven't had any success.


Thanks again for all your helpl!

You aren't paying very close attention to what's going on here, while you're ignoring the problem that has been pointed out to you by the earlier comments.


— You haven't narrowed down the problem beyond stating "my code doesn't work". You need to be able to analyze which parts of the code do what you want, and focus on the code the doesn't. Without that skill, you are going to find software development extremely frustrating, to say the least.


— You say that when you set user defaults, "nothing happens". What does that mean? Nothing should appear to happen, except that a key in user defaults will get written. If there's a problem with the wrong thing getting written (and there is!), you'll notice incorrect behavior when your app starts up again, but you haven't said anything about that, or shown any code for handling restoration of the button states.


— The main thing that's wrong (it appears) is that you've wired up your "saveButtonStates" button to the "saveButtonStates" action method. That means the "sender" parameter is the "saveButtonStates" button itself. Nothing about the states of the other 14 buttons is referenced or used in this method, so you can't possibly succeed in saving the state of those 14 buttons for later.

This is so far beyond me... I've spent all day on this, and what it comes down to is that I just have no idea what I'm doing. I connected all my buttons as outlets, which is what I thought OOPer meant when they said to connect PRToggleButton as an outlet. I'm not sure because all of my buttons are connected to that single IBAction. I added the two UserDefaults under the saveButtonStates because I'd originally wanted to use that button to save the states and label output, but now I'm wondering if it would be easier to just remove that button and use UserDefaults in the ButtonPressed function? Also, I'm sure I'm not setting up the dictionary correctly because I couldn't figure out what type of value to use for 'state' in your suggestions [title: state]. Lastly, to get the saved data back, I created the override func viewDidAppear, but I'm getting erros and "fix" suggestions there.

So what I need to know is:

1) am I putting everything in the right place?

2) what value do I use for the 'state' in the dictionary, and is it in the right place?

3) do I need to have all my buttons connected as outlets?

import UIKit

class MedTrackViewController: UIViewController {


@IBOutlet weak var progressLabel: UILabel!

@IBOutlet weak var bio1Button: PRToggleButton!

@IBOutlet weak var bio2Button: PRToggleButton!

@IBOutlet weak var biochem1Button: PRToggleButton!

@IBOutlet weak var chem1Button: PRToggleButton!

@IBOutlet weak var chem2Button: PRToggleButton!

@IBOutlet weak var ochem1Button: PRToggleButton!

@IBOutlet weak var ochem2Button: PRToggleButton!

@IBOutlet weak var phy1Button: PRToggleButton!

@IBOutlet weak var phy2Button: PRToggleButton!

@IBOutlet weak var precalcButton: PRToggleButton!

@IBOutlet weak var calcButton: PRToggleButton!

@IBOutlet weak var statsButton: PRToggleButton!

@IBOutlet weak var psych1Button: PRToggleButton!

@IBOutlet weak var soc1Button: PRToggleButton!


let dictionary = ["bio1Button": String, "bio2Button": String, "biochem1Button": String, "chem1Button": String, "chem2Button": String, "ochem1Button": String, "ochem2Button": String, "phy1Button": String, "phy2Button": String, "precalcButton": String, "calcButton": String, "statsButton": String, "psych1Button": String, "soc1Button": String]


var countClickedButtons: Int = 0 {

didSet {

let percent = Int(100.0 * Double(countClickedButtons) / 14.0)

progressLabel.text = String(percent) + " % Complete!"


}

}


override func viewDidLoad() {

super.viewDidLoad()


}

override func viewWillAppear(_ animated: Bool) {

if let value = UserDefaults.standard.string(forKey: "saved") {

let selectedState = Bool(value)

}

}



/


@IBAction func ButtonPressed (_ sender: PRToggleButton) {

if !sender.isSelected {

countClickedButtons += 1

}else{

countClickedButtons -= 1

}

sender.isSelected = !sender.isSelected

}




@IBAction func saveButtonStates(_ sender: Any) {

UserDefaults.standard.set(String(sender.isSelected), forKey: "saved")

UserDefaults.standard.set([bio1Button : String(sender.isSelected)], forKey: "saved")

}


@IBAction func closeView(_ sender: Any) {

self.dismiss(animated: true, completion: nil)

}

}

1) am I putting everything in the right place?

No. We cannot call the code as put in the right place when it does not compile.


2) what value do I use for the 'state' in the dictionary, and is it in the right place?

Your purpose of having dictionary is not clear enough to answer.


3) do I need to have all my buttons connected as outlets?

Depends on many other things, but if you want to save all buttons' `isSelected` states, you'd better have them all as outlets.

Remember you can use an outlet array, when you can use it properly.


Here's what I would do when I have 14 buttons and I want to save all `isSelected` state into `UserDefaults`.

class MedTrackViewController: UIViewController {

    @IBOutlet weak var progressLabel: UILabel!
    @IBOutlet var toggleButtons: [PRToggleButton] = []

    func updateButtonPercentage() {
        let countSelected = toggleButtons.map{$0.isSelected ? 1 : 0}.reduce(0, +)
        let percent = Int(100.0 * Double(countSelected) / Double(toggleButtons.count))
        progressLabel.text = "\(percent) % Complete!"
    }

    func saveButtonState(_ button: PRToggleButton) {
        let buttonKey = "saved\(button.tag)"
        UserDefaults.standard.set(button.isSelected, forKey: buttonKey)
    }

    func loadButtonState(_ button: PRToggleButton) {
        let buttonKey = "saved\(button.tag)"
        button.isSelected = UserDefaults.standard.bool(forKey: buttonKey)
    }

    //...

    override func viewWillAppear(_ animated: Bool) {
        for button in toggleButtons {
            loadButtonState(button)
        }
        updateButtonPercentage()
    }

    @IBAction func buttonPressed(_ sender: PRToggleButton) {
        sender.isSelected = !sender.isSelected
        updateButtonPercentage()
        saveButtonState(sender)
    }

    //..
}

Preparations needed:

  • Connect all your `PRToggleButton`s into a single arry outlet `toggleButtons`.
  • Give different `tag`s to all your `PRToggleButton`s.

I'm receiving the following error when I test my app: Thread 1: Fatal error: Double value cannot be converted to Int because it is either infinite or Nan.


It's happening at line 18: let percent = Int(100.0 * Double(countSelected) / Double(toggleButtons.count))


Here's the full code:


class MedTrackViewController: UIViewController {

@IBOutlet weak var progressLabel: UILabel!

@IBOutlet var toggleButtons: [PRToggleButton] = []

func updateButtonPercentage() {

let countSelected = toggleButtons.map{$0.isSelected ? 1 : 0}.reduce(0, +)

let percent = Int(100.0 * Double(countSelected) / Double(toggleButtons.count))

progressLabel.text = "\(percent) % Complete"

}

func saveButtonState(_ button: PRToggleButton) {

let buttonKey = "saved\(button.tag)"

UserDefaults.standard.set(button.isSelected, forKey: buttonKey)

}

func loadButtonState(_ button: PRToggleButton) {

let buttonKey = "saved\(button.tag)"

button.isSelected = UserDefaults.standard.bool(forKey: buttonKey)

}

override func viewDidLoad() {

super.viewDidLoad()


}

override func viewWillAppear(_ animated: Bool) {

for button in toggleButtons {

loadButtonState(button)

}

updateButtonPercentage()

}


/


@IBAction func ButtonPressed (_ sender: PRToggleButton) {

sender.isSelected = !sender.isSelected

updateButtonPercentage()

saveButtonState(sender)

}

@IBAction func closeView(_ sender: Any) {

self.dismiss(animated: true, completion: nil)

}

}

The code `Double(countSelected) / Double(toggleButtons.count)` generates infinite when `toggleButtons.count` is zero.


Have you successfully prepared the two things I have written above:

  • Connect all your `PRToggleButton`s into a single array outlet `toggleButtons`.
  • Give different `tag`s to all your `PRToggleButton`s.


This code is a little safer:

    func updateButtonPercentage() {
        let countSelected = toggleButtons.map{$0.isSelected ? 1 : 0}.reduce(0, +)
        if !toggleButtons.isEmpty {
            let percent = Int(100.0 * Double(countSelected) / Double(toggleButtons.count))
            progressLabel.text = "\(percent) % Complete"
        } else {
            progressLabel.text = "--- % Complete"
        }
    }

If you always see "--- % Complete" in your `progressLabel` with this code, you have not prepared your project yet.

Please re-check your storyboard about IBOutlet connections.


PS. Please use the code insertion feature of this site, just click the icon `<>` in the toolbar of the editing area.

Ok, all my PRToggleButtons are connected to the single outlet toggleButtons. For the tags, I could be missing something, but what I did is assign a 'tag' to each button ranging from 0-13 under the View section of the Attributes Inspector (this section also contains the Content Mode, Semantic, Interaction etc.). When I ran my app, I received a new error: Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value. I'm not sure if this is worth mentioning, but this crash happens before I even get to the viewcontroller that contains the code I'm working on, but the error shows on line 07.

func updateButtonPercentage() {
        let countSelected = toggleButtons.map{$0.isSelected ? 1 : 0}.reduce(0, +)
        if !toggleButtons.isEmpty {
            let percent = Int(100.0 * Double(countSelected) / Double(toggleButtons.count))
            progressLabel.text = "\(percent) % Complete"
        }else{
            progressLabel.text = "--- % Complete"
        }
    }

You call

updateButtonPercentage()

in viewWillAppear


Hence view is not loaded yet and outlets are still nil.


You should move this to viewDidLoad.

I moved it, but I still get the same error as before. Possibly I didn't move it to the correct location?


@IBOutlet weak var progressLabel: UILabel!
    @IBOutlet var toggleButtons: [PRToggleButton] = []
   
    func updateButtonPercentage() {
        let countSelected = toggleButtons.map{$0.isSelected ? 1 : 0}.reduce(0, +)
        if !toggleButtons.isEmpty {
            let percent = Int(100.0 * Double(countSelected) / Double(toggleButtons.count))
            progressLabel.text = "\(percent) % Complete"
        }else{
            progressLabel.text = "--- % Complete"
        }
    }
    func saveButtonState(_ button: PRToggleButton) {
        let buttonKey = "saved\(button.tag)"
        UserDefaults.standard.set(button.isSelected, forKey: buttonKey)
    }
   
    func loadButtonState(_ button: PRToggleButton) {
        let buttonKey = "saved\(button.tag)"
        button.isSelected = UserDefaults.standard.bool(forKey: buttonKey)
    }
   
    override func viewDidLoad() {
        super.viewDidLoad()
        updateButtonPercentage()

    }
    override func viewWillAppear(_ animated: Bool) {
        for button in toggleButtons {
            loadButtonState(button)
        }
       
    }
   
   

        /

    @IBAction func ButtonPressed (_ sender: PRToggleButton) {
        sender.isSelected = !sender.isSelected
        updateButtonPercentage()
        saveButtonState(sender)
       
    }
    @IBAction func closeView(_ sender: Any) {
        self.dismiss(animated: true, completion: nil)
       
    }
}
  

`viewWillAppear` is called after `viewDidLoad`. So you have no need to move the call `updateButtonPercentage()`.


But, anyway, in your new code, the property `toggleButtons` is not an Optional. Even if you accessed it before IBOutlet connection, it would never be nil and hold a non-nil empty array. So it does not cause Unexpectedly found nil.


You seem to have checked all your `PRToggleButton`s are connected to `toggleButtons`, but how is `progressLabel` ?

(The line .07 of your former post shows `progressLabel.text = "--- % Complete"`. `progressLabel` is very likely to be the cause of Unexpectedly found nil.)


Sometimes Xcode shows some IBOutlets connected even when they actually are not connected. Disconnect and re-connect the `UILabel` to the outlet `progressLabel`.

I disconnected my UILabel and reconnected the Outlet, but I still came up with the found nil error. I decided to try changing the function to this

unc updateButtonPercentage() {
        let countSelected = toggleButtons.map{$0.isSelected ? 1 : 0}.reduce(0, +)
        if !toggleButtons.isEmpty {
            let percent = Int(100.0 * Double(countSelected) / Double(toggleButtons.count))
            progressLabel.text = String(percent) + " % Complete"
      
        }

I decided to try taking out the }else{ line since that was the line causing the error.

I ran my app again, and I was able to get to this particular viewcontroller. I selected a few buttons and saw that the label and percentage changed appropriately. Also, with each tap, my buttons changed from their unselected state to their selected state. I exited the view and reentered, and while the progressLabel stayed the same (indicating the percentage calculated had been saved), the button states all reverted to their unselected states. To me this tells me that for some reason the selected states of the buttons are not being saved and loaded again appropriately. Could it be the tags or something else?

Is this the only change you did, really, to delete


else { 
            progressLabel.text = "--- % Complete" 
        }


That seem really surprising.


If so, could you test by reintroducing this else and breakpoint on it, to understand when you get there.


Are the toggleButtons radio button ? In that case there can only be one selected at a time.