Implementing Codable and NSManagedObject simultaneously in Swift - swift

I have an order processing application I'm working on for my employers that was originally designed to get all data about orders, products, and clients dynamically from the API. So all of the objects and and all of the functions dealing with those objects are interacting in the app with a "pass by value" expectation, utilizing structs conforming to Codable.
I now have to cache pretty much all of these objects. Enter CoreData.
I have no desire to create two files for one object(one as a Codable struct and the other as an NSManagedObject class) and then trying to figure out how to convert one to another. So I want to implement both in the same file...while still somehow being able to use my "pass by value" code.
Perhaps this is impossible.
edit
I'm looking for something a bit simpler than rebuilding all my data structures from the ground up. I understand I'll have to do some alterations to make a Codable struct compatible with a NSManagedObject class. I'd like to avoid making a custom initializer that requires me to enter in every property by hand, because there's hundreds of them.

In the end, it sounds like there is no "good" solution when migrating from an API dynamic app without caching to a cached app.
I decided to just bite the bullet and try the method in this Question: How to use swift 4 Codable in Core Data?
EDIT:
I couldn't figure out how to make that work so I used the following solution:
import Foundation
import CoreData
/*
SomeItemData vs SomeItem:
The object with 'Data' appended to the name will always be the codable struct. The other will be the NSManagedObject class.
*/
struct OrderData: Codable, CodingKeyed, PropertyLoopable
{
typealias CodingKeys = CodableKeys.OrderData
let writer: String,
userID: String,
orderType: String,
shipping: ShippingAddressData
var items: [OrderedProductData]
let totals: PaymentTotalData,
discount: Float
init(json:[String:Any])
{
writer = json[CodingKeys.writer.rawValue] as! String
userID = json[CodingKeys.userID.rawValue] as! String
orderType = json[CodingKeys.orderType.rawValue] as! String
shipping = json[CodingKeys.shipping.rawValue] as! ShippingAddressData
items = json[CodingKeys.items.rawValue] as! [OrderedProductData]
totals = json[CodingKeys.totals.rawValue] as! PaymentTotalData
discount = json[CodingKeys.discount.rawValue] as! Float
}
}
extension Order: PropertyLoopable //this is the NSManagedObject. PropertyLoopable has a default implementation that uses Mirror to convert all the properties into a dictionary I can iterate through, which I can then pass directly to the JSON constructor above
{
convenience init(from codableObject: OrderData)
{
self.init(context: PersistenceManager.shared.context)
writer = codableObject.writer
userID = codableObject.userID
orderType = codableObject.orderType
shipping = ShippingAddress(from: codableObject.shipping)
items = []
for item in codableObject.items
{
self.addToItems(OrderedProduct(from: item))
}
totals = PaymentTotal(from: codableObject.totals)
discount = codableObject.discount
}
}

Related

How to store properties in generic struct?

I have a Codable struct that is part of my app, RemoteData. I’m building a reusable package that will fetch the data and store it in UserDefaults. The data fetching, DataFetcher class has a Codable generic parameter. I am subclassing DataFetcher to pass in RemoteData as the generic param.
// in my app
struct RemoteData: Codable {
var experimentOne: [Variant<[Page]>]
var experimentTwo: [Variant<Bool>]
var experimentThree: [Variant<String>]
}
All of the properties in RemoteData will be arrays of type Variant<T> where T is Codable:
// in my package
public struct Variant<T: Codable>: Codable, VariantProtocol {
public var experimentName: String
public var variantName: String
public var percent: Int
public var value: T
}
I’d like to be able to save this data in UserDefaults. I’d like to perform some filtering on the Variant array to see if this user should see that configuration. I’d like to save the data so that each experiment name is the key and the single variant the user should see is the value rather than the whole array. Although if the whole array is the only option, I’d be ok with that too.
However, since my DataFetcher doesn’t know what the properties are since it is just taking in a generic I don’t think I can do that. My first thought was to create a protocol that RemoteConfig confirms to and that the DataFetcher generic also conforms to.
// in my package, but subclassing in my app to provide url
open class DataFetcher<T: Decodable> {
var remoteConfig: T?
var url: URL
public init(url: String) {
self.url = url
}
func fetchAndSaveData() { ... }
}
That doesn’t work because I then need to specify T in Variant and I will only be able to have Variant arrays of one type.
I’m stuck here and not sure how to move forward.

SwiftUI ForEach Bindable List errors

I would appreciate help with SwiftUI bindable lists.
I read the following article and tried it on my app but I'm getting errors.
https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/
First, the following View including the non-bindable ordinary ForEach list works fine without any errors
#ObservedObject var notificationsViewModel = NotificationsViewModel.shared
//NotificationsViewModel does a API call and puts the fetched data in the Notifications Model
var body: some View {
VStack {
ForEach(notificationsViewModel.notifications?.notificationsDetails ?? [NotificationsDetail]()) { notificationsDetail in
---additional code here--- }
}
Model below:
struct Notifications: Codable, Identifiable {
let id = UUID()
let numberOfNotifications: Int
var notificationsDetails: [NotificationsDetail]
enum CodingKeys: String, CodingKey {
case numberOfNotifications = "number_of_notifications"
case notificationsDetails = "notifications"
}
}
struct NotificationsDetail: Codable, Identifiable, Equatable {
let id: Int
let notificationsCategoriesId: Int
let questionsUsersName: String?
enum CodingKeys: String, CodingKey {
case id = "notifications_id"
case notificationsCategoriesId = "notifications_categories_id"
case questionsUsersName = "questions_users_name"
}
}
When I try to change this ForEach to a bindable one, I start getting multiple errors.
ForEach($notificationsViewModel.notifications?.notificationsDetails ?? [NotificationsDetail]()) { $notificationsDetail in
---additional code here using $notificationsDetail---}
When I try to fix some of the errors such as "remove ?", I get a new error saying I need to add the ?.
When I delete the default value ?? NotificationsDetail, I still get errors
The Xcode build version is iOS15.
Does anyone know how to fix this? Thanks in advance.
ForEach($notificationsViewModel.notifications?.notificationsDetails ?? [NotificationsDetail]()
Is confusing the type system, because one side of the ?? is a binding to an array of values, and the other side is an array of values. There's also an optional in your key path to make things more complicated.
Try to rearrange the NotificationsViewModel type so that it just surfaces a non-optional array instead of having all this optional mess at the view level. Is it really meaningful to have an optional notification property, or can an empty one be used instead? Do you need the separate notifications struct? Are you just modelling your data types directly from API responses? Perhaps you can make changes to your model types to make them easier to work with?

Swift Core Data - storing an array of custom types

I am trying to create a data model which mirrors a view model that I use to handle an API call, the idea being that I will be able to store all the necessary data in core data and then access it when the user is offline, effectively giving the app offline functionality.
However, there is one entity which I need to store which is an array of a custom class that I have in the app:
[OrderSheet]
This is a struct defined as follows:
struct OrderSheet {
let order: SheetClass // codable class
let sheet: Sheet // codable struct
init(fuelOrder: SheetClass, sheet: Sheet) {
self.order = order
self.sheet = sheet
}
}
How can I create an entity that would be capable of storing the above?
One simple way would be to have an entity that holds only one Data field (Binary Data in xcdatamodel settings), which would be the orderSheet itself.
Before going with this solution, I'd like to mention that, one down side of this approach is; if later in the future, any of the models inside OrderSheet changes, you won't be able to retrieve already stored objects as conversion will fail. One way of overcoming this issue would be declaring everything inside OrderSheet and sub models as Optional. But if it is not so crucial, meaning, if not being able to read old models on user's device after an app update is okay, (maybe they will be replaced with new networking call) then you can go with not marking properties as optional either.
Lets imagine you create an entity named OrderSheetManaged with one field as I mentioned like following:
import Foundation
import CoreData
#objc(Entity)
public class OrderSheetManaged: NSManagedObject {
}
extension OrderSheetManaged {
#nonobjc public class func fetchRequest() -> NSFetchRequest<OrderSheetManaged> {
return NSFetchRequest<OrderSheetManaged>(entityName: "OrderSheetManaged")
}
#NSManaged public var orderSheet: Data?
}
I will write some code for NSManagedObjectContext, which is not directly related to your question, you should make research on how to initialise a core data stack and a managed context from it if you are not familiar with that since it is crucial.
I also do some force unwrapping for simplicity, make sure to not force unwrap where not needed in production code.
Now whenever you have an actual OrderSheet object (it is orderSheet in my example below), that was parsed before, you are going to convert it to Data and persist it with new Core Data model as following:
// unrelated to question, it should already be initialised from core data stack, I just init with
// concurrency type to make compiler happy, dont do this before further research.
let yourManagedContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let entityDescription = NSEntityDescription.entity(forEntityName: "OrderSheetManaged",
in: yourManagedContext)
let dataForCoreData = try! JSONEncoder().encode(orderSheet)
let managedOrderSheet = NSManagedObject(entity: entityDescription!, insertInto: yourManagedContext)
managedOrderSheet.setValue(dataForCoreData, forKey: "orderSheet")
Now we have persisted your object as Data inside a wrapper core data model (OrderSheetManaged)
Let's see now how we can fetch these models from our core data and convert it back to OrderSheet model:
// when you fetch it
var orderSheets = [OrderSheet]()
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "OrderSheetManaged")
var coreDataObjects: [NSManagedObject]!
do {
coreDataObjects = try yourManagedContext.fetch(request) as? [NSManagedObject]
for coreDataObject in coreDataObjects {
if let orderSheetData = coreDataObject.value(forKey: "orderSheet") as? Data {
let orderSheet = try! JSONDecoder().decode(OrderSheet.self, from: orderSheetData)
orderSheets.append(orderSheet)
}
}
} catch {
error
}
Now you will have all your stored order sheets inside orderSheets array.
You can also write some utility methods to easily modify core data models by converting orderSheet data inside of them to OrderSheet first and then again converting it back to Data after modifying and then persisting again with setValue.

Using Swift 4 Codable Protocol with Unknown Dictionary Keys

I am working with NASA's Near Earth Object Web Service to retrieve data to be displayed in an application. I understand how to use Swift 4's Codable protocol, but I do not understand how to map part of the response.
Using Paw, I inspected the response from the API:
As you can see, the near_earth_objects structure is a Dictionary, and the keys are dates. The issue is that the URL parameters are dates, so these date structures will change, depending on the day of the request. Therefore, I do not know how I can create properties to be automatically mapped when using the Codable protocol.
The data that I am trying to get to inside of these structures are Arrays that contain Dictionarys:
How can I have my model object conform to the Codable protocol and map these structures when the dates will change as the dates of the requests change?
You don't need to know the keys of the Dictionary compile time if you don't mind keeping a Dictionary after decoding.
You just need to specify the property with type Dictionary<String:YourCustomDecodableType>. The keys will be dates corresponding to observation and the value will an array of all objects with your custom type.
struct NearEarthObject: Codable {
let referenceID:String
let name:String
let imageURL:URL
private enum CodingKeys: String, CodingKey {
case referenceID = "neo_reference_id"
case name
case imageURL = "nasa_jpl_url"
}
}
struct NEOApiResponse: Codable {
let nearEarthObjects: [String:[NearEarthObject]]
private enum CodingKeys: String,CodingKey {
case nearEarthObjects = "near_earth_objects"
}
}
do {
let decodedResponse = try JSONDecoder().decode(NEOApiResponse.self, from: data)
} catch {
print(error)
}
As you said, near_earth_objects is a Dictionary, but keys are not Dates, keys are Strings, and values are arrays of the known structures. So the above code will work:
...
let nearEarthObjects: [String: [IndexObject]]
...
enum CodingKey: String, CodingKeys {
case nearEarthObjects = "near_earth_objects"
}
struct IndexObject: Decodable {
...
let name: String
...
}

deep copy for array of objects in swift

I have this class named Meal
class Meal {
var name : String = ""
var cnt : Int = 0
var price : String = ""
var img : String = ""
var id : String = ""
init(name:String , cnt : Int, price : String, img : String, id : String) {
self.name = name
self.cnt = cnt
self.price = price
self.img = img
self.id = id
}
}
and I have an array of Meal :
var ordered = [Meal]()
I want to duplicate that array and then do some changes to the Meal instances in one of them without changing the Meal instances in the second one, how would I make a deep copy of it?
This search result didn't help me
How do I make a exact duplicate copy of an array?
Since ordered is a swift array, the statement
var orderedCopy = ordered
will effectively make a copy of the original array.
However, since Meal is a class, the new array will contain references
to the same meals referred in the original one.
If you want to copy the meals content too, so that changing a meal in one array will not change a meal in the other array, then you must define Meal as a struct, not as a class:
struct Meal {
...
From the Apple book:
Use struct to create a structure. Structures support many of the same behaviors as classes, including methods and initializers. One of the most important differences between structures and classes is that structures are always copied when they are passed around in your code, but classes are passed by reference.
To improve on #Kametrixom answer check this:
For normal objects what can be done is to implement a protocol that supports copying, and make the object class implements this protocol like this:
protocol Copying {
init(original: Self)
}
extension Copying {
func copy() -> Self {
return Self.init(original: self)
}
}
And then the Array extension for cloning:
extension Array where Element: Copying {
func clone() -> Array {
var copiedArray = Array<Element>()
for element in self {
copiedArray.append(element.copy())
}
return copiedArray
}
}
and that is pretty much it, to view code and a sample check this gist
You either have to, as #MarioZannone mentioned, make it a struct, because structs get copied automatically, or you may not want a struct and need a class. For this you have to define how to copy your class. There is the NSCopying protocol which unifies that on the ObjC world, but that makes your Swift code "unpure" in that you have to inherit from NSObject. I suggest however to define your own copying protocol like this:
protocol Copying {
init(original: Self)
}
extension Copying {
func copy() -> Self {
return Self.init(original: self)
}
}
which you can implement like this:
class Test : Copying {
var x : Int
init() {
x = 0
}
// required initializer for the Copying protocol
required init(original: Test) {
x = original.x
}
}
Within the initializer you have to copy all the state from the passed original Test on to self. Now that you implemented the protocol correctly, you can do something like this:
let original = Test()
let stillOriginal = original
let copyOriginal = original.copy()
original.x = 10
original.x // 10
stillOriginal.x // 10
copyOriginal.x // 0
This is basically the same as NSCopying just without ObjC
EDIT: Sadly this yet so beautiful protocol works very poorly with subclassing...
A simple and quick way is to map the original array into the new copy:
let copyOfPersons: [Person] = allPersons.map({(originalPerson) -> Person in
let newPerson = Person(name: originalPerson.name, age: originalPerson.age)
return newPerson
})
The new Persons will have different pointers but same values.
Based on previous answer here
If you have nested objects, i.e. subclasses to a class then what you want is True Deep Copy.
//Example
var dogsForAdoption: Array<Dog>
class Dog{
var breed: String
var owner: Person
}
So this means implementing NSCopying in every class(Dog, Person etc).
Would you do that for say 20 of your classes? what about 30..50..100? You get it right? We need native "it just works!" way. But nope we don't have one. Yet.
As of now, Feb 2021, there is no proper solution of this issue. We have many workarounds though.
Here is the one I have been using, and one with less limitations in my opinion.
Make your class conforms to codable
class Dog: Codable{
var breed : String = "JustAnyDog"
var owner: Person
}
Create this helper class
class DeepCopier {
//Used to expose generic
static func Copy<T:Codable>(of object:T) -> T?{
do{
let json = try JSONEncoder().encode(object)
return try JSONDecoder().decode(T.self, from: json)
}
catch let error{
print(error)
return nil
}
}
}
Call this method whenever you need true deep copy of your object, like this:
//Now suppose
let dog = Dog()
guard let clonedDog = DeepCopier.Copy(of: dog) else{
print("Could not detach Dog")
return
}
//Change/mutate object properties as you want
clonedDog.breed = "rottweiler"
//Also clonedDog.owner != dog.owner, as both the owner : Person have dfferent memory allocations
As you can see we are piggy backing on Swift's JSONEncoder and JSONDecoder, using power of Codable, making true deep copy no matter how many nested objects are there under our object. Just make sure all your Classes conform to Codable.
Though its NOT an ideal solution, but its one of the most effective workaround.