swiftuinspredicatefetchrequestswiftui-charts

SwiftUI - How do I write a fetch request to return the largest value on each date?


I am creating a workout tracking app and trying to plot the largest weight value on a day over time for a given exercise. The entity "ExerciseSet" contains attributes dateCompleted and weight. There are multiple ExerciseSets saved on the same date, so I want to sort the ExerciseSets descending by weight for each dateCompleted and return the first element (being the max) for each date so that there is one entry (the max) for each date. How can I modify the fetch request to do this? Any suggestions would be greatly appreciated. As of now, there are multiple points for each date when I want just the max weight. My code is here:

import SwiftUI
import Charts

struct ExerciseDetailView: View {

    @FetchRequest var exercisesets: FetchedResults<ExerciseSet>

    var exercise: String
    
    init(exercise: String) {
        self.exercise = exercise
        self._exercisesets = FetchRequest(
            entity: ExerciseSet.entity(),
            sortDescriptors: [NSSortDescriptor(key: "weight", ascending: false)],
            predicate: NSPredicate(format: "exercisename == %@", exercise as any CVarArg)
        )}
        
    var body: some View {
        NavigationView {      
            ScrollView {
                if #available(iOS 16.0, *) {
                    Chart(exercisesets) { e in
                        LineMark(x: .value("Date", e.dateCompleted.startOfDay),
                                 y: .value("Reps", e.reps)
                        )}
                }
                else {
                    Text("Oops")
                    // Fallback on earlier versions
                }
           }
        }
       }
    }


Solution

  • AFAIK you can't solve this with the @FetchRequest alone, but you can use a computed var that transforms the fetched exercisesets into a new array that only holds the max weight entries:

    The technique is commonly used for uniqueValues functions on arrays or other sequences. The seenDates set keeps track of dates we already have seen. It's a set and not an array, because a set has a nice insert behavior that we can use: if we insert into the set, we get back (inserted: Bool, memberAfterInsert: Set.Element). So inserted is true if the element did not exist before, and false if it did.

    So the .filter line in the computed var does the magic: it tries to insert the current date into seenDates and as the inserted` result is only true for the first time this date was added, those are the elements we are looking for.

        var exerciseMaxWeightArray: [ExerciseSet] {
            var seenDates: Set<Date> = []
            return exercisesets
                .sorted { $0.weight > $1.weight } // you can skip this line if already sorted by weight in the @fetchrequest
                .filter { seenDates.insert($0.dateCompleted.startOfDay).inserted }
                .sorted { $0.dateCompleted < $1.dateCompleted }
        }