TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

Array Not Saving

Forums > SwiftUI

I'm working on project Milestone: Projects 13-15 (Hot Prospect) and have working code with the exception of saving the array (prospects: [Prospect]) that holds all the prospect data. Because the Prospect class includes an image (that comes from an ImagePicker) I had to conform the class to Codable by hand in order to be able to JSONE encode/decode the data for saving and loading.

Code below. Any insights would be much appreciated.

Prospect Class

import Foundation
import SwiftUI

class Prospect: Codable, Identifiable, ObservableObject {
    @Published var id = UUID()
    @Published var firstName = ""
    @Published var lastName = ""
    @Published var company = ""
    @Published var title = ""

    @Published var image: UIImage?

    enum CodingKeys: CodingKey {
        case id, firstName, lastName, company, title, data, scale
    }

    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(company, forKey: .company)
        try container.encode(title, forKey: .title)

        if let image = self.image {
            try container.encode(image.pngData(), forKey: .data)
            try container.encode(image.scale, forKey: .scale)
        }
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(UUID.self, forKey: .id)
        firstName = try container.decode(String.self, forKey: .firstName)
        lastName = try container.decode(String.self, forKey: .lastName)
        company = try container.decode(String.self, forKey: .company)
        title = try container.decode(String.self, forKey: .title)
        let scale = try container.decode(CGFloat.self, forKey: .scale)
        let data = try container.decode(Data.self, forKey: .data)
        self.image = UIImage(data: data, scale: scale)
    }

    init() { }
}

ContentView

import Foundation
import SwiftUI

struct ContentView: View {
    @State private var image: Image?
    @State private var inputImage: UIImage?
    @State private var showingImagePicker = false
    @State private var showingPhotoNameView = false
    @State private var photoName = ""
    @StateObject var prospect = Prospect()
    @State private var prospects = [Prospect]()

    let savePath = FileManager.documentsDirectory.appendingPathComponent("SavedProspects")

    var body: some View {
        HStack {
            Text("Hot Prospects")
                .font(.largeTitle.weight(.bold))
                .padding(.leading)

            Spacer()

            Button {
                showingImagePicker = true
            }label: {
                Image(systemName: "plus")
            }
            .padding(.trailing)
        }
        VStack(alignment: .leading) {
            NavigationView {
                List {
                    ForEach(prospects, id: \.id) { prospect in
                        NavigationLink {
                            DetailView(prospect: prospect)
                        } label: {
                            HStack {
                                if let image = prospect.image {
                                    Image(uiImage: image)
                                        .resizable()
                                        .scaledToFit()
                                        .frame(width: 100, height: 100)
                                }

                                Spacer()

                                Text("\(prospect.firstName) \(prospect.lastName)")
                            }
                        }
                    }
                }
                .sheet(isPresented: $showingImagePicker) {
                    ImagePicker(inputImage: $inputImage)
                }
                .sheet(isPresented: $showingPhotoNameView) {
                    PhotoNameView { newProspect in
                        newProspect.image = inputImage
                        prospects.append(newProspect)
                        save(prospects: prospects) <--- function call to save prospects array

                    }
                }
                .onChange(of: inputImage) { _ in loadImage() }
            }
        }
    }

    init() {  <--- loads the prospects array data at initialization
        do {
            let data = try Data(contentsOf: savePath)
            prospects = try JSONDecoder().decode([Prospect].self, from: data)
        } catch {
            print("Failed to initialize")
            prospects = [ ]

        }
    }

    func loadImage() {
        guard let inputImage = inputImage else { return }
        image = Image(uiImage: inputImage)
        photoName = ""
        showingPhotoNameView = true
    }

    func save(prospects: [Prospect]) {  <--- function to save prospects array
        do {
            let data = try JSONEncoder().encode(prospects)
            try data.write(to: savePath, options: [.atomic, .completeFileProtection])
        } catch {
            print("Unable to save data")
        }
    }
}   

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

2      

"Array Not Saving"... What exactly does this mean? Do you get an error? A silent fail? Does the rest of your JSON save and just the image doesn't? etc.

But the first thing you should do is replace your meaningless error messages with something that gives you an idea what happened when something goes wrong:

//replace
print("Failed to initialize")

//with
print(error)

//replace
print("Unable to save data")

//with
print(error)

What do you get after you do that?

2      

Thanks for your help.

What I mean by "Array not saving" is that when I enter some prospects via the PhotoNameView those entries are not being loaded when I quit the app and start it again so nothing is being saved.

Per the suggestions...

I added the print(error) calls as suggested and no errors are printing.

Also, I added print calls to print the prospect array at app initializaton and after the save() function is called (just to be sure it is being called) as well as one to print the save path.

Here is what I get in the console...

Content of prospects at app startup: [ ] <---empty prospects array at app startup even after

Save complete <---save function is called.

file:///Users/markmeihaus/Library/Developer/CoreSimulator/Devices/199EE113-5C58-4A56-AC98-F17B2EB23F7C/data/Containers/Data/Application/B1ECD931-0612-4FFA-8B24-95BA0B1F10E6/Documents/SavedProspects

Content of prospects after calling save(): [HotPropsects.Prospect]

After reviewing my code again I think perhaps the issue is when I initialize the PhotoNameView (see arrow below). The prospects array is an ObservedObject so it needs initialization. I think the way I have it written is that every time the PhotoNameView is called I initialize the prospect array with an empty Prospect object? But even so I should be saving at least one entry.

Could that be the issue? Any thoughts on how to initialize the PhotoNameView if my current code is incorrect?

import SwiftUI

struct PhotoNameView: View {
    @Environment(\.dismiss) var dismiss
    @State private var photoName = ""
    @State private var lastName = ""
    @State private var company = ""
    @State private var title = ""
    @ObservedObject var prospect: Prospect
    var onSave: (Prospect) -> Void

    var body: some View {
        NavigationView {
            Form {
                TextField("Enter first name", text: $photoName)
                TextField("Enter last name", text: $lastName)
                TextField("Enter company name", text: $company)
                TextField("Enter job title", text: $title)
            }
            .navigationTitle("Photo Name")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                Button("Save") {
                    createNewProspect()
                    dismiss()
                }
            }
        }
    }

    init(onSave: @escaping (Prospect) -> Void) {  <----initialization of the view.
        self.onSave = onSave
        _prospect = ObservedObject(initialValue: Prospect())
    }

    func createNewProspect() {
        let newProspect = prospect
        newProspect.firstName = photoName
        newProspect.lastName = lastName
        newProspect.company = company
        newProspect.title = title
        onSave(newProspect)

    }
}

2      

Hi, Try changing the init into an onAppear in your ContentView:

.onAppear {
    do {
        let data = try Data(contentsOf: savePath)
        prospects = try JSONDecoder().decode([Prospect].self, from: data)

    } catch {
        print("Failed to initialize")
        prospects = []

    }
}

2      

Brilliant! That did it. Thanks a bunch @Hectorcrdna. Now I need to figure out why that works and not how I originally coded.

One additional question if you don't mind...How do I add images to the picker view in Xcode? Right now when I call the picker I get some stock photos but I would like to add some other ones more appropriate for the project. Any thoughts?

2      

As far as why i know it has to do with it being an @State property, you could also go into the property value like this

init() {
    do {
        let data = try Data(contentsOf: savePath)
        let decoded = try JSONDecoder().decode([Prospect].self, from: data)
        _prospects = State(initialValue: decoded)

    } catch {
        print("Failed to initialize")
        _prospects = State(initialValue: [])

    }
}

but i think i read it could have side effects, not entirely sure; and to add images you just drag and drop them in the simulator and they save to the images app.

2      

Got it, thanks.

One final question if you are so inclined...I've listened to/read/reviewed Paul's discussions on property wrappers and using _Object rather than just Object when assigning values but I'm still trying to get my head around it. When usingState or ObservedObject functions there is another parameter called wrappedValue. How would that differ from using the initial value one?

2      

I quess it would not differ, the documentation for init(initialValue:) says "This initializer has the same behavior as the init(wrappedValue:) initializer. See that initializer for more information."

2      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.