How to store a dictionary [String:[Object]]? - ios

So I have a class called Restaurant and a [String:[Restaurant]] dictionary. How should I properly store this dictionary so that data inside it wouldn't be lost after user relaunches the app?
class Restaurant {
var name: String?
init?(name: String) {
guard !name.isEmpty else {
return nil
}
self.name = name
}
}
var restaurants = [String: [Restaurant]]()
override func viewDidLoad() {
super.viewDidLoad()
// ...
restaurants["Pizza"] = ...
}

Try using realm.io
If you want to conform both decodable and the realm object to create the object in disk you could do it as follow
import RealmSwift
class Restaurant: Object, Decodable {
#objc dynamic var name: String?
private enum CodingKeys: String, CodingKey { case name}
convenience init( name: String?){
self.init()
self.name = name
}
convenience required init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
}
}
func saveToDisk(){
let realm = try! Realm()
try! realm.write {
realm.add(restaurant)
}
}
You can adapt it to save an array of Restaurant Object
You can use other different methods as:
CoreData
Using a file
UserDefaults(too slow and not for large amount of data)

Related

Swift: Loading Codable Class into CoreData from JSON is generating Objects with all properties to nil

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.

Using an Object with ream codable

I have an application where I tried using UserDefaults to save a Codable model locally but the problem is that UserDefault does not store all the values of the Model and assigns nil to some of them and at times the proper value is assigned. So I decided to try an alternative which is realm and I am able to modify my model to work with realm but I have an issue not which is in the process of decoding an Object in an Object using realm. I was able to make it work when dealing with an Array object with List but non array Objects simply failed to map to JSON
below is a sample array that I am dealing with
{
"id": 732,
"name": "Vendor Name",
"logo": ".../thumb/missing.png",
"kitchens":
{
"id": 36,
"name": "Sandwiches"
}
}
model class
class VendorsList : Object, Decodable {
#objc dynamic var id : Int = 0
#objc dynamic var name : String?
#objc dynamic var logo : String?
// Create your Realm List.
var kitchensList = List<VendorKitchens>()
override class func primaryKey() -> String? {
return "id"
}
private enum CodingKeys: String, CodingKey {
case id
case name
case logo
// Set JSON Object Key
case kitchensList = "kitchens"
}
public required convenience init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
self.logo = try container.decode(String.self, forKey: .logo)
// Map your JSON Array response
let kitchens = try container.decodeIfPresent([VendorKitchens].self, forKey: .kitchensList) ?? [VendorKitchens()]
kitchensList.append(objectsIn: kitchens)
}
}
class VendorKitchens : Object, Decodable {
#objc dynamic var id : Int = 0
#objc dynamic var name : String?
override class func primaryKey() -> String? {
return "id"
}
private enum CodingKeys: String, CodingKey {
case id
case name
}
}
this returns an error
Failed to map data to JSON
Here, try this.
class VendorsList : Object, Decodable {
#objc dynamic var id : Int = 0
#objc dynamic var name : String?
#objc dynamic var logo : String?
#objc dynamic var kitchens: VendorKitchens? = nil
override class func primaryKey() -> String? {
return "id"
}
private enum CodingKeys: String, CodingKey {
case id
case name
case logo
case kitchens = "kitchens"
}
public required convenience init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
self.logo = try container.decode(String.self, forKey: .logo)
kitchens = try container.decodeIfPresent(VendorKitchens.self, forKey: .kitchensList)
}
}
class VendorKitchens : Object, Decodable {
#objc dynamic var id : Int = 0
#objc dynamic var name : String?
override class func primaryKey() -> String? {
return "id"
}
private enum CodingKeys: String, CodingKey {
case id
case name
}
}

Issue with Model class of Realm

I was going through some model classes of a project(Realm is being used). This is one class…
#objcMembers class CommA: Object {
dynamic var id = 0
dynamic var recipientId = "0"
dynamic var name: String?
dynamic var picture: String?
dynamic var unreadMessageCount = 0
dynamic var lastMessage: MyMessage?
override class func primaryKey() -> String? {
return "id"
}
}
This pretty much seems easy. A class with variables and a primary key defined..
But there is another class which looks like so…
#objcMembers class CommB: Object, Codable {
dynamic var id = "0"
dynamic var name: String?
dynamic var picture: String?
dynamic var status: String?
dynamic var lastSeen: String?
dynamic var unreadMessageCount = 0
dynamic var lastMessage: MyMessage?
enum CodingKeys: String, CodingKey {
case id = "UsrID"
case name = "UserName"
case picture = "UsrPicture"
case status = "ChatStatus"
}
required init() {
super.init()
}
required init(value: Any, schema: RLMSchema) {
super.init(value: value, schema: schema)
}
required init(realm: RLMRealm, schema: RLMObjectSchema) {
super.init(realm: realm, schema: schema)
}
convenience init(id: String, name: String, picture: String, status: String) {
self.init()
self.id = id
self.name = name
self.picture = picture
self.status = status
}
convenience required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let id = try container.decode(String.self, forKey: .id)
let name = try container.decode(String.self, forKey: .name)
let picture = try container.decode(String.self, forKey: .picture)
//let status = try container.decode(String.self, forKey: .status)
self.init(id: id, name: name, picture: picture, status: "status")
}
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(picture, forKey: .picture)
try container.encode(status, forKey: .status)
}
override class func primaryKey() -> String? {
return "id"
}
}
What I don’t understand here is why the enum, required init, convenience required init etc. is used…?
In fact, some of the initialisers are redundant. Your code can be shortened to
#objcMembers class CommB: Object, Codable {
dynamic var id = "0"
dynamic var name: String?
dynamic var picture: String?
dynamic var status: String?
dynamic var lastSeen: String?
dynamic var unreadMessageCount = 0
dynamic var lastMessage: MyMessage?
enum CodingKeys: String, CodingKey {
case id = "UsrID"
case name = "UserName"
case picture = "UsrPicture"
case status = "ChatStatus"
}
convenience init(id: String, name: String, picture: String, status: String) {
self.init()
self.id = id
self.name = name
self.picture = picture
self.status = status
}
convenience required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let id = try container.decode(String.self, forKey: .id)
let name = try container.decode(String.self, forKey: .name)
let picture = try container.decode(String.self, forKey: .picture)
//let status = try container.decode(String.self, forKey: .status)
self.init(id: id, name: name, picture: picture, status: "status")
}
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(picture, forKey: .picture)
try container.encode(status, forKey: .status)
}
override class func primaryKey() -> String? {
return "id"
}
}
I have removed
required init() {
super.init()
}
required init(value: Any, schema: RLMSchema) {
super.init(value: value, schema: schema)
}
required init(realm: RLMRealm, schema: RLMObjectSchema) {
super.init(realm: realm, schema: schema)
}
Everything else is quite important.
CommB is not only a realm object, but also Codable. And the author of it wanted to customise the de/encoding behaviour so that the de/encoder only de/encodes id, name, picture and status. To do this, a CodingKey enum needs to be created, storing the coding keys. Also, convenience required init(from decoder: Decoder) and func encode(to encoder: Encoder) needs to be implemented.
The convenience init(id: String, name: String, picture: String, status: String) initialiser is there because the init(from decoder: Decoder) delegates to it.
To learn more about how Codable works, visit here.

How to implement Codable while using Realm

Hy I am working on app that uses Realm and Alamofire. I am really happy in using these library in my iOS project.
But then I have to post a List of models that contains multiple lists of models. So that is too much deep thing I mean List inside List that contains models and those model contains list of several model
For demonstration lets just take an example of my models
#objcMembers public class MyModel : Object{
dynamic var Id: String = ""
dynamic var Name: String = ""
dynamic var Favorites: List<String>? = nil
dynamic var Subjects: List<UserSubject>? = nil
}
#objcMembers public class UserSubject: Object{
dynamic var Id: String = ""
dynamic var Name: String = ""
dynamic var Teachers: List<Publications>? = nil
}
#objcMembers public class Publications: Object{
dynamic var Id: String = ""
dynamic var Name: String = ""
dynamic var Editors: List<Editors>? = nil
}
So you can see these are models inside list that contains another list of model.
Due to Realm I am using List for list for creating the RelationShip.
Problem: but Now when I tries to implement Codable on Models/Struct It really unable to recognize List property.
I really do not know how to solve this problem? Do anyone have any Idea how to do it ?
UPDATE:
I am using Swift 4.0 and base sdk is 11.2
Had the same problem in a project and wrote these extensions:
import Foundation
import RealmSwift
extension RealmSwift.List: Decodable where Element: Decodable {
public convenience init(from decoder: Decoder) throws {
self.init()
let container = try decoder.singleValueContainer()
let decodedElements = try container.decode([Element].self)
self.append(objectsIn: decodedElements)
}
}
extension RealmSwift.List: Encodable where Element: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.map { $0 })
}
}
With these extension you easily can make the Realm Object Codable. Like this
#objcMembers public class MyModel: Object, Codable {
dynamic var id: String = ""
dynamic var name: String = ""
var favorites = List<String>()
var subjects = List<UserSubject>()
}
#objcMembers public class UserSubject: Object, Codable {
dynamic var id: String = ""
dynamic var name: String = ""
var teachers = List<Publications>()
}
#objcMembers public class Publications: Object, Codable {
dynamic var id: String = ""
dynamic var name: String = ""
var editors = List<Editor>()
}
#objcMembers public class Editor: Object, Codable {
}
I can suggest you use Unrealm.
It's a powerful library which enables you to save Swift native types (structs, enums, Arrays, Dictionaries) into Realm database. So you don't have to worry about Lists and Codable compatibility anymore.
An example model with Codable implementation
You can use extensions for List
Swift 4.1
extension List: Decodable where Element: Decodable {
public convenience init(from decoder: Decoder) throws {
// Initialize self here so we can get type(of: self).
self.init()
assertTypeIsDecodable(Element.self, in: type(of: self))
let metaType = (Element.self as Decodable.Type) // swiftlint:disable:this force_cast
var container = try decoder.unkeyedContainer()
while !container.isAtEnd {
let element = try metaType.init(__from: &container)
self.append(element as! Element) // swiftlint:disable:this force_cast
}
}
}
extension List: Encodable where Element: Decodable {
public func encode(to encoder: Encoder) throws {
assertTypeIsEncodable(Element.self, in: type(of: self))
var container = encoder.unkeyedContainer()
for element in self {
// superEncoder appends an empty element and wraps an Encoder around it.
// This is normally appropriate for encoding super, but this is really what we want to do.
let subencoder = container.superEncoder()
try (element as! Encodable).encode(to: subencoder) // swiftlint:disable:this force_cast
}
}
}
For these using Swift 5.x and XCode 13.x.x (i have 13.3.1) and RealmSwift (10.25.1):
Realm with Codable (Encode/Decode) (2 class for example)
import Foundation
import RealmSwift
/*
* Hold info about user profile
*/
class Profile: Object, Codable {
#Persisted(primaryKey: true) var _id: String
#Persisted var firstName: String
#Persisted var lastName: String
#Persisted var email: String
#Persisted var role: String
// Relations
#Persisted var session: Session?
#Persisted var companies: List<Company>
// MARK: Codable support
enum CodingKeys: String, CodingKey {
case email, companies
case id = "_id"
case firstName, lastName, role
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(_id, forKey: .id)
try container.encode(firstName, forKey: .firstName)
try container.encode(lastName, forKey: .lastName)
try container.encode(email, forKey: .email)
try container.encode(role, forKey: .role)
try container.encode(companies, forKey: .companies)
}
required init(from decoder: Decoder) throws {
super.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
_id = try container.decode(String.self, forKey: .id)
firstName = try container.decode(String.self, forKey: .firstName)
lastName = try container.decode(String.self, forKey: .lastName)
email = try container.decode(String.self, forKey: .email)
role = try container.decode(String.self, forKey: .role)
let companiesList = try container.decode([Company].self, forKey: .companies)
companies.append(objectsIn: companiesList)
}
}
Other example:
import Foundation
import RealmSwift
/*
* Hold info about user session
*/
class Session: Object, Codable {
#Persisted(primaryKey: true) var _id: String
#Persisted(indexed: true) var accessToken: String
#Persisted var refreshToken: String
#Persisted var tokenType: String
#Persisted var expiresIn: Double
// Relations
#Persisted var profile: Profile?
// MARK: Codable support
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case tokenType = "token_type"
case expiresIn = "expires_in"
case refreshToken = "refresh_token"
case id = "_id"
case v = "__v"
case profile
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(_id, forKey: .id)
try container.encode(accessToken, forKey: .accessToken)
try container.encode(refreshToken, forKey: .refreshToken)
try container.encode(tokenType, forKey: .tokenType)
try container.encode(expiresIn, forKey: .expiresIn)
try container.encode(profile, forKey: .profile)
}
required init(from decoder: Decoder) throws {
super.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
_id = try container.decode(String.self, forKey: .id)
accessToken = try container.decode(String.self, forKey: .accessToken)
refreshToken = try container.decode(String.self, forKey: .refreshToken)
tokenType = try container.decode(String.self, forKey: .tokenType)
expiresIn = try container.decode(Double.self, forKey: .expiresIn)
profile = try container.decode(Profile.self, forKey: .profile)
}
}
You can encode List from realm with this code, for example:
try container.encode(companies, forKey: .companies)
and to decode:
let companiesList = try container.decode([Company].self, forKey: .companies)
companies.append(objectsIn: companiesList)
This is only and example, you can adapt to your need's.
And finally for example when you get data from network (i'm using Moya):
extension Session {
init(data: Data) throws {
self = try JSONDecoder().decode(Session.self, from: data)
}
}
self.xxApi.request(.login(username: "user#domain.com", password: "HiTh3r3.2022")) { result in
switch result {
case let .success(response):
guard let session = try? Session(data: response.data) else {
print("Can't parse session data: \(JSON(response.data))")
return
}
// Request parsed so work with data here
print(session)
case let .failure(error):
print(error)
}
}
Try this:
extension List: Decodable where Element: Decodable {
public convenience init(from decoder: Decoder) throws {
self.init()
var container = try decoder.unkeyedContainer()
while !container.isAtEnd {
let element = try container.decode(Element.self)
self.append(element)
}
}
}
extension List: Encodable where Element: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
for element in self {
try element.encode(to: container.superEncoder())
}
}
}
Found it here: How to use List type with Codable? (RealmSwift)

Unable to access codable struct values in Swift

I am fetching user's data as json from a service and decoding into Codable User struct. I can access that property where I've fetched the response but I want to access that User struct property somewhere else, let's say in another class, function, etc.
I'm new to this and I'm thinking the ideal approach is to "at the time of fetching, store that data into Core Data or User Defaults and then update my views accordingly.
Please suggest what's the best and appropriate approach or wether there is any way to access codable struct values directly.
Here is my codable struct -
struct User: Codable {
let name : String?
enum CodingKeys: String, CodingKey {
case name = "Name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decodeIfPresent(String.self, forKey: .name)
}
}
The wrong way I'm accessing struct in some function is -
UserInfo.CodingKeys.name.rawValue
//Output is the key string - 'Name'
I think static can help you
struct User: Codable {
let name : String?
private enum CodingKeys: String, CodingKey {
case name = "Name"
}
}
assume fetching data here
class FetchingClass{
static var user: User?
func fetchData(){
//After Url request success
do {
//assign directly to static varibale user
FetchingClass.user = try JSONDecoder().decode(User.self, from: data)
} catch _ {
}
}
}
use like this wherever you want without using coreData or UserDefaults
class AnotherClass{
func anotherClassFunc(){
//use if let or guard let
if let user = FetchingClass.user{
print(user.name)
}
//or
if let FetchingClass.user != nil {
print(FetchingClass.name)
}
}
}
You can try using a singleton reference
struct User: Codable {
let name : String?
enum CodingKeys: String, CodingKey {
case name = "Name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decodeIfPresent(String.self, forKey: .name)
User.shared = self
}
}
Create another class which hold the ref. of User
final class AppGlobalManager {
static let sharedInstance = AppGlobalManager()
var currentUser: User? // Initialise it when user logged in
private init()
}
Then you can access any of the user data as
AppGlobalManager.sharedInstance.currentUser.name

Resources