Search code examples
swiftuiswift5swiftui-list

How to Append to Array of Structs and have Change Persistant?


I want to be able to store a list of objects in a separate Swift file and call them in a page to have them show up. I successful did this with this code:

import Foundation
import SwiftUI



struct MatchInfo: Hashable, Codable {
    let theType: String
    let theWinner: String
    let theTime: String
    let id = UUID()
}


var matchInfo = [
MatchInfo(theType: "Capitalism", theWinner: "Julia", theTime: "3/3/2021"),
MatchInfo(theType: "Socialism", theWinner: "Julia", theTime: "3/2/2021"),
MatchInfo(theType: "Authoritarianism", theWinner: "Luke", theTime: "3/1/2021")
]

where I append to the list after a match is played on another page here:

 matchInfo.insert(MatchInfo(theType: typeSelection, theWinner: winnerName, theTime: "\(datetimeWithoutYear)" + "\(year)"), at: 0)

And heres some of the code on another page where I call it into a list:

List {
                    ForEach(matchInfo, id: \.self) { matchData in
                        
                        matchRow(matchData : matchData)
                        
                    } .background(Color("invisble"))
                    .listRowBackground(Color("invisble"))
                } .frame(height: 490)

...

struct matchRow: View {

let matchData: MatchInfo
 
var body: some View {
    HStack {
        VStack(alignment: .leading) {
            Text(matchData.theType)
                .font(.system(size: 20, weight: .medium, design: .default))
            Text("    Winner: " + matchData.theWinner)
        }
        Spacer()
    
        Text(matchData.theTime)
            .padding(.leading, 40)
            .multilineTextAlignment(.trailing)
    
    }
    .foregroundColor(.white)
    .accentColor(.white)
}
}

But this code doesn't save through app restarts. I've never had something save through a restart before and have been struggling to find an answer simple enough for me to understand. How can I update the list without it going away next time I open the app?


Solution

  • Okay so here is an example on how you save to/ load from the documents folder.

    First of all make sure that you object MatchInfo conforms to this protocol.

    import Foundation
    
    protocol LocalFileStorable: Codable {
        static var fileName: String { get }
    }
    
    extension LocalFileStorable {
        static var localStorageURL: URL {
            guard let documentDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first else {
                fatalError("Can NOT access file in Documents.")
            }
            
            return documentDirectory
                .appendingPathComponent(self.fileName)
                .appendingPathExtension("json")
        }
    }
    
    extension LocalFileStorable {
        static func loadFromFile() -> [Self] {
            do {
                let fileWrapper = try FileWrapper(url: Self.localStorageURL, options: .immediate)
                guard let data = fileWrapper.regularFileContents else {
                    throw NSError()
                }
                return try JSONDecoder().decode([Self].self, from: data)
                
            } catch _ {
                print("Could not load \(Self.self) the model uses an empty collection (NO DATA).")
                return []
            }
        }
    }
    
    extension LocalFileStorable {
        static func saveToFile(_ collection: [Self]) {
            do {
                let data = try JSONEncoder().encode(collection)
                let jsonFileWrapper = FileWrapper(regularFileWithContents: data)
                try jsonFileWrapper.write(to: self.localStorageURL, options: .atomic, originalContentsURL: nil)
            } catch _ {
                print("Could not save \(Self.self)s to file named: \(self.localStorageURL.description)")
            }
        }
    }
    
    extension Array where Element: LocalFileStorable {
        ///Saves an array of LocalFileStorables to a file in Documents
        func saveToFile() {
            Element.saveToFile(self)
        }
    }
    

    Your main Content View should look like this: (I modified your object to make it a bit simpler.)

    import SwiftUI
    
    struct MatchInfo: Hashable, Codable, LocalFileStorable {
        static var fileName: String {
            return "MatchInfo"
        }
        
        let description: String
    }
    
    struct ContentView: View {
        @State var matchInfos = [MatchInfo]()
        
        var body: some View {
            VStack {
                Button("Add Match Info:") {
                    matchInfos.append(MatchInfo(description: "Nr." + matchInfos.count.description))
                    MatchInfo.saveToFile(matchInfos)
                }
                List(matchInfos, id: \.self) {
                    Text($0.description)
                }
                .onAppear(perform: {
                    matchInfos = MatchInfo.loadFromFile()
                })
            }
        }
    }