In this article, we will explore property wrappers such as @FetchRequest
, @SectionedFetchRequest
, @Query
, @Namespace
, and @Bindable
. These property wrappers encompass functionalities such as retrieving Core Data and SwiftData within views and creating namespaces in views.
The aim of this article is to provide an overview of the main functionalities and usage considerations of these property wrappers, rather than an exhaustive guide.
- @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, @Environment
- @AppStorage, @SceneStorage, @FocusState, @GestureState, @ScaledMetric
- @UIApplicationDelegateAdaptor, @AccessibilityFocusState, @FocusedObject, @FocusedValue, @FocusedBinding
1. @FetchRequest
In SwiftUI, @FetchRequest
is used for retrieving Core Data entity data within views. It simplifies the process of fetching data from persistent storage and automatically updates the view when the data changes.
1.1 Basic Usage
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
predicate: nil,
animation: .default)
private var items: FetchedResults<Item>
List {
ForEach(items) { item in
if let timestamp = item.timestamp {
NavigationLink {
Text("Item at \(timestamp, format: .dateTime)")
} label: {
Text(timestamp, format: .dateTime)
}
}
}
}
1.2 Main Functions
@FetchRequest
simplifies the process of retrieving data from Core Data, allowing developers to define entity types, sort descriptors, and predicates to filter results.@FetchRequest
automatically updates its fetched results upon any changes in Core Data, ensuring real-time synchronization between the view and the data.- Closely integrated with SwiftUI’s declarative programming model,
@FetchRequest
offers a more natural and smooth way to integrate data-driven interfaces into applications.
1.3 Considerations and Tips
- Before using
@FetchRequest
, ensure that the managed object context has been injected into the current view environment. - The following example demonstrates how to set filtering and sorting conditions in the constructor based on specific parameters:
@FetchRequest
private var items: FetchedResults<Item>
init(startDate:Date,endDate:Date) {
let predicate = NSPredicate(format: "timestamp >= %@ AND timestamp <= %@", startDate as CVarArg,endDate as CVarArg)
_items = FetchRequest(
entity: Item.entity(),
sortDescriptors: [.init(key: #keyPath(Item.timestamp), ascending: true)],
predicate: predicate
)
}
- Starting from iOS 15, developers can dynamically adjust the filtering and sorting conditions of
@FetchRequest
within a view. However, it is important to note that this method is not applicable within.onAppear
and.task
. It is typically used to dynamically update the view based on user actions.
Button("abc") {
$items.nsPredicate.wrappedValue = .init(value: false)
$items.sortDescriptors.wrappedValue = [.init(\Item.timestamp, order: .reverse)]
}
- For more complex configurations, it is recommended to create
@FetchRequest
using an instance ofNSFetchRequest
:
extension Item {
static let noFaultRequest: NSFetchRequest = {
let request = NSFetchRequest<Item>(entityName: "Item")
request.predicate = nil
request.sortDescriptors = [.init(keyPath: \Item.timestamp, ascending: true)]
request.returnsObjectsAsFaults = false
return request
}()
}
@FetchRequest(fetchRequest: Item.noFaultRequest)
private var items: FetchedResults<Item>
- The
animation
parameter in@FetchRequest
determines the animation effect for the interface update when data changes. Both.none
andnil
indicate no animation. - When displaying data in a List, the List ignores the animation effect set in
@FetchRequest
and uses the default animation of the component. - Developers can override the animation effect set in
@FetchRequest
with thewithAnimation
function, as shown below:
withAnimation(.none) {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
}
}
@FetchRequest
is SwiftUI’s encapsulation of NSFetchedResultsController, with its main functions and performance largely consistent with the latter.- Similar to NSFetchedResultsController,
@FetchRequest
only supports returning collections of NSManagedObject type and does not support other NSFetchRequestResult types (such as numbers, NSManagedObjectID, dictionaries). - The lifecycle of
@FetchRequest
is closely tied to the lifespan of the view. It begins fetching data when the view is loaded into the view hierarchy and stops when the view is removed. - Compared to fetching Core Data entity data in a ViewModel,
@FetchRequest
is more closely bound to the view’s lifecycle, with virtually no delay in initial loading. - After the initial data fetch,
@FetchRequest
(NSFetchedResultsController) updates the data set based on the merge information in the managed object context. Therefore, settings based onfetchLimit
may not always be effective in subsequent data changes. - For more information on the working principle of
@FetchRequest
, see SwiftUI and Core Data — Data Fetching. - FetchedResults is an encapsulation of NSFetchRequestResult, following the RandomAccessCollection protocol, allowing data access through subscripts.
- FetchedResults provides a
publisher
property, which sends the entire data set once the result set data changes. Therefore, it is not recommended to subscribe to this property to monitor dataset changes, in order to avoid redundant operations. - In SwiftUI development, it is recommended to encapsulate interfaces displaying
to-Many
data into separate views and fetch data through@FetchRequest
. This approach not only ensures the stability of the data fetching order but also responds promptly to data changes and makes view updates more efficient. For more details, please refer to Mastering Relationships in Core Data: Practical Application.
2. @SectionedFetchRequest
In SwiftUI, the @SectionedFetchRequest
property wrapper offers a convenient way to handle and display Core Data query results that are grouped according to specific criteria. This enables developers to present complex data structures in a grouped format within the user interface.
2.1 Basic Usage
Below is a basic usage example of @SectionedFetchRequest
:
@SectionedFetchRequest(
entity: Item.entity(),
sectionIdentifier: \Item.categoryID,
sortDescriptors: [
.init(keyPath: \Item.categoryID, ascending: true),
.init(keyPath: \Item.timestamp, ascending: true),
]
)
var sectionItems: SectionedFetchResults<Int16, Item>
ForEach(sectionItems) { section in
Section(header: Text("\(section.id)")) {
ForEach(section) { item in
Row(item: item)
}
}
}
This code demonstrates how to create a grouped SectionedFetchRequest
based on the categoryID
attribute of the Item
entity, and sorts it according to predefined rules.
SectionedFetchResults
is a structure with two generic parameters. The first parameter defines the type of attribute used for grouping identification (section identifier), while the second parameter specifies the type of the managed object entity.
2.2 Main Functions
@SectionedFetchRequest
allows developers to group query results based on specific attributes, enabling the presentation of these results in a grouped format within SwiftUI views.- Besides supporting grouping, its other characteristics are essentially similar to
@FetchRequest
.
2.3 Considerations and Tips
- Most considerations and tips applicable to
@FetchRequest
also apply to@SectionedFetchRequest
. - When using
@SectionedFetchRequest
, it is necessary to specify an attribute for grouping. This attribute type should be clearly suitable for segmentation, such as String or Int type. - To ensure the accuracy of the group order, when constructing
sortDescriptors
, the attribute used forsectionIdentifier
should be the primary sorting option:
sortDescriptors: [
.init(keyPath: \Item.categoryID, ascending: true),
.init(keyPath: \Item.timestamp, ascending: true),
]
- For attribute types that are difficult to group precisely, it is recommended to create a specific attribute for the entity for categorization. For example, you can add an Int16 type
year
attribute totimestamp
to facilitate grouping by year. - In SwiftUI, nested
ForEach
can affect the performance optimization of lazy containers. AddingSection
can prevent the recursive expansion ofForEach
and improve performance. For more details, refer to Demystify SwiftUI performance. For example, in the following code, ifSection
is removed, SwiftUI will build child views for each data item in the result set all at once:
ForEach(sectionItems) { section in
// Section(header: Text("\(section.id)")) {
ForEach(section) { item in
Row(item: item)
}
// }
}
3. @Query
In SwiftUI, @Query
is used to retrieve SwiftData entity data within views. It simplifies the process of fetching data from persistent storage and automatically updates the view when data changes.
3.1 Basic Usage
@Query(sort: \Item.timestamp, animation: .default)
private var items: [Item]
3.2 Considerations and Tips
@Query
can be seen as the SwiftData environment’s equivalent to@FetchRequest
. However, unlike@FetchRequest
,@Query
does not support dynamically modifying query predicates and sorting conditions within the view.- The comparison between
@FetchRequest
and@Query
is as follows:- NSFetchRequest in Core Data corresponds to FetchDescriptor in SwiftData.
- NSSortDescriptor corresponds to SortDescriptor (which can also be used with
@FetchRequest
). - NSPredicate corresponds to Predicate in the Foundation framework.
- In terms of parameters, the predicate in
@FetchRequest
corresponds to the filter in@Query
.
- In the
@Predicate
macro, direct calls to external methods, functions, or computed properties are not possible. The values should be computed outside the macro and then used as predicate conditions:
// Example 1: Incorrect approach
@Query
private var items: [Item]
init() {
let predicate = #Predicate<Item>{
$0.timestamp < Date.now // Cannot compile
}
_items = Query(
filter: predicate,
sort:\Item.timestamp,
order: .forward,
animation: .default
)
}
// Example 1: Correct approach
let now = Date.now
let predicate = #Predicate<Item>{
$0.timestamp < now
}
// Example 2: Incorrect approach
init(item: Item) {
let predicate = #Predicate<Item>{
$0.timestamp < item.timestamp // Cannot compile
}
_items = Query(
filter: predicate,
sort:\Item.timestamp,
order: .forward,
animation: .default
)
}
// Example 2: Correct approach
init(item: Item) {
let startDate = item.timestamp
let predicate = #Predicate<Item>{
$0.timestamp < startDate
}
}
- SwiftData does not offer functionality similar to NSFetchedResultsController for retrieving data outside the view and automatically updating the dataset based on data changes. For such needs, Persistent History Tracking might be considered. For more details, refer to How to Observe Data Changes in SwiftData using Persistent History Tracking.
The three property wrappers introduced above perform data filtering, sorting, and grouping operations at the SQLite end. This approach is more efficient and uses fewer system resources compared to using high-order functions in Swift for the same operations in memory.
4. @Namespace
The @Namespace
property wrapper is used to create a unique identifier, allowing for effective grouping and differentiation of views or elements.
4.1 Basic Usage
@Namespace private var namespace
4.2 Main Functions
- Each
@Namespace
property wrapper creates a unique identifier, which remains constant throughout its lifecycle after creation. @Namespace
is often combined with otherid
information to annotate views. This method allows for adding more identifiable information to views without changing theirid
.
4.3 Considerations and Tips
- After creating a
@Namespace
identifier, you can pass it to other views for use:
struct ParentView: View {
@Namespace var namespace
let id = "1"
var body: some View {
VStack {
Rectangle().frame(width: 200, height: 200)
.matchedGeometryEffect(id: id, in: namespace)
SubView3(namespace: namespace, id: id)
}
}
}
struct SubView: View {
let namespace: Namespace.ID
let id: String
var body: some View {
Rectangle()
.matchedGeometryEffect(id: id, in: namespace, properties: .size, isSource: false)
}
}
-
Although developers often use
@Namespace
in conjunction withmatchedGeometryEffect
, it is important to understand that@Namespace
solely plays the role of an identifier and does not directly participate in the actual implementation of geometric information processing or animation transitions. -
@Namespace
is not limited to use withmatchedGeometryEffect
but can also be used with other elements or view modifiers, such asaccessibilityRotorEntry
,AccessibilityRotorEntry
,accessibilityLinkedGroup
,prefersDefaultFocus
, anddefaultFocus
. -
In scenarios using
@Namespace
, a pattern often emerges where views are marked and view information is read in pairs:
// Example 1:
Rectangle().frame(width: 200, height: 200)
.matchedGeometryEffect(id: id, in: namespace) // Marking the view
Rectangle()
.matchedGeometryEffect(id: id, in: namespace, properties: .size, isSource: false) // Reading the geometric information of the view with specific namespace + id
// Example 2:
VStack {
TextField("email", text: $email)
.prefersDefaultFocus(true, in: namespace) // Marking the view that gets default focus in the namespace
SecureField("password", text: $password)
Button("login") {
...
}
}
.focusScope(namespace) // Reading the information of the view with default focus in the namespace and setting the focus on it
- It is permissible to apply multiple different
namespace + id
combinations to the same view. For example, in the code below, we used the sameid
but differentnamespaces
to annotate the sameTrendView
. This approach provides independent identifiers for two different accessibility rotors:
struct TrendsView: View {
let trends: [Trend]
@Namespace private var customRotorNamespace
@Namespace private var countSpace
var body: some View {
VStack {
ScrollViewReader { scrollView in
List {
ForEach(trends, id: \.id) { trend in
TrendView(trend: trend)
.accessibilityRotorEntry(id: trend.id, in: customRotorNamespace) // Identifier 1
.accessibilityRotorEntry(id: trend.id, in: countSpace) // Identifier 2
.id(trend.id)
}
}
.accessibilityRotor("Negative trends") {
ForEach(trends, id: \.id) { trend in
if !trend.isPositive {
AccessibilityRotorEntry(trend.message, trend.id, in: customRotorNamespace) {
scrollView.scrollTo(trend.id)
}
}
}
}
.accessibilityRotor("Count"){
ForEach(trends, id: \.id) { trend in
if trend.count % 2 == 0 {
AccessibilityRotorEntry("\(trend.count)", trend.id, in: countSpace) {
scrollView.scrollTo(trend.id)
}
}
}
}
}
}
}
}
For more information about
AccessibilityRotorEntry
, refer to Accessibility rotors in SwiftUI.
-
When using
ForEach
, we typically use the identifier provided byForEach
as theid
, and by combining differentnamespaces
, we provide multiple distinct identifiers for the same view or element. As shown in the example above. -
At any given time, there should only be one unique
namespace + id
combination. For instance, in the code below, if run, a warning will be generated because there are multiple views using the samenamespace + id
combination:
struct AView: View {
@Namespace var namespace
var body: some View {
VStack {
Rectangle()
.matchedGeometryEffect(id: "111", in: namespace)
Rectangle()
.matchedGeometryEffect(id: "111", in: namespace)
}
}
}
// Warning: Multiple inserted views in matched geometry group Pair<String, ID>(first: "111", second: SwiftUI.Namespace.ID(id: 88)) have `isSource: true`, results are undefined.
- When used in conjunction with
matchedGeometryEffect
, multiple views can share the geometric information of a marked view. In the following code, the two lowerRectangles
will overlap with the position of the firstRectangle
, as they share the position information of the view marked withnamespace + "111"
:
struct AView: View {
@Namespace var namespace
var body: some View {
VStack {
Rectangle()
.matchedGeometryEffect(id: "111", in: namespace)
Rectangle().fill(.red)
.matchedGeometryEffect(id: "111", in: namespace, properties: .position, isSource: false)
Rectangle().fill(.blue)
.matchedGeometryEffect(id: "111", in: namespace, properties: .position, isSource: false)
}
}
}
- When using
matchedGeometryEffect
in modal views, if the correct geometric information cannot be obtained, this is usually due to a known issue with SwiftUI, not a problem with@Namespace
. A solution is to place the view inside a navigation container. This ensures correct geometric information retrieval in modal views (such as sheet or fullscreenCover).
// Issue example: Unable to obtain geometric information
struct NaviTest: View {
@Namespace var namespace
@State var show = false
var body: some View {
VStack {
Button("Show") {
show.toggle()
}
.sheet(isPresented: $show) {
VStack {
Rectangle()
.fill(.cyan)
.matchedGeometryEffect(id: "1", in: namespace, properties: .size, isSource: false)
}
.frame(width: 300, height: 300)
}
Rectangle().fill(.orange).frame(width: 100, height: 200).matchedGeometryEffect(id: "1", in: namespace)
}
}
}
// Solution: Correct geometric information retrieval within a navigation container
NavigationStack {
VStack {
Button("Show") {
show.toggle()
}
.sheet(isPresented: $show) {
VStack {
Rectangle()
.fill(.cyan)
.matchedGeometryEffect(id: "1", in: namespace, properties: .size, isSource: false)
}
.frame(width: 300, height: 300)
}
Rectangle().fill(.orange).frame(width: 100, height: 200).matchedGeometryEffect(id: "1", in: namespace)
}
}
To learn more about the details of
matchedGeometryEffect
, refer to MatchedGeometryEffect – Part 1 (Hero Animations).
5. @Bindable
The @Bindable
property wrapper provides a convenient and efficient way to create binding (Binding) for mutable properties of observable (Observable) object instances.
5.1 Basic Usage
@Observable
class People {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
struct PeopleView: View {
@State var people = People(name: "fat", age: 18)
var body: some View {
VStack {
Text("\(people.name) \(people.age)")
PeopleName(people: people)
PeopleAge(people: people)
}
}
}
struct PeopleName: View {
@Bindable var people: People // Usage 1
var body: some View {
TextField("Name", text: $people.name)
}
}
struct PeopleAge: View {
let people: People
var body: some View {
@Bindable var people = people // Usage 2
TextField("Age:", value: $people.age, format: .number)
}
}
5.2 Considerations and Tips
@Bindable
is specifically used for types conforming to theObservation.Observable
protocol, applicable to those declared via the@Observable
or@Model
macro.- Currently, special care is needed when applying
@Bindable
to instances of SwiftData’s PersistentModel, especially when theautoSave
feature is activated, as it may lead to stability issues. It is expected that this issue will be resolved and improved in future updates.
Conclusion
To this point, we have introduced 16 different property wrappers in SwiftUI. Another article will explore the functionalities of the remaining property wrappers, so stay tuned.