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

SOLVED: UPDATED: SwiftData Attribute Fetching

Forums > Swift

So I posted the below last night after having an issue trying to figure out how to fetch a specific attribute of a model. In this instance, I only want a single attribute to calculate a total from. I've been reading through "SwiftData by Example" and reread the section on the propertiesToFetch modifier. I'm still running into issues with the actual use of this modifier, though. So I've played around with xCode trying to just figure out why I'm stuck and here's what I have:

var totalTime = FetchDescriptor<Job> 
totalTime.propertiesToFetch = [\.timeWorked]

That is placed into the struct before the body. xCode want's to treat this like a function, but referencing both Apple and Paul's documentation, this isn't referenced at all. I get the following six errors:

Consecutive declarations on a line must be seperated by ';'
Insert ';'
Expected '(' in argument list of function declaration
Expected '{' in body of function declaration
Expected 'func' keword in instance method declaration
Insert 'func'
Expected declaration
Invalid redeclaration of 'totalTime()'

What exactly am I missing here? I've tried to follow the example'sw in the book to figure out the implementation but I'm at a loss.


Alright, this should be easier than it's turning out to be, and I could use some help. It's been a long day so I'm thinking my brain is just making this a bigger issue than it should be.

I have a SwiftData model that is as follows:

@Model
class Job: Identifiable {
let id: UUID()
var timeWorked: Double

init...}

I ONLY want to fetch all instances of the timeWorked attribute, and nothing else from the model's instances. Is there a quick and easy way to get just this data? I bought SwiftData by Example and have read the whole thing but it doesn't go into this kind of fetch.

I've tried using predicates and sorts but I think I'm overthinking this or just not understanding how SwiftData returns its instances.

However when I attempt to reduce that value to total all values together I wind up getting an error saying "... requires Job to conform to 'BinaryInteger'"

Thanks for your help, Andy

   

Hi Andy,

just a tiny code snippet to show you how this works. To keep things simple and easy, I've used a Button view for "the action" ;-)

My model for the "Brand" type has four properties, the code below fetches only the names. Therefore I define the FetchDescriptor accordingly. Please be aware of the brackets () following the FetchDescriptor call, which are missing in your code.

After you've set up the FetchDescriptor, you have to fetch the data from the ModelContext.

The result is an array of the fetched data type. In my case, this is [Brand].

                Button("Fetch names") {
                    var fetchDescriptor = FetchDescriptor<Brand>()
                    fetchDescriptor.propertiesToFetch = [\.name]
                    do {
                        let names = try modelContext.fetch(fetchDescriptor)
                        print(names.count)
                    } catch {
                        print("Error")
                    }
                }

You'll receive [Job] and have to sum up the total working time afterwards on your own by adding up the "timeWorked Doubles" within your results array.

I assume this isn't exactly what you want, because I read your post as if you're looking for a way to retrieve only the Double values, right?

Hope this helps anyway.

Best Lars

1      

[https://www.hackingwithswift.com/forums/swift/swiftdata-attribute-fetching/26762/26768]

Hey Lars,

This fixed the issue quite nicely. I really appreciate your help looking into this.

Can you point me in the direction of how to make this value observable? In other words, how can I make a property that is watching for the .name attribute and automatically update whenever the attribute changes? I had an idea on making a closure but that doesn't seem to work...

Thanks, Andy

   

I'm not sure, if I'm getting you right, Andy.

Based on your initial data structure you can run some code when your Job object's timeWorked property changes like this:

class Job: Identifiable {
    let id: UUID
    var timeWorked: Double {
        didSet {
            print("Time worked: \(timeWorked)")
        }
    }

    init(id: UUID, timeWorked: Double) {
        self.id = id
        self.timeWorked = timeWorked
    }
}

var job = Job(id: UUID(), timeWorked: 0)
job.timeWorked = 1
job.timeWorked += 2
job.timeWorked = 0

Running this in a playground shows:

Time worked: 1.0

Time worked: 3.0

Time worked: 0.0

Is this what you mean?

1      

If you're interested in adding up the time worked over all Job objects fetched by your query and show this in a view, you can work with a computed property:

struct HwsView: View {
    @Query var jobs: [Job]
    @Environment(\.modelContext) var modelContext

    var totalTimeWorked: Double {
        var total: Double = 0.0
        for i in 0 ..< jobs.count {
            total += jobs[i].timeWorked
        }
        return total
    }

    var body: some View {
        Button("Add") {
            let job = Job(id: UUID(), timeWorked: 1.0)
            modelContext.insert(job)
        }
        Text("Total time worked: \(totalTimeWorked)")
    }
}

Here the computed property totalTimeWorked will be computed every time a change to the jobs array happens, whether it is due to the initial filling with the query results, the insert of a new Job object or the changing timeWorked property of an already existing Job object.

1      

@lik,

A computed property was what I was looking for. I think this will meet the need I'm trying to accomplish. I sincerely appreciate your help. For some reason, I had it in my head that the query should only be for the attribute I'm attempting to calculate. I think the computed property you modeled made me realize it's okay to pull the entire record and not a speific attribute -- specically because other attributes in the record will eventually be computed on their own.

I was trying to reduce performance impact by only pulling in the specific data needed for an individual view, but as multiple views will live within a single view, it really would be the other way around. Having multiple individual queries for each attribute would probably do more harm than just parsing the attribute components each view needs out of a single query.

Just a bit of backwards thinking on my part.... I think?....

If I'm off course here, I'd love the right coordinates lol

Thanks, Andy

   

Hi Andy,

Just a bit of backwards thinking on my part.... I think?....

I don't think so. :-) Your thought reminds me more of an SQL approach like

"select sum(timeWorked) from jobs"

which I miss from time to time, too.

If you're concerned about performance and memory usage, you can use a FetchDescriptor combined with a calculated property. The FetchDescriptor will return an array of your objects but you can control that not all data but only the necessary values will be fetched. This could improve performance and reduce memory usage.

But in regard to your Job class this seems to be overkill.

Best Lars

   

Hacking with Swift is sponsored by Blaze.

SPONSORED Still waiting on your CI build? Speed it up ~3x with Blaze - change one line, pay less, keep your existing GitHub workflows. First 25 HWS readers to use code HACKING at checkout get 50% off the first year. Try it now for free!

Reserve your spot now

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

Reply to this topic…

You need to create an account or log in to reply.

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.