Implementing a SwiftData Query View as the most convenient way to fetch data in SwiftUI
Many will find my opinion controversial, but I consider that SwiftUI views cannot be perceived as real UI views. SwiftUI views are models that describe the real UI still based on UIKit.
Based on that opinion, we have all rights to design services based on the View protocol to integrate them smoothly into the UI flow.
Today, I’ll show you how to create SwiftUI SwiftData Query service integrated SwiftUI flow and expanded a lot compared to just a Query
macro.
If you like my content and want to support me, please buy me a coffee and subscribe!
Also, I have the Telegram channel Swift in UA, where you can find more exciting things.
Creating a QueryView
The first steps are so close to a general Query
usage:
- Create a View
- Add a
query
- Add
type
marker - Add content
ViewBuilder
that accepts aModel
array and returns some View
struct QueryView<Model: PersistentModel, Content: View>: View {
@Query private var query: [Model]
var type: Model.Type
@ViewBuilder var content: ([Model]) -> (Content)
var body: some View {
content(query)
}
}
The code is straightforward, and so is its usage:
But what benefits does this code provide compared to the standard Query macro way? Radical! But at the moment, it is not so obvious.
The Query’s biggest problem
It’s impossible to use stored properties in the Query initializer. So, it means that Qurey can’t be updated dynamically. No filter changes, no sorting changes.
And, it’s impossible to re-assign the query on the example search word update.
No way. Query macro can be initialized once and that’s all… Query is not a typical view state… Wait!
Initializing the QueryView
SwiftUI View
updates when the related state changes! It’s possible to move Query
initialization to the QueryView
initialization with all properties that need to set up the Query
!
struct QueryView<Model: PersistentModel, Content: View>: View {
@Query private var query: [Model]
private var content: ([Model]) -> (Content)
init(for type: Model.Type,
sort: [SortDescriptor<Model>] = [],
@ViewBuilder content: @escaping ([Model]) -> Content,
filter: (() -> (Predicate<Model>))? = nil) {
_query = Query(filter: filter?(), sort: sort)
self.content = content
}
var body: some View {
content(query)
}
}
Now, the filter and sort are initializer parameters of QueryView, which means they can be set up externally. And if the external parameters are View state parameters (State, Binding, AppStorage, etc…), they will trigger the QueryView update on change! Let’s see:
That’s it! Now, the QueryView
is ready and works well! It is fully integrated with SwiftUI flow and is much more understandable and flexible.
But there’s one more thing to prepare the QueryView for most cases.
Sectioning the Query View
If there’s a lot of data in the list, separating the data by some key attribute is convenient. For example, using a sorted list of keys and transforming the array into a dictionary:
let people = [p1, p2, p3, ...., pn]
let grouped = Dictionary(grouping: people, by: \.city)
To add logic to the QueryView
, one more type is needed — Hashable
Key. Then add the keyExtractor
property. The property has the same signature as in the Dictionary
:
struct SectionedQueryView<Content: View, Model: PersistentModel, Key: Hashable>: View {
@Query private var query: [Model]
private var content: ([QueryViewDataSection<Key, Model>]) -> Content
private var keyExtractor: ((Model) -> Key)
init(for type: Model.Type,
sectionedBy keyExtractor: @escaping ((Model) -> Key),
sort: [SortDescriptor<Model>] = [],
@ViewBuilder content: @escaping ([QueryViewDataSection<Key, Model>]) -> Content,
filter: (() -> (Predicate<Model>))? = nil) {
_query = Query(filter: filter?(), sort: sort)
self.content = content
self.keyExtractor = keyExtractor
}
var body: some View {
let data = Dictionary(grouping: query, by: keyExtractor)
let result = keys.reduce([QueryViewDataSection]()) { partialResult, key in
partialResult + [.init(key: key, models: data[key] ?? [])]
}
content(result)
}
private var keys: [Key] {
var seen: Set<Key> = []
var result: [Key] = []
for model in query {
let key = keyExtractor(model)
if !seen.contains(key) {
seen.insert(key)
result.append(key)
}
}
return result
}
}
struct QueryViewDataSection<Key: Hashable, Model: PersistentModel>: Identifiable {
let key: Key
let models: [Model]
let id = UUID()
}
Now, this is a SectionedQueryView! The main differences from QueryView are:
- Hashable Key
keyExtractor
property- Updated
content
closure — now it accepts an array of sections instead of an array of models keys
computed property. Thekeys
sort in the same order that theQuery
sortsQueryViewDataSection
to handle the key and related models
Let’s see how it looks!