@FetchRequest w SwiftUI — property wrapper do automatycznego pobierania danych z Core Data z reaktywnym aktualizowaniem interfejsu. Wrapper przyjmuje NSFetchRequest z predykatami filtrowania i sortowaniem, a wynik — tablica NSManagedObject — jest automatycznie aktualizowany przy każdej zmianie w kontekście Core Data. Według Apple Developer Documentation (2025), @FetchRequest wymaga managedObjectContext w środowisku SwiftUI i obsługuje dynamiczne predykaty, zapewniając reaktywne połączenie między Core Data a interfejsem użytkownika.
Najważniejsze
@FetchRequest — to property wrapper SwiftUI, który tworzy i zarządza FetchRequest z Core Data. Podczas inicjalizacji wrapper przyjmuje typ encji, tablicę NSSortDescriptor do sortowania i opcjonalny NSPredicate do filtrowania. Wynik jest przechowywany jako tablica obiektów określonego typu NSManagedObject i automatycznie aktualizowany przy zmianach w warstwie trwałości.
Kluczową cechą @FetchRequest jest reaktywność. Gdy dane w Core Data zmieniają się (save, insert, delete, update), SwiftUI automatycznie wykonuje ponowne pobieranie i przerysowuje wszystkie View używające tego fetch request. Deweloper nie musi ręcznie wywoływać refresh, subskrybować NSManagedObjectContextDidSaveNotification ani przeładowywać danych. SwiftUI zarządza całym cyklem synchronizacji.
Do działania @FetchRequest wymaga NSManagedObjectContext dostępnego w środowisku SwiftUI przez @Environment(\.managedObjectContext). Kontekst jest zwykle przekazywany z korzenia aplikacji, gdzie tworzony jest NSPersistentContainer. Standardowy wzorzec — użycie ViewModel lub App struct do skonfigurowania stosu Core Data i przekazania kontekstu przez modyfikator .environment().
@FetchRequest przyjmuje kilka parametrów do konfiguracji pobierania. SortDescriptors — tablica NSSortDescriptor określająca kolejność rekordów. Predicate — NSPredicate do filtrowania, na przykład pobieranie tylko zadań ze statusem „ukończone”. Animation — animacja do aktualizacji listy przy zmianach danych.
Predykaty Core Data używają formatu wyrażeń podobnych do SQL: NSPredicate(format: „status == %@”, „completed”). Można łączyć wiele warunków przez AND, OR i NOT. SortDescriptors określają kolejność: NSSortDescriptor(keyPath: \Task.dueDate, ascending: true). Do sortowania po wielu polach przekazywana jest tablica deskryptorów — pierwszy stosowany jest jako główny, pozostałe jako drugorzędne.
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 ?? "")
}
}
}
W przykładzie @FetchRequest wybiera wszystkie niezakończone zadania (isCompleted == NO), posortowane najpierw według daty (rosnąco), następnie według priorytetu (malejąco). FetchedResults<Task> — to automatycznie aktualizowana kolekcja, która zachowuje się jak tablica dla ForEach i List. Animacja .default zapewnia płynne pojawianie/znikanie rekordów przy zmianach.
@FetchRequest idealnie łączy się z List i ForEach do wyświetlania dynamicznych list Core Data. Wynik pobierania (FetchedResults) jest zgodny z protokołem RandomAccessCollection, więc można go używać bezpośrednio w List i ForEach. Przy dodawaniu, usuwaniu lub zmianie rekordów lista jest automatycznie przerysowywana z animacją.
Do usuwania rekordów używana jest metoda kontekstu delete. Do dodawania — utworzenie nowego NSManagedObject i wywołanie save. Wszystkie zmiany są wykonywane przez viewContext, który wraz z @FetchRequest zapewnia spójność danych. Do edycji używany jest @ObservedObject lub @Bindable dla konkretnego obiektu 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: "dodaj") { 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()
}
}
Ten przykład demonstruje pełny CRUD z @FetchRequest. Dodawanie: utworzenie Task przez kontekst i save(). Usuwanie: delete() na kontekście i save(). Aktualizacja: toggle właściwości i save(). Po każdym save() @FetchRequest automatycznie ponownie pobiera dane, a List aktualizuje się z animacją. Modyfikatory onDelete i onTapGesture zapewniają standardowe interakcje iOS.
Rozważmy pełny przykład aplikacji dziennika z @FetchRequest do pobierania wpisów. Formularz wyszukiwania używa dynamicznego predykatu: podczas wprowadzania tekstu FetchRequest automatycznie aktualizuje wyniki, filtrując wpisy według tytułu i zawartości. Sortowanie — według daty utworzenia, od najnowszych do najstarszych.
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)
}
}
Dziennik wpisów z wyszukiwarką: podczas wpisywania tekstu w polu searchable tworzony jest NSPredicate z CONTAINS[c] (wyszukiwanie bez uwzględniania wielkości liter) po polach title i content. Init przyjmuje searchText i tworzy FetchRequest z odpowiednim predykatem. Modyfikator Searchable automatycznie aktualizuje searchText, a @FetchRequest reaguje na zmianę predykatu i ponownie pobiera dane.
W przypadku bardziej złożonych scenariuszy z wieloma filtrami łączcie NSCompoundPredicate z logiką AND/OR. @FetchRequest obsługuje predykaty o dowolnej złożoności, w tym zagnieżdżone podzapytania (SUBQUERY) do filtrowania po relacjach. Należy jednak pamiętać: złożone predykaty wpływają na wydajność — dla dużych zbiorów danych (ponad 10 000 rekordów) używajcie indeksowania Core Data i ograniczajcie pobieranie przez fetchLimit.
@FetchRequest obsługuje dynamiczne predykaty przez parametr predicate, który można zmieniać w czasie wykonania. W tym celu należy utworzyć @FetchRequest ze zmienną-predykatem i przekazać ją przez Binding. Gdy użytkownik zmienia filtr — na przykład wybiera kategorię zadań — predykat jest aktualizowany, a pobieranie automatycznie uruchamiane ponownie.
Dynamiczne predykaty są implementowane przez @State lub @Published właściwość, która jest przekazywana jako Binding do inicjalizatora @FetchRequest. SwiftUI śledzi zmianę tego Binding i odtwarza NSFetchRequest z nowym predykatem, automatycznie aktualizując listę.
Często zadawane pytania
Dodajcie @Environment(\.managedObjectContext) var viewContext w korzeniowym View i przekarzcie go przez modyfikator .environment(\.managedObjectContext, context). Wszystkie podrzędne View używające @FetchRequest automatycznie otrzymają kontekst ze środowiska.
Tak, zmieńcie kontekst Core Data: dodajcie, usuńcie lub zaktualizujcie obiekt i wywołajcie save(). @FetchRequest automatycznie wykona ponowne pobieranie i zaktualizuje UI. Do wymuszenia aktualizacji bez save() użyjcie viewContext.refreshAllObjects().
@FetchRequest zwraca płaską tablicę obiektów. @SectionedFetchRequest grupuje wyniki według określonego key path w sekcje, podobnie jak UITableView z sectionNameKeyPath. Używajcie @SectionedFetchRequest do grouped-list z nagłówkami sekcji.
Do filtrowania po relacjach używajcie notacji kropkowej: NSPredicate(format: „category.name == %@”, „Work”). @FetchRequest automatycznie wykonuje JOIN w SQLite. Do dostępu do relacji przez wybrany obiekt używajcie zwykłych właściwości NSManagedObject.
@FetchRequest nie obsługuje paginacji bezpośrednio, ponieważ jest przeznaczony do wyświetlania wszystkich rekordów z reaktywną aktualizacją. Do paginacji używajcie NSFetchRequest z fetchLimit/fetchOffset i ładujcie dane ręcznie podczas przewijania listy.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również