I have a struct that I want to save to UserDefaults. Here's my struct
struct Song {
var title: String
var artist: String
}
var songs: [Song] = [
Song(title: "Title 1", artist "Artist 1"),
Song(title: "Title 2", artist "Artist 2"),
Song(title: "Title 3", artist "Artist 3"),
]
In another ViewController, I have a UIButton that appends to this struct like
#IBAction func likeButtonPressed(_ sender: Any) {
songs.append(Song(title: songs[thisSong].title, artist: songs[thisSong].artist))
}
I want it so that whenever the user clicks on that button also, it saves the struct to UserDefaults so that whenever the user quits the app and then opens it agian, it is saved. How would I do this?
In Swift 4 this is pretty much trivial. Make your struct codable simply by marking it as adopting the Codable protocol:
struct Song:Codable {
var title: String
var artist: String
}
Now let's start with some data:
var songs: [Song] = [
Song(title: "Title 1", artist: "Artist 1"),
Song(title: "Title 2", artist: "Artist 2"),
Song(title: "Title 3", artist: "Artist 3"),
]
Here's how to get that into UserDefaults:
UserDefaults.standard.set(try? PropertyListEncoder().encode(songs), forKey:"songs")
And here's how to get it back out again later:
if let data = UserDefaults.standard.value(forKey:"songs") as? Data {
let songs2 = try? PropertyListDecoder().decode(Array<Song>.self, from: data)
}
This is my UserDefaults extension in main thread, to set get Codable object into UserDefaults
// MARK: - UserDefaults extensions
public extension UserDefaults {
/// Set Codable object into UserDefaults
///
/// - Parameters:
/// - object: Codable Object
/// - forKey: Key string
/// - Throws: UserDefaults Error
public func set<T: Codable>(object: T, forKey: String) throws {
let jsonData = try JSONEncoder().encode(object)
set(jsonData, forKey: forKey)
}
/// Get Codable object into UserDefaults
///
/// - Parameters:
/// - object: Codable Object
/// - forKey: Key string
/// - Throws: UserDefaults Error
public func get<T: Codable>(objectType: T.Type, forKey: String) throws -> T? {
guard let result = value(forKey: forKey) as? Data else {
return nil
}
return try JSONDecoder().decode(objectType, from: result)
}
}
Update This is my UserDefaults extension in background, to set get Codable object into UserDefaults
// MARK: - JSONDecoder extensions
public extension JSONDecoder {
/// Decode an object, decoded from a JSON object.
///
/// - Parameter data: JSON object Data
/// - Returns: Decodable object
public func decode<T: Decodable>(from data: Data?) -> T? {
guard let data = data else {
return nil
}
return try? self.decode(T.self, from: data)
}
/// Decode an object in background thread, decoded from a JSON object.
///
/// - Parameters:
/// - data: JSON object Data
/// - onDecode: Decodable object
public func decodeInBackground<T: Decodable>(from data: Data?, onDecode: #escaping (T?) -> Void) {
DispatchQueue.global().async {
let decoded: T? = self.decode(from: data)
DispatchQueue.main.async {
onDecode(decoded)
}
}
}
}
// MARK: - JSONEncoder extensions
public extension JSONEncoder {
/// Encodable an object
///
/// - Parameter value: Encodable Object
/// - Returns: Data encode or nil
public func encode<T: Encodable>(from value: T?) -> Data? {
guard let value = value else {
return nil
}
return try? self.encode(value)
}
/// Encodable an object in background thread
///
/// - Parameters:
/// - encodableObject: Encodable Object
/// - onEncode: Data encode or nil
public func encodeInBackground<T: Encodable>(from encodableObject: T?, onEncode: #escaping (Data?) -> Void) {
DispatchQueue.global().async {
let encode = self.encode(from: encodableObject)
DispatchQueue.main.async {
onEncode(encode)
}
}
}
}
// MARK: - NSUserDefaults extensions
public extension UserDefaults {
/// Set Encodable object in UserDefaults
///
/// - Parameters:
/// - type: Encodable object type
/// - key: UserDefaults key
/// - Throws: An error if any value throws an error during encoding.
public func set<T: Encodable>(object type: T, for key: String, onEncode: #escaping (Bool) -> Void) throws {
JSONEncoder().encodeInBackground(from: type) { [weak self] (data) in
guard let data = data, let `self` = self else {
onEncode(false)
return
}
self.set(data, forKey: key)
onEncode(true)
}
}
/// Get Decodable object in UserDefaults
///
/// - Parameters:
/// - objectType: Decodable object type
/// - forKey: UserDefaults key
/// - onDecode: Codable object
public func get<T: Decodable>(object type: T.Type, for key: String, onDecode: #escaping (T?) -> Void) {
let data = value(forKey: key) as? Data
JSONDecoder().decodeInBackground(from: data, onDecode: onDecode)
}
}
If the struct contains only property list compliant properties I recommend to add a property propertyListRepresentation and a corresponding init method
struct Song {
var title: String
var artist: String
init(title : String, artist : String) {
self.title = title
self.artist = artist
}
init?(dictionary : [String:String]) {
guard let title = dictionary["title"],
let artist = dictionary["artist"] else { return nil }
self.init(title: title, artist: artist)
}
var propertyListRepresentation : [String:String] {
return ["title" : title, "artist" : artist]
}
}
To save an array of songs to UserDefaults write
let propertylistSongs = songs.map{ $0.propertyListRepresentation }
UserDefaults.standard.set(propertylistSongs, forKey: "songs")
To read the array
if let propertylistSongs = UserDefaults.standard.array(forKey: "songs") as? [[String:String]] {
songs = propertylistSongs.flatMap{ Song(dictionary: $0) }
}
If title and artist will never be mutated consider to declare the properties as constants (let) .
This answer was written while Swift 4 was in beta status. Meanwhile conforming to Codable is the better solution.
Here is a modern Swift 5.1 #propertyWrapper, allowing to store any Codable object in form of a human readable JSON string:
#propertyWrapper struct UserDefaultEncoded<T: Codable> {
let key: String
let defaultValue: T
init(key: String, default: T) {
self.key = key
defaultValue = `default`
}
var wrappedValue: T {
get {
guard let jsonString = UserDefaults.standard.string(forKey: key) else {
return defaultValue
}
guard let jsonData = jsonString.data(using: .utf8) else {
return defaultValue
}
guard let value = try? JSONDecoder().decode(T.self, from: jsonData) else {
return defaultValue
}
return value
}
set {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
guard let jsonData = try? encoder.encode(newValue) else { return }
let jsonString = String(bytes: jsonData, encoding: .utf8)
UserDefaults.standard.set(jsonString, forKey: key)
}
}
}
Usage:
extension Song: Codable {}
#UserDefaultEncoded(key: "songs", default: [])
var songs: [Song]
func addSong(_ song: Song) {
// This will automatically store new `songs` value
// to UserDefaults
songs.append(song)
}
From here:
A default object must be a property list—that is, an instance of (or for collections, a combination of instances of):
NSData
,
NSString
,
NSNumber
,
NSDate
,
NSArray
, or
NSDictionary
. If you want to store any other type of object, you should typically archive it to create an instance of NSData.
You need to use NSKeydArchiver. Documentation can be found here and examples here and here.
If you are just trying to save this array of songs in UserDefaults and nothing fancy use this:-
//stores the array to defaults
UserDefaults.standard.setValue(value: songs, forKey: "yourKey")
//retrieving the array
UserDefaults.standard.object(forKey: "yourKey") as! [Song]
//Make sure to typecast this as an array of Song
If you are storing a heavy array, I suggest you to go with NSCoding protocol or the Codable Protocol in swift 4
Example of coding protocol:-
struct Song {
var title: String
var artist: String
}
class customClass: NSObject, NSCoding { //conform to nsobject and nscoding
var songs: [Song] = [
Song(title: "Title 1", artist "Artist 1"),
Song(title: "Title 2", artist "Artist 2"),
Song(title: "Title 3", artist "Artist 3"),
]
override init(arr: [Song])
self.songs = arr
}
required convenience init(coder aDecoder: NSCoder) {
//decoding your array
let songs = aDecoder.decodeObject(forKey: "yourKey") as! [Song]
self.init(are: songs)
}
func encode(with aCoder: NSCoder) {
//encoding
aCoder.encode(songs, forKey: "yourKey")
}
}
I'd imagine that it should be quite common to represent a user's settings as an observable object. So, here's an example of keeping observable data synchronised with user defaults and updated for xCode 11.4. This can be used in the context of environment objects also.
import SwiftUI
final class UserData: ObservableObject {
#Published var selectedAddress: String? {
willSet {
UserDefaults.standard.set(newValue, forKey: Keys.selectedAddressKey)
}
}
init() {
selectedAddress = UserDefaults.standard.string(forKey: Keys.selectedAddressKey)
}
private struct Keys {
static let selectedAddressKey = "SelectedAddress"
}
}
Swift 5
If you want need to save struct in UserDefault using only on data format.
Smaple struct
struct StudentData:Codable{
var id: Int?
var name: String?
var createdDate: String?
// for decode the value
init(from decoder: Decoder) throws {
let values = try? decoder.container(keyedBy: codingKeys.self)
id = try? values?.decodeIfPresent(Int.self, forKey: .id)
name = try? values?.decodeIfPresent(String.self, forKey: .name)
createdDate = try? values?.decodeIfPresent(String.self, forKey: .createdDate)
}
// for encode the value
func encode(to encoder: Encoder) throws {
var values = encoder.container(keyedBy: codingKeys.self)
try? values.encodeIfPresent(id, forKey: .id)
try? values.encodeIfPresent(name, forKey: .name)
try? values.encodeIfPresent(createdDate, forKey: .createdDate)
}
}
There are two types to convert as data
Codable (Encodable and Decodable).
PropertyListEncoder and PropertyListDecoder
First we using the Codable (Encodable and Decodable) to save the struct
Example for save value
let value = StudentData(id: 1, name: "Abishek", createdDate: "2020-02-11T11:23:02.3332Z")
guard let data = try? JSONEncoder().encode(value) else {
fatalError("unable encode as data")
}
UserDefaults.standard.set(data, forKey: "Top_student_record")
Retrieve value
guard let data = UserDefaults.standard.data(forKey: "Top_student_record") else {
// write your code as per your requirement
return
}
guard let value = try? JSONDecoder().decode(StudentData.self, from: data) else {
fatalError("unable to decode this data")
}
print(value)
Now we using the PropertyListEncoder and PropertyListDecoder to save the struct
Example for save value
let value = StudentData(id: 1, name: "Abishek", createdDate: "2020-02-11T11:23:02.3332Z")
guard let data = try? PropertyListEncoder().encode(value) else {
fatalError("unable encode as data")
}
UserDefaults.standard.set(data, forKey: "Top_student_record")
Retrieve value
guard let data = UserDefaults.standard.data(forKey: "Top_student_record") else {
// write your code as per your requirement
return
}
guard let value = try? PropertyListDecoder().decode(StudentData.self, from: data) else {
fatalError("unable to decode this data")
}
print(value)
In your convenience you can use the any type to save the struct in userDefault.
Here is a simpler solution
#propertyWrapper
struct CodableUserDefault<Value: Codable> {
let key: String
let defaultValue: Value
private let container: UserDefaults = .standard
var wrappedValue: Value {
get {
guard let data = container.data(forKey: key), let object = try? JSONDecoder().decode(Value.self, from: data) else {
return defaultValue
}
return object
}
set {
container.set(try? JSONEncoder().encode(newValue), forKey: key)
}
}
}
Usage
enum ACodableEnum: String, Codable {
case first
case second
}
class SomeController {
#CodableUserDefault<ACodableEnum>(key: "key", defaultValue: .first)
private var aCodableEnum: ACodableEnum
}
Related
I'm trying to retrieve maps values from a Firebase Firestore JSON file. So far I've been able to get any field I wanted but I'm having trouble with maps since they are so nested.
Here you can see an image of the JSON.
And here you can see as text.
"user_metrics": {
"arrayValue": {
"values": [
{
"mapValue": {
"fields": {
"point1": {
"integerValue": "0"
},
"point2": {
"integerValue": "0"
},
"metricDescription": {
"stringValue": "Distanza Pupillare"
},
"metricResult": {
"doubleValue": 6.27
},
"metricDescriptionEn": {
"stringValue": "Distance between pupils"
},
"metricTitle": {
"stringValue": "DBP"
}
}
}
},
And I'm decoding it as follows:
struct SessionResponse : Codable {
let sessions : [Session_Struct]
private enum CodingKeys : String, CodingKey {
case sessions = "documents"
}
}
struct SessionStringValue : Codable {
let value : String
private enum CodingKeys : String, CodingKey {
case value = "stringValue"
}
}
struct Session_Struct : Codable {
let is_first_session : Bool
let device : String
let glassesRefs : [String]
let ref_catalogo : String
let total_time : Double
let data_inizio_sessione : String
let data_fine_user_session : String
let device_id : String
let lineaRefs : [String]
let modelRefs : [String]
let user_metrics : [String]
private enum SessionKeys : String, CodingKey {
case fields
}
private enum FieldKeys : String, CodingKey {
case is_first_session
case device
case glassesRefs
case ref_catalogo
case total_time
case data_inizio_sessione
case data_fine_user_session
case device_id
case lineaRefs
case modelRefs
case user_metrics
}
// MARK: - Total Time
struct TotalTime: Codable {
let doubleValue: Double
}
// MARK: - First Session
struct FirstSession: Codable {
let booleanValue: Bool
}
// MARK: - ListaRefsSizes
struct MapMetrics: Codable {
let arrayValue: MetricsArrayValue
}
// MARK: - ArrayValue
struct MetricsArrayValue: Codable {
let values: [ValueMetric]
}
// MARK: - ArrayValue
struct ValueMetric: Codable {
let mapValue : MapValue
}
// MARK: - ListaRefsSizes
struct ListaRefsGlasses: Codable {
let arrayValue: ArrayValue
}
// MARK: - ArrayValue
struct ArrayValue: Codable {
let values: [Value]
}
// MARK: - Value
struct Value: Codable {
let stringValue: String
}
// MARK: - Value
struct MapValue: Codable {
let mapValue: String
}
// MARK: - Value
struct ReferenceValue: Codable {
let referenceValue: String
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: SessionKeys.self)
let fieldContainer = try container.nestedContainer(keyedBy: FieldKeys.self, forKey: .fields)
is_first_session = try fieldContainer.decode(FirstSession.self, forKey: .is_first_session).booleanValue
device = try fieldContainer.decode(SessionStringValue.self, forKey: .device).value
ref_catalogo = try fieldContainer.decode(ReferenceValue.self, forKey: .ref_catalogo).referenceValue
total_time = try fieldContainer.decode(TotalTime.self, forKey: .total_time).doubleValue
data_inizio_sessione = try fieldContainer.decode(SessionStringValue.self, forKey: .data_inizio_sessione).value
data_fine_user_session = try fieldContainer.decode(SessionStringValue.self, forKey: .data_fine_user_session).value
device_id = try fieldContainer.decode(SessionStringValue.self, forKey: .device_id).value
//Lists
glassesRefs = try fieldContainer.decode(ListaRefsGlasses.self, forKey: .glassesRefs)
.arrayValue.values.map{ $0.stringValue }
lineaRefs = try fieldContainer.decode(ListaRefsGlasses.self, forKey: .lineaRefs)
.arrayValue.values.map{ $0.stringValue }
modelRefs = try fieldContainer.decode(ListaRefsGlasses.self, forKey: .modelRefs)
.arrayValue.values.map{ $0.stringValue }
user_metrics = try fieldContainer.decode(MapMetrics.self, forKey: .user_metrics)
.arrayValue.values.map{ $0.mapValue } // <-- How to read this??
}
}
My issue is that I'm not being able to read the array inside the field "user_metrics". Any idea on how to achieve that? Thanks to anyone who can help!
import Foundation
// MARK: - Welcome
struct Welcome: Codable {
let userMetrics: UserMetrics
enum CodingKeys: String, CodingKey {
case userMetrics = "user_metrics"
}
}
// MARK: - UserMetrics
struct UserMetrics: Codable {
let arrayValue: ArrayValue
}
// MARK: - ArrayValue
struct ArrayValue: Codable {
let values: [Value]
}
// MARK: - Value
struct Value: Codable {
let mapValue: MapValue
}
// MARK: - MapValue
struct MapValue: Codable {
let fields: Fields
}
// MARK: - Fields
struct Fields: Codable {
let point1, point2: Point
let metricDescription: Metric
let metricResult: MetricResult
let metricDescriptionEn, metricTitle: Metric
}
// MARK: - Metric
struct Metric: Codable {
let stringValue: String
}
// MARK: - MetricResult
struct MetricResult: Codable {
let doubleValue: Double
}
// MARK: - Point
struct Point: Codable {
let integerValue: String
}
there is structure for your json
call the api request and get data to Welcome(renamed it as you want)
func requestHTTP(urlString:String,completionHandler:#escaping(_ model:Welcome?,_ error:Error?) -> Void) {
guard let requestUrl = URL(string: urlString) else {return}
URLSession.shared.dataTask(with: requestUrl) { (data, httpUrlResponse, error) in
if(error == nil && data != nil && data?.count != 0) {
do {
let response = try JSONDecoder().decode(Welcome.self, from: data!)
completionHandler(response,nil)
} catch {
debugPrint("error")
completionHandler(nil,error)
}
} else {
completionHandler(nil,error)
}
}.resume()
}
and your will get your data easily
func getDataOnController() {
requestHTTP(urlString: "your url here") { model, error in
if let model = model {
let mapVlues = model.userMetrics.arrayValue.values.map({$0.mapValue})
print(mapVlues)
print(mapVlues.count)
}
}
}
I have an App and also a Share Extension. Between them I share data via UserDefaults. But it stopped working all of a sudden. Only bools or Strings can now be retrieved inside the Share Extension but when trying to retrieve a Custom Struct it is always returning nil.
Custom Struct getter/setter in UserDefaults:
//MARK: dataSourceArray
func setDataSourceArray(data: [Wishlist]?){
set(try? PropertyListEncoder().encode(data), forKey: Keys.dataSourceKey)
synchronize()
}
func getDataSourceArray() -> [Wishlist]? {
if let data = self.value(forKey: Keys.dataSourceKey) as? Data {
do {
_ = try PropertyListDecoder().decode(Array < Wishlist > .self, from: data) as [Wishlist]
} catch let error {
print(error)
}
if let dataSourceArray =
try? PropertyListDecoder().decode(Array < Wishlist > .self, from: data) as[Wishlist] {
return dataSourceArray
}
}
return nil
}
I am calling it like this inside my Extension as well as in my Main App:
if let defaults = UserDefaults(suiteName: UserDefaults.Keys.groupKey) {
if let data = defaults.getDataSourceArray() {
print("working")
} else {
print("error getting datasourceArray")
}
}
This is printing "working" in the Main App but "error getting datasourceArray" in my Extension. I don't understand the issue, especially because simple Bool-Getter are working also from my Share Extension, the issue is only with the Custom Struct.
What am I missing here?
Wishlist Struct:
import UIKit
enum PublicState: String, Codable {
case PUBLIC
case PUBLIC_FOR_FRIENDS
case NOT_PUBLIC
}
struct Wishlist: Codable {
var id: String
var name: String
var image: UIImage
var wishes: [Wish]
var color: UIColor
var textColor: UIColor
var index: Int
var publicSate: PublicState
enum CodingKeys: String, CodingKey {
case id, name, image, wishData, color, textColor, index, isPublic, isPublicForFriends, publicSate
}
init(id: String, name: String, image: UIImage, wishes: [Wish], color: UIColor, textColor: UIColor, index: Int, publicSate: PublicState) {
self.id = id
self.name = name
self.image = image
self.wishes = wishes
self.color = color
self.textColor = textColor
self.index = index
self.publicSate = publicSate
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
name = try values.decode(String.self, forKey: .name)
wishes = try values.decode([Wish].self, forKey: .wishData)
color = try values.decode(Color.self, forKey: .color).uiColor
textColor = try values.decode(Color.self, forKey: .textColor).uiColor
index = try values.decode(Int.self, forKey: .index)
publicSate = try values.decode(PublicState.self, forKey: .publicSate)
let data = try values.decode(Data.self, forKey: .image)
guard let image = UIImage(data: data) else {
throw DecodingError.dataCorruptedError(forKey: .image, in: values, debugDescription: "Invalid image data")
}
self.image = image
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(wishes, forKey: .wishData)
try container.encode(Color(uiColor: color), forKey: .color)
try container.encode(Color(uiColor: textColor), forKey: .textColor)
try container.encode(index, forKey: .index)
try container.encode(image.pngData(), forKey: .image)
try container.encode(publicSate, forKey: .publicSate)
}
}
Update
This is the part where it fails:
if let data = self.value(forKey: Keys.dataSourceKey) as? Data
Is there any way to catch an error?
I also found out that this feature is actually working for other users. The app is live: https://apps.apple.com/de/app/wishlists-einfach-w%C3%BCnschen/id1503912334
But it is not working for me? I deinstalled the app, downloaded it from the App Store but it is still not working.
I had the same problem but with another type of extension. Hope it works for you too.
Create a file you share between the two targets and put the following code there:
//MARK: - Model
struct WishlistStruct: Codable {
//your wishlist struct, I'll assume you'll have a name and some items
var name : String
var items : [String]
}
typealias Wishlist = WishlistStruct
//MARK: - Defaults
let sharedUserdefaults = UserDefaults(suiteName: SharedDefault.suitName)
struct SharedDefault {
static let suitName = "yourAppGroupHere"
struct Keys{
static let WishlistKey = "WishlistKey"
}
}
var myWishlist: [Wishlist] {
get {
if let data = sharedUserdefaults?.data(forKey: SharedDefault.Keys.WishlistKey) {
let array = try! PropertyListDecoder().decode([Wishlist].self, from: data)
return array
} else{
//Here you should return an error but I didn't find any way to do that so I put this code which hopefully will never be executed
return sharedUserdefaults?.array(forKey: SharedDefault.Keys.WishlistKey) as? [Wishlist] ?? [Wishlist]()
}
} set {
}
}
Now, whenever you need to retrieve the struct, both in app and extension, use the following code:
var wishlist : [Wishlist] = []
var currentWishlist = myWishlist
//In your viewDidLoad call
wishlist.append(contentsOf: myWishlist)
To edit the data inside of your wishlist use the following code
wishlist.append(Wishlist(name: "wishlist", items: ["aaa","bbb","ccc"]))
currentWishlist.append(Wishlist(name: "wishlist", items: items: ["aaa","bbb","ccc"]))
if let data = try? PropertyListEncoder().encode(currentWishlist) {
sharedUserdefaults?.set(data, forKey: SharedDefault.Keys.WishlistKey)
}
Let me know if you need more clarifications
Updated code to your struct. You should change some type of properties to yours(i remove some field for test).
import UIKit
enum PublicState: String, Codable {
case PUBLIC
case PUBLIC_FOR_FRIENDS
case NOT_PUBLIC
}
struct Wishlist: Codable {
var id: String = ""
var name: String = ""
var image: Data = Data()//TODO: use Data type
var color: String = ""//TODO: change it to your class
// var wish: //TODO: add this filed, i don't have it
var textColor: String = "" //TODO: change it to your class
var index: Int = 0
var publicSate: PublicState = .PUBLIC
enum CodingKeys: String, CodingKey {
case id, name, image, color, textColor, index, publicSate
}
init() {}
init(id: String, name: String, image: Data, color: String, textColor: String, index: Int, publicSate: PublicState) {
self.id = id
self.name = name
self.image = image
self.color = color
self.textColor = textColor
self.index = index
self.publicSate = publicSate
}
}
struct WishlistContainer: Codable {
var list: [Wishlist] = []
enum CodingKeys: String, CodingKey {
case list
}
}
class UserDefaultsManager {
//be sure your correctly setup your app groups
private var currentDefaults: UserDefaults = UserDefaults(suiteName: "put here your app group ID")!
private func getFromLocalStorage<T: Codable>(model: T.Type, key: String) -> T? {
if let decoded = currentDefaults.object(forKey: key) as? String {
guard let data = decoded.data(using: .utf8) else { return nil }
if let product = try? JSONDecoder().decode(model.self, from: data) {
return product
}
}
return nil
}
private func saveToLocalStorage(key: String, encodedData: String) {
currentDefaults.set(encodedData, forKey: key)
}
private func removeObject(key: String) {
currentDefaults.removeObject(forKey: key)
}
var wishList: WishlistContainer? {
set {
guard let value = newValue else {
removeObject(key: "wishList")
return
}
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
guard let jsonData = try? encoder.encode(value) else { return }
guard let jsonString = String(data: jsonData, encoding: .utf8) else { return }
saveToLocalStorage(key: "wishList", encodedData: jsonString)
}
get {
guard let value = getFromLocalStorage(model: WishlistContainer.self, key: "wishList") else {
return nil
}
return value
}
}
}
//MARK: - Usage
let list: [Wishlist] = [Wishlist()]
let container: WishlistContainer = WishlistContainer(list: list)
UserDefaultsManager().wishList = container //set
UserDefaultsManager().wishList // get
Background:
I am creating a game with the option to buy/use power-ups. I want to pre-load these power-up objects into my CoreData database with a quantity of 0. The idea being that the user will buy power-ups and then the context of how many they own is saved in the database.
Problem:
My Codable objects are being generated with the properties all being nil or 0, i.e. not taking on the information provided by the JSON. Please can you help me see where I am going wrong.
My Codable conforming Class:
import Foundation
import CoreData
class PowerUp: NSManagedObject, Codable {
enum CodingKeys: String, CodingKey {
case name
case desc
case image
case quantity
}
var name: String?
var desc: String?
var image: String?
var quantity: Double?
required convenience init(from decoder: Decoder) throws {
guard let codingUserInfoKeyManagedObjectContext = CodingUserInfoKey.context,
let context = decoder.userInfo[codingUserInfoKeyManagedObjectContext] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName: "PowerUp", in: context) else {
fatalError("Failed to decode PowerUp")
}
self.init(entity: entity, insertInto: context)
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decodeIfPresent(String.self, forKey: .name)
self.desc = try container.decodeIfPresent(String.self, forKey: .desc)
self.image = try container.decodeIfPresent(String.self, forKey: .image)
self.quantity = try container.decodeIfPresent(Double.self, forKey: .quantity)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.desc, forKey: .desc)
try container.encode(self.image, forKey: .image)
try container.encode(self.quantity, forKey: .quantity)
}
}
public extension CodingUserInfoKey {
// Helper property to retrieve the context
static let context = CodingUserInfoKey(rawValue: "context")
}
My JSON (powerUpData.json):
[
{
"name": "Extra Time",
"desc": "Wind back the clock with an extra 30 seconds.",
"image": "sand-clock",
"quantity": 0.0
},
{
"name": "Voice Trade",
"desc": "Offload an asset to a friend for 10% more than originally paid.",
"image": "microphone",
"quantity": 0.0
}
]
My AppDelegate (where the decoding and pre-loading is done):
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
preloadPowerUps()
return true
}
// Skipped out non-edited, irrelevant AppDelegate functions for clarity...
func preloadPowerUps() {
guard let url = Bundle.main.url(forResource: "powerUpData", withExtension: "json") else { fatalError("no file") }
do {
let json = try Data(contentsOf: url)
print(json)
let decoder = JSONDecoder()
decoder.userInfo[CodingUserInfoKey.context!] = persistentContainer.viewContext
do {
let subjects = try decoder.decode([PowerUp].self, from: json)
print(subjects)
do {
try persistentContainer.viewContext.save()
} catch {
print("error")
}
} catch {
print("error")
}
} catch {
print("error")
}
}
What's more is that when debugging, my PowerUp objects do seem to be taking on the values of my json but also kind of not...
To summarize from the questions comments:
It's important to remember that CoreData still relies heavily on Objective-C. In your example code, the properties on your class, although expressible as CoreData attributes, the implementation is not being handled by CoreData.
You'll need to add the #NSManaged attribute to your properties like this:
#NSManaged var name: String?
#NSManaged var desc: String?
#NSManaged var image: String?
#NSManaged var quantity: Double?
This will expose them to Obj-C as dynamic and allow CoreData to handle the implementation. This would also help to explain your debugging, in that at runtime the print statement would show values, but the saved managed object had nil values.
I have a dictionary of values
class Objects {
let values = [
"AAA": ["AAAAAAA", "111111111"],
"BBB": ["BBBBBBBB", "2222222"],
"CCC": ["CCCCCCCC", "3333333333"],
"DDD": ["DDDDDD", "44444444"],
]
}
Which I turn into custom objects and display in a tableview.
struct Object {
var heading : String!
var imageName: String!
}
Then the user can select two objects to store in UserDefaults
let defaults = UserDefaults.standard
func addObject(_ object1: String, object2: String) {
// Get objects for user
var userObjects = fetchObjectsFromUserDefaults()
// Add to user currencies
userObjects.append([object1,object2])
//Update user defaults value for key
// [ [Object1, Object2], [Object1, Object2] ]
defaults.set(userObject, forKey: "userCurrencies")
}
// Gets [[String]] values from user defaults for key
func fetchObjectsFromUserDefaults() -> [[String]] {
if let objects = UserDefaults.standard.value(forKey: "userObjects") {
return objects as! [[String]]
} else {
return []
}
}
// Uses [[String]] values and turns them into objects by using the dictionary to determine property values
func getObject() -> [[Object]] {
let userObject = fetchObjectsFromUserDefaults()
// [ [Object1, Object2], [Object1, Object2] ]
let object = Object()
var fetchedObject = [[Object]]()
if !userObjects.isEmpty {
for c in userObjects {
var set = [Object]()
if let val = object.available[c[0]] {
set.append(Currency(currencyTitle: c[0], imageName: val[0] ))
}
if let val2 = object.available[c[1]] {
set.append(Currency(currencyTitle: c[0], imageName: val2[0] ))
}
if !set.isEmpty {
fetchedObjects.append(set)
}
}
return fetchedObjects
}
return [[]]
}
View Controller
Here I get the objects to load into the TableView
let fetched = dataManager.getObjects
print(fetched)
self.objects = fetched()
However this prints out
(Function)
What am I doing wrong and is their a better method of storing and retrieving this data from user defaults ? I feel this is over kill and there is a swifter and safer approach.
Step 1.
Make your struct Codable. The compiler will write all of the functions for you if all of the members of your struct are Codable and fortunately String is Codable so its just:
struct Object: Codable {
var heading : String!
var imageName: String!
}
Step 2.
The problem with Codable is that it converts to and from Data, but you want to convert to and from a Dictionary. Fortunately JSONSerialization converts from Data to Dictionary so make a new protocol and give it a default implementation with a protocol extension:
protocol JSONRepresentable {
init?(json: [String: Any])
func json() -> [String: Any]
}
extension JSONRepresentable where Self: Codable {
init?(json: [String:Any]) {
guard let value = (try? JSONSerialization.data(withJSONObject: json, options: []))
.flatMap ({ try? JSONDecoder().decode(Self.self, from: $0) }) else {
return nil
}
self = value
}
func json() -> [String:Any] {
return (try? JSONEncoder().encode(self))
.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) } as? [String: Any] ?? [:]
}
}
Step 3.
Conform your struct to JSONRepresentable
struct Object: Codable, JSONRepresentable {
var heading : String!
var imageName: String!
}
Step 4.
Place your object into Userdefaults and get it out again:
let o = Object.init(heading: "s", imageName: "a").json()
UserDefaults.standard.set(o, forKey: "test")
print(Object.init(json: UserDefaults.standard.dictionary(forKey: "test") ?? [:]))
Here is the whole playground if you want to try:
import UIKit
struct Object: Codable, JSONRepresentable {
var heading : String!
var imageName: String!
}
protocol JSONRepresentable {
init?(json: [String: Any])
func json() -> [String: Any]
}
extension JSONRepresentable where Self: Codable {
init?(json: [String:Any]) {
guard let value = (try? JSONSerialization.data(withJSONObject: json, options: []))
.flatMap ({ try? JSONDecoder().decode(Self.self, from: $0) }) else {
return nil
}
self = value
}
func json() -> [String:Any] {
return (try? JSONEncoder().encode(self))
.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) } as? [String: Any] ?? [:]
}
}
let o = Object.init(heading: "s", imageName: "a").json()
UserDefaults.standard.set(o, forKey: "test")
print(Object.init(json: UserDefaults.standard.dictionary(forKey: "test") ?? [:]))
Im trying to save struct array into UserDefaults and I cant figure out why JsonEncoder return empty data. I have setup model that conforms Codable protocol
struct MenuItem : Codable{
let name : String?
let icon : String?
init(name : String?, icon : String?){
self.name = name
self.icon = icon
}
}
and also created Defaults manager for saving it into user defaults.
class DefaultsManager {
static let shared = DefaultsManager()
init(){}
var items: [MenuItem]{
get{
if let json = UserDefaults.standard.data(forKey: "key"){
return decodeFromJson(jsonData: json)
} else {
return []
}
}
set{
let json = codeToJson(data: items)
UserDefaults.standard.set(json, forKey: "key")
}
}
fileprivate func codeToJson<T:Codable>(data: Array<T>) -> Data?{
do {
return try JSONEncoder().encode(data)
} catch {
print(error)
return nil
}
}
fileprivate func decodeFromJson<T:Codable>(jsonData: Data) -> [T]{
do {
return try JSONDecoder().decode(Array<T>.self, from: jsonData)
} catch {
print(error)
return []
}
}
}
but whatever I do JsonEncoder returns empty data.. I tried to google but without success.
That's a very common mistake.
In a setter of a computed property the new value is represented by the implicit newValue variable, it's not the property itself.
set {
let json = codeToJson(data: newValue)
UserDefaults.standard.set(json, forKey: "key")
}
Change Array<T>.self to [MenuItem].self
return try JSONDecoder().decode([MenuItem].self, from: jsonData)