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
- @FetchRequestsimplifies the process of retrieving data from Core Data, allowing developers to define entity types, sort descriptors, and predicates to filter results.
- @FetchRequestautomatically 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, @FetchRequestoffers 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 @FetchRequestwithin a view. However, it is important to note that this method is not applicable within.onAppearand.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 @FetchRequestusing 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 animationparameter in@FetchRequestdetermines the animation effect for the interface update when data changes. Both.noneandnilindicate no animation.
- When displaying data in a List, the List ignores the animation effect set in @FetchRequestand uses the default animation of the component.
- Developers can override the animation effect set in @FetchRequestwith thewithAnimationfunction, as shown below:
withAnimation(.none) {
    let newItem = Item(context: viewContext)
    newItem.timestamp = Date()
    do {
        try viewContext.save()
    } catch {
    }
}- @FetchRequestis SwiftUI’s encapsulation of NSFetchedResultsController, with its main functions and performance largely consistent with the latter.
- Similar to NSFetchedResultsController, @FetchRequestonly supports returning collections of NSManagedObject type and does not support other NSFetchRequestResult types (such as numbers, NSManagedObjectID, dictionaries).
- The lifecycle of @FetchRequestis 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, @FetchRequestis 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 onfetchLimitmay 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 publisherproperty, 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-Manydata 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
- @SectionedFetchRequestallows 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 @FetchRequestalso 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 forsectionIdentifiershould 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 yearattribute totimestampto facilitate grouping by year.
- In SwiftUI, nested ForEachcan affect the performance optimization of lazy containers. AddingSectioncan prevent the recursive expansion ofForEachand improve performance. For more details, refer to Demystify SwiftUI performance. For example, in the following code, ifSectionis 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
- @Querycan be seen as the SwiftData environment’s equivalent to- @FetchRequest. However, unlike- @FetchRequest,- @Querydoes not support dynamically modifying query predicates and sorting conditions within the view.
- The comparison between @FetchRequestand@Queryis 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 @FetchRequestcorresponds to the filter in@Query.
 
- In the @Predicatemacro, 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 namespace4.2 Main Functions
- Each @Namespaceproperty wrapper creates a unique identifier, which remains constant throughout its lifecycle after creation.
- @Namespaceis often combined with other- idinformation to annotate views. This method allows for adding more identifiable information to views without changing their- id.
4.3 Considerations and Tips
- After creating a @Namespaceidentifier, 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 @Namespacein conjunction withmatchedGeometryEffect, it is important to understand that@Namespacesolely plays the role of an identifier and does not directly participate in the actual implementation of geometric information processing or animation transitions.
- 
@Namespaceis not limited to use withmatchedGeometryEffectbut 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 + idcombinations to the same view. For example, in the code below, we used the sameidbut differentnamespacesto 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 byForEachas 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 + idcombination. For instance, in the code below, if run, a warning will be generated because there are multiple views using the samenamespace + idcombination:
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 lowerRectangleswill 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 matchedGeometryEffectin 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
- @Bindableis specifically used for types conforming to the- Observation.Observableprotocol, applicable to those declared via the- @Observableor- @Modelmacro.
- Currently, special care is needed when applying @Bindableto instances of SwiftData’s PersistentModel, especially when theautoSavefeature 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.