@FetchRequest у SwiftUI — property wrapper для автоматичної вибірки даних із Core Data з реактивним оновленням інтерфейсу. Wrapper приймає NSFetchRequest з предикатами фільтрації та сортуваннями, а результат — масив NSManagedObject — автоматично оновлюється при будь-яких змінах у контексті Core Data. За даними Apple Developer Documentation (2025), @FetchRequest вимагає managedObjectContext в оточенні SwiftUI та підтримує динамічні предикати, забезпечуючи реактивний зв’язок між Core Data та користувацьким інтерфейсом.
Головне
@FetchRequest — це property wrapper SwiftUI, який створює та керує FetchRequest із Core Data. При ініціалізації wrapper приймає тип сутності, масив NSSortDescriptor для сортування та опціональний NSPredicate для фільтрації. Результат зберігається як масив об’єктів указаного типу NSManagedObject і автоматично оновлюється при змінах у persistence layer.
Ключова особливість @FetchRequest — реактивність. Коли дані в Core Data змінюються (save, insert, delete, update), SwiftUI автоматично виконує повторну вибірку та перемальовує всі View, що використовують цей fetch request. Розробнику не потрібно вручну викликати refresh, підписуватися на NSManagedObjectContextDidSaveNotification або перезавантажувати дані. SwiftUI керує всім циклом синхронізації.
Для роботи @FetchRequest потрібен NSManagedObjectContext, доступний в оточенні SwiftUI через @Environment(\.managedObjectContext). Контекст зазвичай передається з кореня застосунку, де створюється NSPersistentContainer. Стандартний патерн — використання ViewModel або App struct для налаштування стека Core Data та передачі контексту через модифікатор .environment().
@FetchRequest приймає кілька параметрів для налаштування вибірки. SortDescriptors — масив NSSortDescriptor, що визначає порядок записів. Predicate — NSPredicate для фільтрації, наприклад, вибірка лише завдань зі статусом «виконано». Animation — анімація для оновлення списку при змінах даних.
Предикати Core Data використовують формат SQL-подібних виразів: NSPredicate(format: «status == %@», «completed»). Можна комбінувати кілька умов через AND, OR та NOT. SortDescriptors задають порядок: NSSortDescriptor(keyPath: \Task.dueDate, ascending: true). Для сортування за кількома полями передається масив дескрипторів — перший застосовується як основний, решта як вторинні.
struct TaskListView: View {
@Environment(\.managedObjectContext) var viewContext
@FetchRequest(
sortDescriptors: [
NSSortDescriptor(keyPath: \.Task.dueDate, ascending: true),
NSSortDescriptor(keyPath: \.Task.priority, ascending: false)
],
predicate: NSPredicate(format: "isCompleted == NO"),
animation: .default
)
var tasks: FetchedResults<Task>
var body: some View {
List(tasks) { task in
Text(task.title ?? "")
}
}
}
У прикладі @FetchRequest вибирає всі незавершені завдання (isCompleted == NO), відсортовані спочатку за датою (зростання), потім за пріоритетом (спадання). FetchedResults<Task> — це колекція, що автоматично оновлюється та поводиться як масив для ForEach і List. Анімація .default забезпечує плавну появу/зникнення записів при змінах.
@FetchRequest ідеально поєднується з List та ForEach для відображення динамічних списків Core Data. Результат вибірки (FetchedResults) відповідає протоколу RandomAccessCollection, тому його можна використовувати безпосередньо в List та ForEach. При додаванні, видаленні або зміні записів список автоматично перемальовується з анімацією.
Для видалення записів використовується метод контексту delete. Для додавання — створення нового NSManagedObject та виклик save. Усі зміни виконуються через viewContext, який разом із @FetchRequest забезпечує консистентність даних. Для редагування використовується @ObservedObject або @Bindable для конкретного об’єкта Core Data.
struct TaskListWithActions: View {
@Environment(\.managedObjectContext) var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \.Task.createdAt,
ascending: false)]
)
var tasks: FetchedResults<Task>
var body: some View {
List {
ForEach(tasks) { task in
HStack {
Text(task.title ?? "")
Spacer()
Image(systemName: task.isCompleted
? "checkmark.circle.fill"
: "circle")
}
.onTapGesture { toggleTask(task) }
}
.onDelete(perform: deleteTasks)
}
.toolbar {
Button(systemImage: "plus") { addTask() }
}
}
private func addTask() {
let newTask = Task(context: viewContext)
newTask.title = "New task \(tasks.count + 1)"
newTask.createdAt = Date()
try? viewContext.save()
}
private func toggleTask(_ task: Task) {
task.isCompleted.toggle()
try? viewContext.save()
}
private func deleteTasks(at offsets: IndexSet) {
for index in offsets {
viewContext.delete(tasks[index])
}
try? viewContext.save()
}
}
Цей приклад демонструє повний CRUD із @FetchRequest. Додавання: створення Task через контекст і save(). Видалення: delete() на контексті та save(). Оновлення: перемикання властивості та save(). Після кожного save() @FetchRequest автоматично перезапитує дані, і List анімовано оновлюється. Модифікатори onDelete та onTapGesture надають стандартні взаємодії iOS.
Розглянемо повноцінний приклад застосунку-журналу з @FetchRequest для вибірки записів. Форма пошуку використовує динамічний предикат: при введенні тексту FetchRequest автоматично оновлює результати, фільтруючи записи за заголовком та вмістом. Сортування — за датою створення, від нових до старих.
struct JournalEntryView: View {
@Environment(\.managedObjectContext) var viewContext
@State private var searchText = ""
private var searchPredicate: NSPredicate? {
guard !searchText.isEmpty else { return nil }
return NSPredicate(format: "title CONTAINS[c] %@", searchText)
}
@FetchRequest var entries: FetchedResults<JournalEntry>
init(searchText: String) {
let sort = [NSSortDescriptor(keyPath: \.JournalEntry.date,
ascending: false)]
let pred: NSPredicate? = searchText.isEmpty
? nil
: NSPredicate(format: "title CONTAINS[c] %@ OR content CONTAINS[c] %@",
searchText, searchText)
self._entries = FetchRequest(
sortDescriptors: sort,
predicate: pred,
animation: .default
)
}
var body: some View {
List(entries) { entry in
VStack(alignment: .leading) {
Text(entry.title ?? "").font(.headline)
Text(entry.content ?? "").font(.subheadline)
.lineLimit(2)
}
}
.searchable(text: $searchText)
}
}
Журнал записів із пошуком: при введенні тексту в searchable-поле створюється NSPredicate із CONTAINS[c] (регістронезалежний пошук) за полями title та content. Init приймає searchText та створює FetchRequest із відповідним предикатом. Searchable-модифікатор автоматично оновлює searchText, а @FetchRequest реагує на зміну предиката та перезапитує дані.
Для складніших сценаріїв із множинними фільтрами комбінуйте NSCompoundPredicate з логікою AND/OR. @FetchRequest підтримує предикати будь-якої складності, включаючи вкладені підзапити (SUBQUERY) для фільтрації за relationship. Однак пам’ятайте: складні предикати впливають на продуктивність — для великих наборів даних (понад 10 000 записів) використовуйте індексацію Core Data та обмежуйте вибірку через fetchLimit.
@FetchRequest підтримує динамічні предикати через параметр predicate, який можна змінювати під час виконання. Для цього потрібно створити @FetchRequest зі змінною-предикатом і передати її через Binding. Коли користувач змінює фільтр — наприклад, вибирає категорію завдань — предикат оновлюється, і вибірка автоматично перезапускається.
Динамічні предикати реалізуються через @State або @Published властивість, яка передається як Binding в ініціалізатор @FetchRequest. SwiftUI відстежує зміну цього Binding і перестворює NSFetchRequest із новим предикатом, автоматично оновлюючи список.
Часті запитання
Додайте @Environment(\.managedObjectContext) var viewContext у кореневий View та передайте його через модифікатор .environment(\.managedObjectContext, context). Усі дочірні View, що використовують @FetchRequest, автоматично отримають контекст з оточення.
Так, змініть контекст Core Data: додайте, видаліть або оновіть об’єкт і викличте save(). @FetchRequest автоматично виконає повторну вибірку та оновить інтерфейс. Для примусового оновлення без save() використовуйте viewContext.refreshAllObjects().
@FetchRequest повертає плаский масив об’єктів. @SectionedFetchRequest групує результати за вказаним key path у секції, аналогічно UITableView із sectionNameKeyPath. Використовуйте @SectionedFetchRequest для grouped-списків із заголовками секцій.
Для фільтрації за relationship використовуйте крапкову нотацію: NSPredicate(format: «category.name == %@», «Work»). @FetchRequest автоматично виконує JOIN у SQLite. Для доступу до relationship через вибраний об’єкт використовуйте звичайні властивості NSManagedObject.
@FetchRequest не підтримує пагінацію безпосередньо, оскільки призначений для відображення всіх записів із реактивним оновленням. Для пагінації використовуйте NSFetchRequest із fetchLimit/fetchOffset та підвантажуйте дані вручну під час прокручування списку.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.