Generic function with structs

Viewed 65

I try to create reusable function. It select data only for current week. But I need use this function few times in my app, so I've decided to do it generic.

I have two structs

struct BodyWeightCalendarModel {
    let weight: Float
    let date: Date
}

struct RetrievedWorkoutsByExercise {
    
    let exerciseTitle: String
    let sets: Int
    let maxWeight: Int
    let maxReps: Int
    let date: Date
    let volume: Int
    
}


 func loopForWeek<T> (data: [T], completionHandler: ([T]) -> Void)  {
        
        let arrayForPeriod2: [T] = []
        
        for value in data where value.date >= (calendar.currentWeekBoundary()?.startOfWeek)! && value.date <= (calendar.currentWeekBoundary()?.endOfWeek)!  {
            arrayForPeriod.append(value)
        }
        completionHandler(arrayForPeriod2)
    }

How to get access to data values? I can't get access through "value.data". So I want to use this function for different struct (but all this structs needs to have field "date").

3 Answers

The reason you can't access value.date is that your function knows nothing about T. Your function declares T but it doesn't constrain it. To the compiler, T can be anything.

You need to create a protocol that tells your function what to expect, and make your structs conform to it:

protocol Timed { // You might find a better name
    var date: Date { get }
}

struct BodyWeightCalendarModel: Timed {
    let date: Date
    ...
}

struct RetrievedWorkoutsByExercise: Timed {
    let date: Date
    ...
}

Now you can constrain T to Timed and your function will know that value has a date property.

func loopForWeek<T: Timed> (data: [T], completionHandler: ([T]) -> Void)  {

What you're trying to do is close. A Generic value doesn't necessarily have a value on it called date so this won't work. What you can do instead is create a protocol that has a variable of date on it. With that protocol you can do something neat with an extension.

protocol Dated {
    var date: Date { get set }
}

extension Dated {
    func loopForWeek() -> [Dated] {
          let arrayForPeriod2: [Dated] = []
        
          for value in date >= (calendar.currentWeekBoundary()?.startOfWeek)! && value.date <= (calendar.currentWeekBoundary()?.endOfWeek)!  {
            arrayForPeriod.append(value)
        }
        return arrayForPeriod2
    }
}

Then we can make your two structs use that protocol like:

struct BodyWeightCalendarModel: Dated {
    let date: Date
}

struct RetrievedWorkoutsByExercise: Dated {
    let date: Date
}

And they can call the function like:

let workouts = RetrievedWorkoutsByExercise()
let result = workouts.loopForWeek()

As mentioned in other answers using a protocol with date property makes most sense in your case. However theoretically you could also use keypaths to achieve that. You could make a function that takes any instances and gets dates from them in a similar way as below:

func printDates(data: [Any], keyPath:AnyKeyPath)  {
    for value in data {
        if let date = value[keyPath: keyPath] as? Date {
            print("date = \(date)")
        }
    }
}

and then pass for example your BodyWeightCalendarModel instances as below:

let one = BodyWeightCalendarModel(date: Date(), weight: 1)
let two = BodyWeightCalendarModel(date: Date(), weight: 2)
printDates(data: [one, two], keyPath: \BodyWeightCalendarModel.date)

But still a protocol makes more sense in your case.

Related