This question already has answers here:
Saving array using NSUserDefaults crashes app
(1 answer)
How to store custom objects in NSUserDefaults
(7 answers)
Closed 7 years ago.
I tried several different method that I can to save this class in NSUserDefaults. I don't know how to save class with override function. How can I make it?
class CountryEntity: AnyObject {
private(set) var id: UInt = 0
private(set) var name = ""
override func cityData(data: Dictionary<String, AnyObject>!) {
id = data.uint(key: "id")
name = data.string(key: "name")
}
}
I tried like that but it doesn't help me
private static var _selectedCountryEntity: AnyObject? = NSUserDefaults.standardUserDefaults().objectForKey(countryNameKey) {
didSet {
let savedData = NSKeyedArchiver.archivedDataWithRootObject(selectedCountryEntity as! NSData)
NSUserDefaults.standardUserDefaults().setObject(savedData, forKey: countryNameKey)
NSUserDefaults.standardUserDefaults().synchronize()
}
}
static var selectedCountryEntity: AnyObject? {
get {
return _selectedCountryEntity
}
set {
// if newValue != _selectedCountryTuple {
_selectedCountryEntity = newValue
// }
}
}
To store custom classes in NSUserDefaults, the data type needs to be a subclass of NSObject and should adhere to NSCoding protocol.
1) Create a custom class for your data
class CustomData: NSObject, NSCoding {
let name : String
let url : String
let desc : String
init(tuple : (String,String,String)){
self.name = tuple.0
self.url = tuple.1
self.desc = tuple.2
}
func getName() -> String {
return name
}
func getURL() -> String{
return url
}
func getDescription() -> String {
return desc
}
func getTuple() -> (String,String,String) {
return (self.name,self.url,self.desc)
}
required init(coder aDecoder: NSCoder) {
self.name = aDecoder.decodeObjectForKey("name") as! String
self.url = aDecoder.decodeObjectForKey("url") as! String
self.desc = aDecoder.decodeObjectForKey("desc") as! String
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(self.name, forKey: "name")
aCoder.encodeObject(self.url, forKey: "url")
aCoder.encodeObject(self.desc, forKey: "desc")
}
}
2) To save data use following function:
func saveData()
{
let data = NSKeyedArchiver.archivedDataWithRootObject(custom)
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(data, forKey:"customArray" )
}
3) To retrieve:
if let data = NSUserDefaults().dataForKey("customArray"),
custom = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? [CustomData] {
// Do something with retrieved data
for item in custom {
print(item)
}
}
Note: Here I am saving and retrieving an array of trhe custom class objects.
Related
This question already has answers here:
Saving custom Swift class with NSCoding to UserDefaults
(12 answers)
Closed 2 years ago.
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)
}
}
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) ?? []
}
...
}
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]) ?? []
}
I'm trying to save a custom class array to UserDefaults but it doesn't work. I get nil back on if let. I looked everywhere online. I'm using Swift 4.2
extension UserDefaults {
func saveReciters(_ reciters: [Reciter]) {
do {
let encodedData = try NSKeyedArchiver.archivedData(withRootObject: reciters, requiringSecureCoding: false)
self.set(encodedData, forKey: UD_RECITERS)
} catch {
debugPrint(error)
return
}
}
func getReciters() -> [Reciter] {
if let reciters = self.object(forKey: UD_RECITERS) as? Data {
return NSKeyedUnarchiver.unarchiveObject(with: reciters) as! [Reciter]
} else {
print("EMPTY RECITERS")
return [Reciter]()
}
}
}
UserInfo={NSDebugDescription=Caught exception during archival: -[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x600001babcc0
Thats my class:
class Reciter: NSCoding {
private(set) public var name: String
private(set) public var image: UIImage?
private(set) public var surahs: [Surah]
private(set) public var documentID: String
private let quranData = QuranData()
init(name: String, image: UIImage?, surahCount: Int?, documentID: String) {
self.name = name
self.image = image
self.documentID = documentID
if let surahCount = surahCount {
surahs = Array(quranData.getAllSurahs().prefix(surahCount))
} else {
surahs = quranData.getAllSurahs()
}
}
func encode(with aCoder: NSCoder) {
}
required init?(coder aDecoder: NSCoder) {
}
}
On my Surah class i get nil back. All other properties i get back succesfully
Most often I see developer's use codeable, here I am using user as an example:
YourDataModel.swift
struct User: Codable {
var userId: String = ""
var name: String = ""
var profileImageData: Data? }
UserDefaults.swift
import Foundation
extension UserDefaults {
/// The current user of the application, see `./Models/User.swift`
var currentUser: User? {
get {
guard let userData = self.object(forKey: #function) as? Data else { return nil }
return try? JSONDecoder().decode(User.self, from: userData)
}
set {
guard let newuser = newValue else { return }
if let userData = try? JSONEncoder().encode(newuser) {
self.set(userData, forKey: #function)
}
}
}
}
Transform the data into json data... #function is the function or value name i.e.
// For the case the user doesn't yet exist.
if ( UserDefaults.standard.currentUser == nil ) {
// Create a new user
user = User()
// Generate an id for the user, using a uuid.
user?.userId = UUID().uuidString
} else {
// otherwise, fetch the user from user defaults.
user = UserDefaults.standard.currentUser
}
I'm new in Swift and iOS and I came across this issue while making an app. I want to basically store user data using NSCoding onto local storage. However, my code below doesn't do it. Anyone can tell me what's wrong with it? Much appreciated!
Also, both saveChecklist and LoadChecklist are called in appDelegate, under applicationDidEnterBackground and applicationWillTerminate.
I have a feeling my issue lies in encodeWithCoder and init(aDecoder), as I used GET to append my Checklist item into lists.
My code in DataModel.class:
import Foundation
class DataModel: NSObject, NSCoding {
var checklist = Checklist()
var lists: [Checklist] {
get {
return [checklist]
}
set {
}
}
override init() {
super.init()
loadChecklist()
}
// MARK: - All the saving stuff
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(checklist, forKey: "Checklist")
}
required init?(coder aDecoder: NSCoder) {
checklist = aDecoder.decodeObjectForKey("Checklist") as! Checklist
}
func documentsDirectory() -> String {
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
return paths[0]
}
func dataFilePath() -> String {
return (documentsDirectory() as NSString).stringByAppendingPathComponent("Checklist.plist") // create file if no checklist.plist is present
}
func saveChecklist() {
let data = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWithMutableData: data)
archiver.encodeObject(lists, forKey: "Checklists")
archiver.finishEncoding()
data.writeToFile(dataFilePath(), atomically: true)
}
func loadChecklist() {
let path = dataFilePath()
if NSFileManager.defaultManager().fileExistsAtPath(path) {
if let data = NSData(contentsOfFile: path) {
let unarchiver = NSKeyedUnarchiver(forReadingWithData: data)
lists = unarchiver.decodeObjectForKey("Checklists") as! [Checklist]
unarchiver.finishDecoding()
}
}
}
My code in Checklist.class:
import Foundation
class Checklist: NSObject, NSCoding {
var item = [items]()
var rituals = [items]()
var doneButtonVisible: Bool
var streak: Int
var itemDoneCount: Int
var startDate: NSDate
var dayHasStarted: Bool
override init() {
doneButtonVisible = false
itemDoneCount = 0
streak = 0
startDate = NSDate()
dayHasStarted = false
super.init()
}
// saves
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(item, forKey: "Items")
aCoder.encodeObject(rituals, forKey: "Rituals")
aCoder.encodeObject(itemDoneCount, forKey: "ItemDoneCount")
aCoder.encodeObject(doneButtonVisible, forKey: "DoneButtonVisible")
aCoder.encodeObject(streak, forKey: "Streak")
aCoder.encodeObject(startDate, forKey: "StartDate")
aCoder.encodeObject(dayHasStarted, forKey: "DayHasStarted")
}
// loads
required init?(coder aDecoder: NSCoder) {
item = aDecoder.decodeObjectForKey("Items") as! [items]
rituals = aDecoder.decodeObjectForKey("Rituals") as! [items]
itemDoneCount = aDecoder.decodeObjectForKey("ItemDoneCount") as! Int
doneButtonVisible = aDecoder.decodeObjectForKey("DoneButtonVisible") as! Bool
streak = aDecoder.decodeObjectForKey("Streak") as! Int
startDate = aDecoder.decodeObjectForKey("StartDate") as! NSDate
dayHasStarted = aDecoder.decodeObjectForKey("DayHasStarted") as! Bool
super.init()
}
}
UPDATE
Solved the saving and loading issue by revamping my data model. Using Core Data instead to save and load data.
However, I still have no idea as to why the above code does not load. Apparently, it does not save the contents of checklist to begin with.
Your Checklist class needs to inherit from NSCoding too, and implement the encodeWithCoder(coder: NSCoder) method. This is a good tutorial on NSCoding.
I am trying to use NSUserDefaults to save an array in to my app's core data. I thought it would be good to use NSUserDefaults but the problem is that wherever I put the code that creates the default it throws up the SIGABRT error.
Here is the code that creates the default:
let levelArrayDefault = NSUserDefaults.standardUserDefaults()
levelArrayDefault.setValue(levelsArray, forKey: "levelsArray")
levelArrayDefault.synchronize()
levelsArray is an array of List objects:
class List: NSObject, NSCoding {
// MARK: Properties
var name: String
var AnswersArray = [Answer]()
init?(name: String) {
// Initialize stored properties.
self.name = name
if name.isEmpty {
return nil
}
}
required init(coder decoder: NSCoder){
self.AnswersArray = (decoder.decodeObjectForKey("AA") as? [Answer])!
self.name = (decoder.decodeObjectForKey("name") as? String)!
}
func encodeWithCoder(coder: NSCoder) {
if let AnswersArray = AnswersArray { coder.encodeObject(AnswersArray, forKey: "AA") }
if let name = name { coder.encodeObject(name, forKey: "name") }
}
}
class Answer: NSObject, NSCoding {
var EnglishAnswer: String = ""
var ChineseAnswer: String = ""
init(newEng: String, newChi: String){
self.EnglishAnswer = newEng
self.ChineseAnswer = newChi
}
required init(coder decoder: NSCoder){
self.EnglishAnswer = (decoder.decodeObjectForKey("EnglishAnswer") as? String)!
self.ChineseAnswer = (decoder.decodeObjectForKey("ChineseAnswer") as? String)!
}
func encodeWithCoder(coder: NSCoder) {
if let EnglishAnswer = EnglishAnswer { coder.encodeObject(EnglishAnswer, forKey: "EnglishAnswer") }
if let ChineseAnswer = ChineseAnswer { coder.encodeObject(ChineseAnswer, forKey: "ChineseAnswer") }
}
}
How can I stop SIGABRT from popping up and get the array to be stored.
Help would be much appreciated.
You need to convert it to NSData using NSKeyedArchiver before storing it to NSUserDefaults, try like this:
update: Xcode 11.4 • Swift 5.2 or later
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let list = List(name: "Student")
list.answers = [Answer(english: "english answer", chinese: "中文回答")]
let data = (try? NSKeyedArchiver.archivedData(withRootObject: [list], requiringSecureCoding: false)) ?? Data()
UserDefaults.standard.set(data, forKey: "listData")
guard
let loadedData = UserDefaults.standard.data(forKey: "listData"),
let loadedArray = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(loadedData) as? [List]
else { return }
print(loadedData.count)
print(loadedArray.first ?? "none")
print(loadedArray.first?.name ?? "no name")
print(loadedArray.first?.answers.first?.english ?? "no english")
print(loadedArray.first?.answers.first?.chinese ?? "no chinese")
}
}
class Answer: NSObject, NSCoding {
let english: String
let chinese: String
init(english: String, chinese: String) {
self.english = english
self.chinese = chinese
}
required init(coder decoder: NSCoder) {
self.english = decoder.decodeString(forKey: "english")
self.chinese = decoder.decodeString(forKey: "chinese")
}
func encode(with coder: NSCoder) {
coder.encode(english, forKey: "english")
coder.encode(chinese, forKey: "chinese")
}
}
class List: NSObject, NSCoding {
let name: String
fileprivate var data = Data()
var answers: [Answer] {
get {
(try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)) as? [Answer] ?? []
}
set {
data = (try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: false)) ?? Data()
}
}
init(name: String) {
self.name = name
}
required init(coder decoder: NSCoder) {
self.data = decoder.decodeData(forKey: "answersData")
self.name = decoder.decodeString(forKey: "name")
}
func encode(with coder: NSCoder) {
coder.encode(data, forKey: "answersData")
coder.encode(name, forKey: "name")
}
}
extension NSCoder {
func decodeString(forKey key: String) -> String {
return decodeObject(forKey: key) as? String ?? ""
}
func decodeData(forKey key: String) -> Data {
return decodeObject(forKey: key) as? Data ?? Data()
}
}
If you want to save your custom object in NSUserDefaults, it's not enough to make your class NSCoding-compliant -- you have to actually encode the data into an NSData object. This is a common mistake -- see my answer to another question for a similar situation.
So, you've added NSCoding to your Answer and List classes. That's a good start. Before you continue, you should verify that you've got that step right by using a NSKeyedArchiver to encode an example of a List object containing a few Answer objects into an instance of NSData, and then use NSKeyedUnarchiver to decode that data object back into your List. Verify that everything that you care about completes the round trip with no problems. This would be an excellent place to use Xcode's testing facility -- you could write a unit test that does exactly what I've described.
Once you know you've got the NSCoding stuff right, you should modify your code so that it encodes your List as NSData and stores the resulting data object in NSUserDefaults using the -setObject:forKey: method.
I am trying to use NSUserDefaults to save an array in to my app's core data. I thought it would be good to use NSUserDefaults but the problem is that wherever I put the code that creates the default it throws up the SIGABRT error.
Here is the code that creates the default:
let levelArrayDefault = NSUserDefaults.standardUserDefaults()
levelArrayDefault.setValue(levelsArray, forKey: "levelsArray")
levelArrayDefault.synchronize()
levelsArray is an array of List objects:
class List: NSObject, NSCoding {
// MARK: Properties
var name: String
var AnswersArray = [Answer]()
init?(name: String) {
// Initialize stored properties.
self.name = name
if name.isEmpty {
return nil
}
}
required init(coder decoder: NSCoder){
self.AnswersArray = (decoder.decodeObjectForKey("AA") as? [Answer])!
self.name = (decoder.decodeObjectForKey("name") as? String)!
}
func encodeWithCoder(coder: NSCoder) {
if let AnswersArray = AnswersArray { coder.encodeObject(AnswersArray, forKey: "AA") }
if let name = name { coder.encodeObject(name, forKey: "name") }
}
}
class Answer: NSObject, NSCoding {
var EnglishAnswer: String = ""
var ChineseAnswer: String = ""
init(newEng: String, newChi: String){
self.EnglishAnswer = newEng
self.ChineseAnswer = newChi
}
required init(coder decoder: NSCoder){
self.EnglishAnswer = (decoder.decodeObjectForKey("EnglishAnswer") as? String)!
self.ChineseAnswer = (decoder.decodeObjectForKey("ChineseAnswer") as? String)!
}
func encodeWithCoder(coder: NSCoder) {
if let EnglishAnswer = EnglishAnswer { coder.encodeObject(EnglishAnswer, forKey: "EnglishAnswer") }
if let ChineseAnswer = ChineseAnswer { coder.encodeObject(ChineseAnswer, forKey: "ChineseAnswer") }
}
}
How can I stop SIGABRT from popping up and get the array to be stored.
Help would be much appreciated.
You need to convert it to NSData using NSKeyedArchiver before storing it to NSUserDefaults, try like this:
update: Xcode 11.4 • Swift 5.2 or later
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let list = List(name: "Student")
list.answers = [Answer(english: "english answer", chinese: "中文回答")]
let data = (try? NSKeyedArchiver.archivedData(withRootObject: [list], requiringSecureCoding: false)) ?? Data()
UserDefaults.standard.set(data, forKey: "listData")
guard
let loadedData = UserDefaults.standard.data(forKey: "listData"),
let loadedArray = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(loadedData) as? [List]
else { return }
print(loadedData.count)
print(loadedArray.first ?? "none")
print(loadedArray.first?.name ?? "no name")
print(loadedArray.first?.answers.first?.english ?? "no english")
print(loadedArray.first?.answers.first?.chinese ?? "no chinese")
}
}
class Answer: NSObject, NSCoding {
let english: String
let chinese: String
init(english: String, chinese: String) {
self.english = english
self.chinese = chinese
}
required init(coder decoder: NSCoder) {
self.english = decoder.decodeString(forKey: "english")
self.chinese = decoder.decodeString(forKey: "chinese")
}
func encode(with coder: NSCoder) {
coder.encode(english, forKey: "english")
coder.encode(chinese, forKey: "chinese")
}
}
class List: NSObject, NSCoding {
let name: String
fileprivate var data = Data()
var answers: [Answer] {
get {
(try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)) as? [Answer] ?? []
}
set {
data = (try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: false)) ?? Data()
}
}
init(name: String) {
self.name = name
}
required init(coder decoder: NSCoder) {
self.data = decoder.decodeData(forKey: "answersData")
self.name = decoder.decodeString(forKey: "name")
}
func encode(with coder: NSCoder) {
coder.encode(data, forKey: "answersData")
coder.encode(name, forKey: "name")
}
}
extension NSCoder {
func decodeString(forKey key: String) -> String {
return decodeObject(forKey: key) as? String ?? ""
}
func decodeData(forKey key: String) -> Data {
return decodeObject(forKey: key) as? Data ?? Data()
}
}
If you want to save your custom object in NSUserDefaults, it's not enough to make your class NSCoding-compliant -- you have to actually encode the data into an NSData object. This is a common mistake -- see my answer to another question for a similar situation.
So, you've added NSCoding to your Answer and List classes. That's a good start. Before you continue, you should verify that you've got that step right by using a NSKeyedArchiver to encode an example of a List object containing a few Answer objects into an instance of NSData, and then use NSKeyedUnarchiver to decode that data object back into your List. Verify that everything that you care about completes the round trip with no problems. This would be an excellent place to use Xcode's testing facility -- you could write a unit test that does exactly what I've described.
Once you know you've got the NSCoding stuff right, you should modify your code so that it encodes your List as NSData and stores the resulting data object in NSUserDefaults using the -setObject:forKey: method.