How to Save and Retrieve multiple Buttons state to/from UserDefaults

I'm beginner in Swift and I've decided to create my Fitness application and through this long journey I've started to learn and understand a lot. But I have a problem with my buttons. I've created Buttons in StackViews to achieve this result: https://1drv.ms/v/s!AuvEQ2Wt-yaxhaASYXpBHlw4Bx1F9g?e=6sQOqF

And I did it, but I'm sure that I've made something wrong, because I can't figure out HOW and which is the best approach to Store and Retrieve the state of my buttons using UserDefaults. I'm confused and stuck. Trying this for 2 days.


Here is my main screen SelectFoodVC code:


import UIKit

class SelectFoodVC: UIViewController {

    let selectFoodDefaults           = UserDefaults.standard
    let vStackView                   = UIStackView()

    var foodButtonsArray = [FoodButtons.button1, FoodButtons.button2, FoodButtons.button3, FoodButtons.button4, FoodButtons.button5, FoodButtons.button6, FoodButtons.button7, FoodButtons.button8, FoodButtons.button9, FoodButtons.button10, FoodButtons.button11, FoodButtons.button12, FoodButtons.button13, FoodButtons.button14, FoodButtons.button15, FoodButtons.button16, FoodButtons.button17, FoodButtons.button18, FoodButtons.button19]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = Colors.white
        
        setupVerticalSV()
        selectFood()
   
    }
    
    func selectFood() {
        for sender in foodButtonsArray {
            sender.addTarget(self, action: #selector(foodButtonToggle(_:)), for: .touchUpInside)
        }
    }
    
    @objc func foodButtonToggle(_ sender: UIButton!) {
        
        guard let button = sender else { return }
        
        if  !button.isSelected {
             button.isSelected = true
                button.layer.shadowColor = Colors.foodShadow.cgColor
                button.layer.shadowOffset = CGSize(width: 0, height: 5)
                button.layer.shadowRadius = 5
                button.layer.shadowOpacity = 0.4
                button.layer.borderColor = Colors.red.cgColor
                button.setTitleColor(Colors.red, for: .normal)
            selectFoodDefaults.set([button.titleLabel?.text : String(button.isSelected)], forKey: "isSaved")
                
            } else {
            
            button.isSelected = false
            button.layer.shadowColor = .none
            button.layer.shadowOffset = .zero
            button.layer.shadowRadius = 0
            button.layer.shadowOpacity = .zero
            button.layer.borderColor = Colors.gray.cgColor
            button.setTitleColor(Colors.black, for: .normal)
            selectFoodDefaults.removeObject(forKey: "isSaved")

        }
    }
    
    
    //MARK: - Vertical Stack View
    
    func setupVerticalSV() {
        vStackView.axis = .vertical
        vStackView.alignment = .center // .leading .firstBaseline .center .trailing .lastBaseline
        vStackView.distribution = .fillEqually // .fillEqually .fillProportionally .equalSpacing .equalCentering
        vStackView.spacing = 10
        vStackView.addArrangedSubview(hStacks.hStackViewOne)
        vStackView.addArrangedSubview(hStacks.hStackViewTwo)
        vStackView.addArrangedSubview(hStacks.hStackViewThree)
        vStackView.addArrangedSubview(hStacks.hStackViewFour)
        vStackView.addArrangedSubview(hStacks.hStackViewFive)
        vStackView.addArrangedSubview(hStacks.hStackViewSix)
        view.addSubview(vStackView)
        vStackView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            vStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            vStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
    }
    
}

With this CustomButton file I'm initializing my custom buttons:


class FoodButton: UIButton {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        configure()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    override func layoutSubviews() {
        super.layoutSubviews()
        configure()
    }
    
    
    init(title: String, borderColor: CGColor, titleColor: UIColor) {
        super.init(frame: .zero)
        self.setTitle(title, for: .normal)
        self.layer.borderColor = borderColor
        self.setTitleColor(titleColor, for: .normal)
        configure()
    }
    
    
    func configure() {
        layer.borderWidth                         = 1
        backgroundColor                           = Colors.white
        contentEdgeInsets                         = UIEdgeInsets(top: 8, left: 15, bottom: 8, right: 15)
        layer.cornerRadius                        = frame.size.height / 2
        titleLabel?.font                          = UIFont.systemFont(ofSize: 15, weight: .regular)
        translatesAutoresizingMaskIntoConstraints = false
    }
    
}



In this SelectFoodModelVC file I'm creating Buttons and Horizontal StackViews, the adding FoodButtons and hStacks in main SelectFoodVC


import UIKit

struct FoodButtons {
    
    static var button1 = FoodButton(title: "Spices", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button2 = FoodButton(title: "Sellfish", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button3 = FoodButton(title: "Eggs", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button4 = FoodButton(title: "Onion", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button5 = FoodButton(title: "Garlic", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button6 = FoodButton(title: "Citrus", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button7 = FoodButton(title: "Milk", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button8 = FoodButton(title: "Peanuts", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button9 = FoodButton(title: "Soy", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button10 = FoodButton(title: "Fish", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button11 = FoodButton(title: "Tree Nuts", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button12 = FoodButton(title: "Coriander", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button13 = FoodButton(title: "Mushrooms", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button14 = FoodButton(title: "Beef", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button15 = FoodButton(title: "Pork", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button16 = FoodButton(title: "Salmon", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button17 = FoodButton(title: "Tuna", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button18 = FoodButton(title: "Shrimps", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    static let button19 = FoodButton(title: "Gluten", borderColor: Colors.gray.cgColor, titleColor: Colors.black)
    
}

struct hStacks {
    
    static var hStackViewOne : UIStackView = {
        let hSV = UIStackView()
        
        hSV.axis = .horizontal
        hSV.alignment = .center // .leading .firstBaseline .center .trailing .lastBaseline
        hSV.distribution = .fillEqually // .fillEqually .fillProportionally .equalSpacing .equalCentering
        hSV.spacing = 10
        hSV.addArrangedSubview(FoodButtons.button1)
        hSV.addArrangedSubview(FoodButtons.button2)
        hSV.addArrangedSubview(FoodButtons.button3)
        
        return hSV
    }()
    
    static var hStackViewTwo: UIStackView = {
        let hSV = UIStackView()
        
        hSV.axis = .horizontal
        hSV.alignment = .center // .leading .firstBaseline .center .trailing .lastBaseline
        hSV.distribution = .fillEqually // .fillEqually .fillProportionally .equalSpacing .equalCentering
        hSV.spacing = 10
        hSV.addArrangedSubview(FoodButtons.button4)
        hSV.addArrangedSubview(FoodButtons.button5)
        hSV.addArrangedSubview(FoodButtons.button6)
        hSV.addArrangedSubview(FoodButtons.button7)
        
        return hSV
    }()
    
    static var hStackViewThree: UIStackView = {
        let hSV = UIStackView()
        
        hSV.axis = .horizontal
        hSV.alignment = .center // .leading .firstBaseline .center .trailing .lastBaseline
        hSV.distribution = .fillEqually // .fillEqually .fillProportionally .equalSpacing .equalCentering
        hSV.spacing = 10
        hSV.addArrangedSubview(FoodButtons.button8)
        hSV.addArrangedSubview(FoodButtons.button9)
        hSV.addArrangedSubview(FoodButtons.button10)
        
        return hSV
    }()
    
    static var hStackViewFour: UIStackView = {
        let hSV = UIStackView()
        
        hSV.axis = .horizontal
        hSV.alignment = .center // .leading .firstBaseline .center .trailing .lastBaseline
        hSV.distribution = .fillEqually // .fillEqually .fillProportionally .equalSpacing .equalCentering
        hSV.spacing = 10
        hSV.addArrangedSubview(FoodButtons.button11)
        hSV.addArrangedSubview(FoodButtons.button12)
        hSV.addArrangedSubview(FoodButtons.button13)
        
        return hSV
    }()
    
    static var hStackViewFive: UIStackView = {
        let hSV = UIStackView()
        
        hSV.axis = .horizontal
        hSV.alignment = .center // .leading .firstBaseline .center .trailing .lastBaseline
        hSV.distribution = .fillEqually // .fillEqually .fillProportionally .equalSpacing .equalCentering
        hSV.spacing = 10
        hSV.addArrangedSubview(FoodButtons.button14)
        hSV.addArrangedSubview(FoodButtons.button15)
        hSV.addArrangedSubview(FoodButtons.button16)
        
        return hSV
    }()
    
    static var hStackViewSix: UIStackView = {
        let hSV = UIStackView()
        
        hSV.axis = .horizontal
        hSV.alignment = .center // .leading .firstBaseline .center .trailing .lastBaseline
        hSV.distribution = .fillEqually // .fillEqually .fillProportionally .equalSpacing .equalCentering
        hSV.spacing = 10
        hSV.addArrangedSubview(FoodButtons.button17)
        hSV.addArrangedSubview(FoodButtons.button18)
        hSV.addArrangedSubview(FoodButtons.button19)
        
        return hSV
    }()
}

Replies

What do you think is the state of your buttons? Only `isSelected`, or any other states held in UIButton?

A few advices.


You would have it much simpler doing in IB instead of code.

- create objects (as buuttons) with all their attributes

- setting constraints and arrange all objects


As you have a lot of buttons, it is convenient to create IBOutlets collection for them, so that you can reference as button[i], do it in loop, instead of being forced to call myButtonX indivdually


There is nowhere in code a reference to UserDefaults.

What did you try ?

What states do you want to save ?

If it is only isSeleted, then save an array of Bool and reload when viewDidLoad.

What is it you do not succeed in doing ?

At the moment I know about '.isSelected'

I'm doing all programmatically, without storyboards.


selectFoodDefaults.set([button.titleLabel?.text : String(button.isSelected)], forKey: "isSaved")


Here I'm saving the state of my button, but I think it is wrong, because I should be able to retrieve this UserDefaults for later in User Profile to display the same view.

Then you should first design your data structure to be saved.

At the moment, Dictionary<String, Bool> would work.

In the future, you may need something like Dictionary<String, ButtonState>.

(ButtonState may contain `isSelected` and some other status you want to save in the future.)


To avoid complexity, try using Dictionary<String, Bool>.


You can write something like this:

    func retrieveFoodSelection() {
        let foodSelection = selectFoodDefaults.dictionary(forKey: "foodSelection") as? [String: Bool] ?? [:]
        for foodButton in foodButtonsArray {
            if let foodName = foodButton.titleLabel?.text {
                let isSelected = foodSelection[foodName, default: false]
                if foodButton.isSelected != isSelected {
                    foodButtonToggle(foodButton)
                }
            }
        }
    }
    
    func saveFoodSelection() {
        let foodSelection: [String: Bool] = Dictionary(uniqueKeysWithValues: foodButtonsArray.map {($0.titleLabel?.text ?? "", $0.isSelected)})
        selectFoodDefaults.set(foodSelection, forKey: "foodSelection")
    }


Generally, it is not recommended to use text held in UILabel as key, but I have shown a simplified code.

(For example, when your app is internationalized, your app would not work if user changes language.)

You try to save a dictionary ? For a single button ?

You have 20 buttons and a single key ?


Did you read the doc about defaults.set

func set(_ value: Any?, forKey defaultName: String)

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. For more information, see What is a Property List? in Property List Programming Guide.


I would advise to storae differently:

create an array for the 20 buttons

var stateToSave = Array

(repeating: "true", count: 20)


for each button, you'll set the value as needed.

For button3

stateToSave[2] = String(button3.isSelected)


and save to defaults with

selectFoodDefaults.set(stateToSave, forKey: "isSaved")