0

I have a custom object called Badge and I have an array of Badges ([Badge]) that I want to store in UserDefaults. I believe I may be doing it incorrectly. I am able to get my code to build but I get the following error on start inside getBadges() : Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value**. Can someone help. I have tried the solution from here but had no luck.

//
//  Badge.swift
//

import Foundation

class Badge: NSObject {
    var name: String
    var info: String
    var score: Float?
    
    init(name: String, info: String, score: Float?) {
        self.name = name
        self.info = info
        self.score = score
    }
    
    static func ==(lhs: Badge, rhs: Badge) -> Bool {
        return lhs.name == rhs.name
    }
    
    func encodeWithCoder(coder: NSCoder) {
        coder.encode(self.name, forKey: "name")
        coder.encode(self.info, forKey: "info")
    }
}

//
//  BadgeFactory.swift
//

import Foundation

class BadgeFactory {
    let defaults: UserDefaults
    var badges: [Badge] = []
    var userBadges: [Badge] = []
    static let b = "Badges"
    
    init() {
        
        self.defaults = UserDefaults.standard
        self.userBadges = self.getBadges()

    }
    
    func addBadges(score: Float) -> [Badge]
    {
        var new_badges: [Badge] = []
        
        for badge in self.badges {
            if (!self.checkIfUserHasBadge(badge: badge) && badge.score != nil && score >= badge.score!) {
                new_badges.append(badge)
                self.userBadges.append(badge)
            }
        }
        
        self.defaults.set(self.userBadges, forKey: BadgeFactory.b)
        
        return new_badges
    }
    
    func checkIfUserHasBadge(badge: Badge) -> Bool
    {
        if self.badges.contains(badge) {
            return true
        }
        else {
            return false
        }
    }
    
    func getBadges() -> [Badge] {
        return self.defaults.array(forKey: BadgeFactory.b) as! [Badge]
    }
    
    func loadDefaultBadges() {
        // Score badges.
        self.badges.append(Badge(name: "Badge1", info: "My cool badge", score: 80))
        self.badges.append(Badge(name: "Badge2", info: "My second cool badge", score: 90))
    }
        
}
//
//  ViewController.swift
//

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        var bf = BadgeFactory()
        bf.getBadges()
        bf.addBadges(score: 85)
    }

}
albertski
  • 2,428
  • 1
  • 25
  • 44
  • getBadges() is called twice before addBadges(). First in the BadgeFactory.init() and then in the ViewControllers.viewDidLoad(). – Marcy Feb 20 '21 at 02:39

2 Answers2

1

The reason for this error is located in your getBadges() function:

func getBadges() -> [Badge] {
    return self.defaults.array(forKey: BadgeFactory.b) as! [Badge]
}  

With as! you are implicitly unwrapping the array you expect. But as long as you didn't write data to this userDefaults key, array(forKey:) will always return nil!
For this reason, you need to use safe unwrapping here, for example like so:
return self.defaults.array(forKey: BadgeFactory.b) as? [Badge] ?? [].


But that's not the only problem. Like you already stumbled about, you still need to implement the solution of the thread you posted. Custom NSObjects cannot be stored in Defaults without encoding.
You need to implement the NSCoding protocol in your Badge class (init(coder:) is missing) and use an Unarchiver for reading, along with an Archiver for writing your data to defaults.

So your code should look something like this:

class Badge: NSObject, NSCoding {
    var name: String
    var info: String
    var score: Float?
    
    init(name: String, info: String, score: Float?) {
        self.name = name
        self.info = info
        self.score = score
    }

    required init?(coder: NSCoder) {
        self.name = coder.decodeObject(forKey: "name") as! String
        self.info = coder.decodeObject(forKey: "info") as! String
        self.score = coder.decodeObject(forKey: "score") as? Float
    }
    
    static func ==(lhs: Badge, rhs: Badge) -> Bool {
        return lhs.name == rhs.name
    }
    
    func encodeWithCoder(coder: NSCoder) {
        coder.encode(self.name, forKey: "name")
        coder.encode(self.info, forKey: "info")
        coder.encode(self.score, forKey: "score")
    }
}  
class BadgeFactory {
    ...

    func addBadges(score: Float) -> [Badge] {
        ...
        let data = NSKeyedArchiver.archivedData(withRootObject: self.userBadges)
        defaults.set(data, forKey: BadgeFactory.b)
        ...
    }

    func getBadges() -> [Badge] {
        guard let data = defaults.data(forKey: BadgeFactory.b) else { return [] }
        return NSKeyedUnarchiver.unarchiveObject(ofClass: Badge, from: data) ?? [] 
    }
    
    ...
}
Robin Schmidt
  • 1,153
  • 8
  • 15
0

the error likely comes from this line in your viewDidLoad:

bf.getBadges()

This will try to execute self.defaults.array(forKey: BadgeFactory.b) as! [Badge]

At this point UserDefaults are empty (because you do that before calling .addBadges or providing any other value for the key). So self.defaults.array(forKey: BadgeFactory.b) will evaluate to nil and the forced casting as! [Badge] will fail at runtime with a message like the one you provided.

To resolve I would adjust the function like this:

func getBadges() -> [Badge] {
    return (self.defaults.array(forKey: BadgeFactory.b) as? [Badge]) ?? []
}