@Observable
public class ExerciseSearchModel: RouteAware {
public var exercises: [Exercise] = []
public var searchQuery: String = ""
public var filters: ExerciseFilters = .init()
public var sort: ExerciseSort = .defaultValue
...
// "computed" property, conceptually
public var filteredExercises: IdentifiedArrayOf<Exercise> = []
}
I have a very simple task. If any of the 3 fields changes, I want to calculate filteredExercises.
{
if exercises.isEmpty {
return []
}
let result = filterExercisesUseCase.execute(
exercises: exercises,
query: searchQuery,
filters: filters,
sort: sort
)
return IdentifiedArrayOf(uniqueElements: result)
}
I am very new to the Swift and I have no idea how to implement it idiomatically.
I have tried:
Making filteredExercises a computed get-only property. Everything works, but the recompute happens frequently, more than needed, definitely more than once -- I assume it depends on how many times filteredExercises mentioned in the view. I have no control, and there is no caching.
Making the model an ObservableObject, every field is @Published with combineLateston the three. It works but I am under the impression that Apple moves away from Combine to Observation which supersedes / replaces it. I want to build it on the latest future-looking stack. I don't know if I should avoid Combine.
I looked into didSet on all the dependencies, but discarded it quickly. I need proper "reactivity", and a correct way to "subscribe" / see the changes to a property (or a collection of them).
It should be embarrassingly simple, and I feel I miss a big chuck of hidden framework / functionality to implement it naturally / properly.
Any help / direction would be very appreciated. Thank you!
(My first post here, I hope I am posting it to the right thread)
I believe this is a pretty common performance bottleneck when starting with SwiftUI. Sorting is O(n log n) and performing that work every time you change any state in your view component can lead to a lot of expensive operations you don't really need.
This is one the main bottlenecks in the sample-food-truck repo from Apple. That app is built on SwiftUI and an "MV*" architecture. The sorting and filtering on data models is performed on-demand without any memoization or caching.
The Swift-CowBox-Sample repo [full disclosure: self-promotion] takes two approaches to speed up the Food Truck app. The first approach is to transform the primary data model to a copy-on-write data structure with the Swift-CowBox repo [full disclosure: self-promotion]. The second approach is to "memoize" the derived data with a custom DynamicProperty. This is all free and open-source and might have some ideas to get you started. You can also follow the steps to measure your changes so you can actually track what difference these speed-ups are making to your app.
If your primary data model is Exercise and this model is not very complex… you might not see a big speed up from the CowBox macro. Copy-on-write semantics do come with a performance tradeoff… so please measure your changes before and after. Even if you don't go with the CowBox macro you can still use this code for an example of what a memoized DynamicProperty looks like.
Building your own custom DynamicProperty values works… but might be difficult to scale as your app grows in size. Building a custom DynamicProperty from scratch for every different view component looks like repetitive code that should live in an infra. If you would like to see a different approach the ImmutableData-FoodTruck repo [full disclosure: self-promotion] refactors the Food Truck app from Apple away from an "MV*" architecture to a unidirectional data flow using the ImmutableData architecture [full disclosure: self-promotion]. The ImmutableData infra gives product engineers the tools to define the derived data that view components should display and the infra is where the memoization and caching takes place on your behalf.
If you're just starting out with Swift and SwiftUI… my advice is probably to start with the basics. Learn how to put views on screen and update state using imperative mutations directly from your components. Just keep in mind that building complex applications from mutable data is difficult to scale as these apps grow in size. I would recommend coming back to the ImmutableData project once you feel comfortable with the basics on SwiftUI.