Persistence of array of custom objects - json

I'm developing an application that will search through a database of recipes in json format (see eg. of one entry below) and return valid recipes that a user can make based on what ingredients they have at their disposal. I represent a recipe in the following struct:
struct Recipe: Codable, Identifiable, Hashable {
// required because Recipe conforms to Identifiable
var id = UUID()
var isFavorite: Bool = false
var isTracked: Bool = false
var isComplete: Bool = false
// properties
var title: String
// detailed ingredients (eg. 1/2 tbs of sugar)
var ingredients: [String]
var directions: [String]
var link: String
var source: String
// raw ingredients (eg. sugar)
var NER: [String]
// all properties that are in the json file everything else will be ignored i.e decoder will work even though id is not a property in the json file
private enum CodingKeys: String, CodingKey { case title, ingredients, directions, link, source, NER } }
Where I use the variables isTracked, isComplete and isFavorite in order to display certain recipes in certain lists (eg. view to display the favorite recipes of a user). I'm following the MVVM design pattern and my view model RecipeData is as follows:
class RecipeData: ObservableObject {
#Published var recipes = [Recipe]()
#Published var usersRecipes = [Recipe]()
.
.
.
.
private var documentDirectory: URL {
try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
}
// CODE TO SAVE AND LOAD USERS TRACKED RECIPES
private var usersRecipesFile: URL {
documentDirectory
.appendingPathComponent("usersRecipes")
.appendingPathExtension(for: .json)
}
func saveUsersRecipes() throws {
let data = try JSONEncoder().encode(usersRecipes)
try data.write(to: usersRecipesFile)
}
func loadUsersRecipes() throws {
guard FileManager.default.isReadableFile(atPath: usersRecipesFile.path) else { return }
let data = try Data(contentsOf: usersRecipesFile)
usersRecipes = try JSONDecoder().decode([Recipe].self, from: data)
}
The first view that gets displayed when the app is loaded is MainTabView which is:
struct MainTabView: View {
// view model that allows us to interact between views and the models
#StateObject var recipeData: RecipeData = RecipeData()
var body: some View {
// View that will allow the user to choose between a HomePageView and a FavoritesViews
TabView {
HomeScreenView()
.tabItem { Label("Home", systemImage: "house") }
// FAVORITES VIEW CONTENT
NavigationView {
FavoritesView()
.navigationTitle("Favorites")
}
.tabItem { Label("Favorites", systemImage: "heart.fill") }
}
.environmentObject(recipeData)
// when the main tab view is shown load the users recipes
.onAppear {
// try! means if an error occurs crash the entire app
try! recipeData.loadUsersRecipes()
}
// when the users recipes changes save the recipes
.onChange(of: recipeData.usersRecipes) { _ in
try! recipeData.saveUsersRecipes()
}
}
}
So when the view is displayed the users recipes will be loaded and when the usersRecipes array is changed all changes will be saved. The problem I have is that when I append an element to the usersRecipes array through the recipeData object like this for eg.
Button(action: {
recipeData.usersRecipes.append(Recipe(isFavorite: false, isTracked: true, isComplete: false, title: recipe.title, ingredients: recipe.ingredients, directions: recipe.directions, link: recipe.link, source: recipe.source, NER: recipe.NER))
}) {
Text("Track Recipe")
.font(.system(.body))
}
(Where recipe is one of the valid recipes and the user wants to track is i.e isTracked = true) I get the recipe displayed in the list that only displays tracked recipes (as expected), however when I close the app and reopen, the tracked recipes list is empty. When I try to debug I see that the recipe itself is still persistent however its isTracked variable (along with isComplete and isFavorite) are all set to false which is how I defined them in the struct, rather than the value I gave them when I appended the object to the usersRecipes array. The weird thing is that all other fields like the title, link, ingredients, etc. are all still persistent which leads me to think that the way I defined the 3 booleans in the struct is whats causing the issue. I cannot not initialize them to a boolean since I'll get an error saying that Recipe does not conform to Decodable and I cannot place them in the CodingKeys enum since the json file doesn't include the fields so I get a Decoding error. I also tried to use optional booleans and not initializing them to anything but I get a similar behavior where they get reset to nil once the app is closed and opened.
JSON ENTRY EG.
{
"": 0,
"title": "No-Bake Nut Cookies",
"ingredients": [
"1 c. firmly packed brown sugar",
"1/2 c. evaporated milk",
"1/2 tsp. vanilla",
"1/2 c. broken nuts (pecans)",
"2 Tbsp. butter or margarine",
"3 1/2 c. bite size shredded rice biscuits"
],
"directions": [
"In a heavy 2-quart saucepan, mix brown sugar, nuts, evaporated milk and butter or margarine.",
"Stir over medium heat until mixture bubbles all over top.",
"Boil and stir 5 minutes more. Take off heat.",
"Stir in vanilla and cereal; mix well.",
"Using 2 teaspoons, drop and shape into 30 clusters on wax paper.",
"Let stand until firm, about 30 minutes."
],
"link": "www.cookbooks.com/Recipe-Details.aspx?id=44874",
"source": "Gathered",
"NER": [
"brown sugar",
"milk",
"vanilla",
"nuts",
"butter",
"bite size shredded rice biscuits"
] }
Thank you so much in advance I've really been struggling on this for a while and would appreciate any help. Sorry if the question is too long I couldn't think of another way to explain it without showing the different files.

Related

How to add a boolean variable for each item in a list SwiftUI

I have a menu list which has a list of items. I have a button "Order this" attached to each item. To prevent a user ordering the same menu multiple times (yes, they are not supposed to order more than one...) I want to set a boolean variable for each item. When they click "Order this", the boolean changes to true and the button changes to a text "menu ordered".
My initial attempt was to use an EnvironmentObject var:
class MenuState: ObservableObject {
#Published var items = [MenuItem]()
var mstate: Bool = false }
However, when I do this, the boolean is not assigned to each item in the list. As a result, all items in the list change when clicked.
Assigning the variable in the menu struct seems to be the solution, but I have made the items using json, which is immutable.
So, how could I attach boolean var for each item when the items are created using json?
struct MenuItem: Codable, Equatable, Identifiable {
var id: UUID
var name: String}
[
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A66",
"name": "Dish",
"items": [
{
"id": "36A7CC40-18C1-48E5-BCD8-3B42D43BEAEE",
"name": "Stack-o-Pancakes",
}]
You should probably add your boolean as a transient property of each MenuItem - the way you have it, the mstate refers to the entire menu:
struct MenuItem: Codable, Equatable, Identifiable {
var id: UUID
var name: String
var isOrdered: Bool?
}
There are a few ways to add a property to MenuItem and prevent it from being decoded: I made isOrdered an optional in the code here, but a better solution would be manually decoding the MenuItem objects, like in this answer.
Another option would be to have an array or dictionary of Bool values on your MenuState, to associate Bool values to a specific MenuItem.

How to decode a nested JSON file into a swift class with an unowned property

I have a custom model with several classes that I've written in Swift for a document-based Mac app. As the user inputs data to their document, instances of one class Itinerary are created and stored within an array which is a property of another "root" object of a different class Course. On top of that, multiple courses can be created and are stored in an array in the app’s Document class. I'd like the user to be able to save their data and load it back up again at a later point. So I'm attempting to implement the Codable protocol on my custom types.
The Encodable conformance works fine; when I attempt to save, the desired JSON file is produced. The problem comes when decoding.
When attempting to decode each itinerary, its course property needs to be set before the end of initialization because it was defined as an unowned var that points back to the course it belong's to. The place where that happens is within the course's addItinerary(_:) func, which is called from directly within the Itinerary's designated initializer. That means that I also need to pass a Course to the Itinerary's designated init. Therein lies the problem...when I try to reinitialize an itinerary from within the decodable initializer I don't have access to the course that the itinerary needs to be added to and so can't set the course property during initialization.
The issue seems to revolve around how I am attempting to initialize itineraries along with how I am attempting to manage the memory. A little research has pointed me in the direction of changing the course property either to an optional weak var, i.e. weak var course: Course? or marking it as an implicitly unwrapped optional, i.e. var course: Course!. Both of those options seem as though they may be introducing more complexity into the model than is necessary because of how I'll need to rewrite the Itinerary initializers. According to Apple's docs, it's appropriate to mark a property as unowned when the other instance has the same lifetime or a longer lifetime, which accurately describes the scenario here.
My question is sort of two-fold; there seems to be an inherent flaw with how I am attempting to initialize my model:
If not, how would I go about reassembling my model when decoding? Do I need to reconsider my initialization logic?
If so, what is the flaw? Am I using the right ARC strategy to manage the memory? Should I be considering a data structure other than JSON?
In the interest of brevity I excluded all the properties from the classes that seemed irrelevant. But I'll include more code if people feel it would be necessary to offer a complete solution.
Cliff notes of the important points:
the desired JSON is produced w/ out encoding the course property of the Itinerary class.
course's addItinerary(_:) func is when the itinerary's course property is set...it's called within Itinerary's designated init.
BUT...I can't call that init while decoding because I never encoded a course...when I try to encode a course in the Itinerary's encode(to:), Xcode throws BAD ACCESS errors and points to an address in memory that I am unsure of how to debug.
class Document: NSDocument {
var masterCourseList = [Course]()
}
class Course: NSObject {
// ...other properties
private(set) var itineraries: [Itinerary]() {
didSet {
// sorting code
}
}
func addItinerary(_ itinerary: Itinerary) {
itinerary.course = self
self.itineraries.append(itinerary)
}
// encodable conformance
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
// ...encode other properties
try container.encode(itineraries, forKey: .itineraries)
}
// decodable conformance
required convenience init decode(from: Decoder) throws {
let decoder = try decoder.container(keyedBy: CodingKeys.self)
// ...decode other properties
let decodedItineraries = container.decode(Array<Itinerary>.self, forKey: .itineraries)
self.init()
for itinerary in decodedItineraries {
self.addItinerary(itinerary)
}
}
enum CodingKeys: CodingKey {
case itineraries, // other keys... //
}
}
class Itinerary: NSObject {
#objc dynamic let date: Date
unowned var course: Course
// ...other properties
private init(date: Date, course: Course) {
self.course = course
self.date = date
super.init()
course.addItinerary(self)
}
// encodable conformance
func encode(to encoder: Encoder) throws {
// ...encode all the desired properties of itinerary class
// ...never encoded the 'course' property but still got the desired JSON file
}
// decodable conformance
required convenience init(from decoder: Decoder) throws {
// ...decode all the properties
// ...never decoded the 'course' property because it was never encoded
self.init(date: decodedDate, course: decodedCourse)
}
}
And a sample of the JSON that is being produced...
[
{
"itineraries" : [
{
"date" : 587357150.51135898
},
{
"date" : 587443563.588081
}
],
"title" : "sample course 1",
"startDate" : 587357146.98558402,
"endDate" : 587789146.98558402
},
{
"itineraries" : [
{
"date" : 587357150.51135898
},
{
"date" : 587443563.588081
},
{
"date" : 587443563.588081
}
],
"title" : "sample course 2",
"startDate" : 587957146.98558402,
"endDate" : 588389146.98558402
}
]

Decoding JSON with swift's Codable

I'm trying to download some JSON data from Wikipedia using their APIs and trying to parse it through swift's Codable. The url I'm using returns the following:
{
"batchcomplete": "",
"query": {
"pageids": [
"143540"
],
"pages": {
"143540": {
"pageid": 143540,
"ns": 0,
"title": "Poinsettia",
"extract": "The poinsettia ( or ) (Euphorbia pulcherrima) is a commercially important plant species of the diverse spurge family (Euphorbiaceae). The species is indigenous to Mexico. It is particularly well known for its red and green foliage and is widely used in Christmas floral displays. It derives its common English name from Joel Roberts Poinsett, the first United States Minister to Mexico, who introduced the plant to the US in 1825."
}
}
}
}
in swift I have declared the following:
struct WikipediaData: Codable {
let batchComplete: String
let query: Query
enum CodingKeys : String, CodingKey {
case batchComplete = "batchcomplete"
case query
}
}
struct Query: Codable {
let pageIds: [String]
let pages: Page
enum CodingKeys : String, CodingKey {
case pageIds = "pageids"
case pages
}
}
struct Page: Codable {
let pageId: Int
let ns: Int
let title: String
let extract: String
enum CodingKeys : String, CodingKey {
case pageId = "pageid"
case ns
case title
case extract
}
}
I'm not sure if the above correctly models the data. Specifically,
let pages: Page
as there might be more than one page returned.
Also, shouldn't that be a dictionary? if so, what would the key be since it is data ("143540" in the above case) rather than a label?
I hope you could point me in the right direction.
thanks
Since your pageIds are Strings your pageId should be too. Its mesmerizingly simple to use a Dictionary at this point, in fact you can simply use
let pages: [String:Page]
and you will be done (and given a Dictionary with minimal effort). Codable alone almost seems a reason to pick up Swift. I am intrigued to see to what other interesting and elegant solutions it will lead eventually. It has already changed how I think about JSON parsing permanently. Besides: your pages really want to be a Dictionary.

Running a loop through JSON dictionary with survey questions SWIFT

So here is my code to iterate through my JSON dictionary and on my console it prints each question and not the question title.
for x in r {
if(x.0.containsString("Question")){
print(x.1)
self.questionLabel.text = (x.1) as! String
} else if (x.0.containsString("Ladder")) {
print(x.1)
}
}
I want to be able to control the loop by pressing a NEXT button before it will iterate onto next question. How do I do this?
Dictionaries are a SequenceType which sit on top of something called generators. Generators facilitate looping by having a method called next(). By calling next() you get access to the next item in the Sequence. Something along the following lines should get you most of the way towards what you're after:
let dict = ["Question":"What is the population of ...",
"Ladder" : "Something else I guess?"]
var generator = dict.generate()
func nextQuestion() -> String? {
if let pair = generator.next() {
if pair.0.containsString("Question") {
return pair.1
}
// recusively calls itself until if finds another question
// or runs out of entries
return nextQuestion()
}
// no more entries in the dictionary
return nil
}
print(nextQuestion()) // prints: Optional("What is the population of ...")
print(nextQuestion()) // prints: nil
So basically, nextQuestion() will hunt through the generator until it finds the next question and returns it. It will either run out of things to search and so return nil, or it will find the next question and return that instead.
If you can give some information about how your dictionary is built I could provide a better answer, but this is what I can give you with the code you provided. Hope it helps
let trackingNumber: Int = 0
let arrayWithDictionary = NSArray() //Assuming you have a array with dictionaries
func getNextQuestion(){
let question = arrayWithDictionary[trackingNumber].valueForKey("Question") as! String
let ladder = arrayWithDictionary[trackingNumber].valueForKey("ladder") as! String
self.questionLabel.text = question
trackingNumber += 1
}
Connect your "next question" button and call 'getNextQuestion()' to get the next question.
#IBAction func nextButton(sender: UIButton){
getNextQuestion()
}

Nested Arrays throwing error in realm.create(value: JSON) for Swift

I'm using Realm in my Swift project and have a rather long JSON file with a couple of nested properties. I'm aware that in order for Realm to use this serialized JSON data directly, the properties need to match exactly (https://realm.io/docs/swift/latest/#json).
But because Realm Lists need to have an Object instead of a String, I have to use something like List with Requirement being a Realm Object that holds a single String called 'value'.
When I run this code:
try! realm.write {
let json = try! NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions())
let exhibit = Exhibit(value: json)
exhibit.id = "1"
realm.add(exhibit, update: true)
}
I get this error message:
*** Terminating app due to uncaught exception 'RLMException', reason: 'req1' to initialize object of type 'Requirements': missing key 'value''
Here's a shortened version of the JSON I'm feeding in there:
{
"exhibit_name": "test1",
"requirements": [
"req1",
"req2"
],
"geofence": {
"latitude": 36.40599779999999,
"longitude": -105.57696279999999,
"radius": 500
}
}
And my Realm model classes are this:
class Exhibit: Object {
override static func primaryKey() -> String? {
return "id"
}
dynamic var id = "0" //primary key
dynamic var exhibit_name: String = ""
let requirements = List<Requirements>()
dynamic var geofence: Geofence?
}
class Geofence: Object {
dynamic var latitude: Float = 0.0
dynamic var longitude: Float = 0.0
dynamic var radius: Float = 0.0
}
class Requirements: Object {
dynamic var value = ""
}
I find it interesting that I'm not getting any errors for the Geofence property, since that's a dictionary.
How do I set up the Requirements model to make this work properly?
Unfortunately you can't just setup your Requirements model in a different way, which would allow you to directly map your JSON to Realm objects.
The init(value: AnyObject) initializer expects either a dictionary, where the keys are the names of your object properties, or an array, where the property values are ordered in the same like they are defined in your object model. This initializer is recursively called for related objects.
So to make that work, you will need to transform your JSON, so that you nest the string values into either dictionaries or arrays. In your specific case you could achieve that like seen below:
…
var jsonDict = json as! [String : AnyObject]
jsonDict["requirements"] = jsonDict["requirements"].map { ["value": $0] }
let exhibit = Exhibit(value: jsonDict)
…
Side Note
I'd recommend using singular names for your Realm model object classes (here Requirement instead Requirements) as each object just represent a single entity, even if you use them only in to-many relationships.