Convert a variable into JSON (Lunch Card expansion)

Another expansion of the Lunch Card app. I figured out my problem in the last thread so I started a new one.

Please see my last thread for code references.

To save List data without using Core Data — because that would be a bit of a hassle — I was told to use Codable to convert the array to Data and export that into a file.

Thing is; I don’t know how I’d convert cardsInfo into Data, furthermore, convert to JSON.

Once I figure out how to convert that to JSON and into writable data, I’ll pretty much be good to go.
Answered by OOPer in 655686022
Sorry for my poor suggestions which did not work well.
But once found, it is very simple.

A simple sample code:
Code Block
import Foundation
struct CardInfo: Identifiable, Codable { //<- Make `CardInfo` conform to `Codable`
var name: String = ""
var id: String = ""
var cname: String = ""
var code: String = ""
}
class CardsInfo: ObservableObject {
@Published var newCard: CardInfo = CardInfo()
@Published var cards: [CardInfo] = []
func add() {
cards.append(newCard)
}
}
//...
extension CardsInfo {
var dataUrl: URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("cards.json")
}
func saveCards() {
do {
//Convert Array of `CardInfo` to `Data`
let data = try JSONEncoder().encode(cards)
//Write `Data` to a file specified by URL
try data.write(to: dataUrl, options: .atomic)
} catch {
print(error)
}
}
func loadCards() {
do {
if FileManager.default.fileExists(atPath: dataUrl.path) {
return
}
//Read `Data` from a file specified by URL
let data = try Data(contentsOf: dataUrl)
//Convert `Data` to Array of `CardInfo`
let cards = try JSONDecoder().decode([CardInfo].self, from: data)
self.cards = cards
} catch {
print(error)
}
}
}


You need to call cardsInfo.saveCards(), where cardsInfo is the right instance of CardsInfo, maybe some action closure in CardsView.
Accepted Answer
Sorry for my poor suggestions which did not work well.
But once found, it is very simple.

A simple sample code:
Code Block
import Foundation
struct CardInfo: Identifiable, Codable { //<- Make `CardInfo` conform to `Codable`
var name: String = ""
var id: String = ""
var cname: String = ""
var code: String = ""
}
class CardsInfo: ObservableObject {
@Published var newCard: CardInfo = CardInfo()
@Published var cards: [CardInfo] = []
func add() {
cards.append(newCard)
}
}
//...
extension CardsInfo {
var dataUrl: URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("cards.json")
}
func saveCards() {
do {
//Convert Array of `CardInfo` to `Data`
let data = try JSONEncoder().encode(cards)
//Write `Data` to a file specified by URL
try data.write(to: dataUrl, options: .atomic)
} catch {
print(error)
}
}
func loadCards() {
do {
if FileManager.default.fileExists(atPath: dataUrl.path) {
return
}
//Read `Data` from a file specified by URL
let data = try Data(contentsOf: dataUrl)
//Convert `Data` to Array of `CardInfo`
let cards = try JSONDecoder().decode([CardInfo].self, from: data)
self.cards = cards
} catch {
print(error)
}
}
}


You need to call cardsInfo.saveCards(), where cardsInfo is the right instance of CardsInfo, maybe some action closure in CardsView.
I will try this out! Thanks!

You need to call cardsInfo.saveCards(), where cardsInfo is the right instance of CardsInfo, maybe some action closure in CardsView.

Understood.

Would it work to have cardsInfo.saveCards() as...
  1. Another action in the Create button in AddView:

Code Block
Button(action: {
cardsInfo.add()
cardsInfo.saveCards()
sheetInfo.showSheetView = false
}) {
Text("Create")
.bold()
}
.disabled(self.cardsInfo.newCard.name.isEmpty self.cardsInfo.newCard.id.isEmpty self.cardsInfo.newCard.cname.isEmpty)
.foregroundColor(.white)
.padding()
.padding(.horizontal, 100)
.background(Color.accentColor)
.cornerRadius(10)

2. In onDelete

3. In onMove

Code Block
private func onDelete(offsets: IndexSet) {
self.presentMode.toggle()
cardsInfo.cards.remove(atOffsets: offsets)
cardsInfo.saveCards()
}
    
private func onMove(source: IndexSet, destination: Int) {
cardsInfo.cards.move(fromOffsets: source, toOffset: destination)
cardsInfo.saveCards()
}


But where would I put cardsInfo.loadCards()? I assume I'd need that to load the saved cards, but I don't know where I'd put that during initial launch.

I assume I'd need that to load the saved cards, but I don't know where I'd put that during initial launch.

There may be some possibilities depending on your view hierarchy.
For example, you may put it in onAppear of CardsView, if you call saveCards() as you describe.
I'll test this and other options out. Thank you!
I tried different methods of .onAppear in different places:

perform:
Code Block
.onAppear(perform: cardsInfo.loadCards)

and the content way:
Code Block
.onAppear {
cardsInfo.loadCards()
}

Neither worked. I was assuming it would save the JSON info from CardInfo and load it the next session, but didn't.

Neither worked.

The first is syntactically bad in Swift,

And a code fragment does not help to see what's going, Please show your latest code.
Here's Info.swift and CardsView:

Info.swift:
Code Block
//
//  Info.swift
//  Lunch Card (iOS)
//
//  Created by Joshua Srery on 12/21/20.
//  Additional code by OOPer on Apple Developer Forums
//
import Foundation
struct CardInfo: Identifiable, Codable {
    var name: String = ""
    var id: String = ""
    var cname: String = ""
//    var code: String = ""
}
class CardsInfo: ObservableObject {
    @Published var newCard: CardInfo = CardInfo()
    @Published var cards: [CardInfo] = []
    
    func add() {
      cards.append(newCard)
    }
}
extension CardsInfo {
    var dataUrl: URL {
        FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            .appendingPathComponent("cards.json")
    }
    
    func saveCards() {
        do {
            let data = try JSONEncoder().encode(cards)
            try data.write(to: dataUrl, options: .atomic)
        } catch {
            print(error)
        }
    }
    
    func loadCards() {
        do {
            if FileManager.default.fileExists(atPath: dataUrl.path) {
                return
            }
            let data = try Data(contentsOf: dataUrl)
            let cards = try JSONDecoder().decode([CardInfo].self, from: data)
            self.cards = cards
        } catch {
            print(error)
        }
    }
}
class SheetInfo: ObservableObject {
    @Published var showSheetView = false
}


CardsView:
Code Block
//
//  CardsView.swift
//  Lunch Card (iOS)
//
//  Created by Joshua Srery on 12/17/20.
//  Additional code by OOPer on Apple Developer Forums
//
import SwiftUI
struct Card: Identifiable {
    let id = UUID()
    let title: String
}
struct CardsView: View {
    @StateObject var cardsInfo = CardsInfo()
    @StateObject var sheetInfo = SheetInfo()
    @State private var editMode = EditMode.inactive
    @State var presentMode = false
    
    var body: some View {
        NavigationView {
            List {
                ForEach(cardsInfo.cards) { card in
                    NavigationLink(destination: CardFullView(cname: card.cname, name: card.name, id: card.id)) {
                        CardRow(cname: card.cname, name: card.name, id: card.id)
                        
                    }
                }
                .onDelete(perform: onDelete)
                .onMove(perform: onMove)
            }.listStyle(PlainListStyle())
            .navigationTitle("Cards")
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    EditButton()
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: {
                        self.sheetInfo.showSheetView.toggle()
                    }) {
                        Image(systemName: "plus")
                    }
                }
            }
            .environment(\.editMode, $editMode)
            .sheet(isPresented: $sheetInfo.showSheetView) {
                AddView(cardsInfo: cardsInfo, sheetInfo: sheetInfo)
            }
            .onAppear {
                cardsInfo.loadCards() // <- Preferred Placement
            }
        }
    }
    
    private func onDelete(offsets: IndexSet) {
        self.presentMode.toggle()
        cardsInfo.cards.remove(atOffsets: offsets)
        cardsInfo.saveCards()
    }
    
    private func onMove(source: IndexSet, destination: Int) {
        cardsInfo.cards.move(fromOffsets: source, toOffset: destination)
        cardsInfo.saveCards()
    }
}

Please tell me if you need more.

Here's Info.swift and CardsView:

Thanks. That revealed a mistake of mine.
Code Block
func loadCards() {
do {
if !FileManager.default.fileExists(atPath: dataUrl.path) { //<- Please do not miss `!`
return
}
let data = try Data(contentsOf: dataUrl)
let cards = try JSONDecoder().decode([CardInfo].self, from: data)
self.cards = cards
} catch {
print(error)
}
}

The first if is the safe way to fall through when file NOT exists.

Seems I had posted my older code. Sorry for taking your time.


One more.

The first is syntactically bad in Swift,

This was another mistake. I was missing you used closure reference (lacking ()) there.

Sorry for taking your time.

You're totally fine, no need to worry!

I tested this and it worked as expected. Thank you so much!
Convert a variable into JSON (Lunch Card expansion)
 
 
Q