← Wszystkie wpisy

SwiftData w iOS 27: Observation i historia

SwiftData pojawił się w iOS 17 z dwoma sposobami obserwowania danych: @Query wewnątrz widoku SwiftUI albo ręczne podpinanie powiadomień na ModelContext dla wszystkiego pozostałego. Żaden z nich nie pokrywał przypadku, który dla aplikacji synchronizowanej ma znaczenie największe, czyli wiedzy o tym, kiedy inne urządzenie zmieniło magazyn. iOS 27 zamyka obie luki w jednym wydaniu. ResultsObserver czyni śledzenie zmian pełnoprawnym obiektem, który można trzymać poza widokiem, a HistoryObserver obserwuje trwałą historię SwiftData i inkrementuje obserwowalny licznik, gdy pojawiają się nowe transakcje, dzięki czemu kod synchronizujący może pobrać tylko najświeższe zmiany. iOS 27 dodaje obserwację jako prymityw, a nie jako efekt uboczny SwiftUI.1

To ujęcie pasuje do reszty tego klastra: SwiftData był tani na start i kosztowny w koordynacji. Obserwowanie pobrania poza hierarchią widoków oznaczało odtwarzanie ręcznie tego, co robi @Query. Utrzymanie magazynu w synchronizacji z zewnętrznym serwerem albo reagowanie na zapisy z rozszerzenia aplikacji oznaczało samodzielne przechodzenie przez trwałą historię i uzgadnianie transakcji. iOS 27 nadaje obu zadaniom nazwany typ zgodny z Observable, dzięki czemu ta sama maszyneria aktualizacji SwiftUI, która już napędza widoki, napędza także warstwę synchronizacji.

TL;DR / Kluczowe wnioski

  • ResultsObserver obserwuje i śledzi zmiany kolekcji modeli trwałych w kontekście modelu, dostarczając aktualizacji w czasie rzeczywistym, gdy dane bazowe się zmieniają. Jest Observable, więc widok SwiftUI aktualizuje się automatycznie, i działa poza widokiem, tam gdzie @Query nie sięga.2
  • HistoryObserver obserwuje trwałą historię SwiftData i udostępnia jedną obserwowalną właściwość, eventCounter, która inkrementuje się, gdy nadchodzą nowe transakcje. Filtruje się po typie modelu i autorze transakcji, a następnie wywołuje ModelContext.fetchHistory, aby odczytać zmiany. To uporządkowana odpowiedź na synchronizację z zewnętrznym serwerem lub reagowanie na zapisy rozszerzeń aplikacji.3
  • @Attribute(.codable) używa codable’owej reprezentacji właściwości do jej przechowywania, dając deklaratywny sposób na utrwalenie typu wartościowego Codable bez ValueTransformer.4
  • Wszystkie trzy są dostępne w iOS, iPadOS, macOS, Mac Catalyst, tvOS, visionOS i watchOS w wersji beta 27.0.234

Obserwowanie pobrania poza widokiem: ResultsObserver

@Query jest doskonały i jednocześnie ograniczony. Żyje w widoku SwiftUI, uruchamia się ponownie, gdy zmienia się jego predykat, i przekazuje widokowi tablicę. Ograniczeniem jest lokalizacja. Model widoku, koordynator synchronizacji, zadanie eksportu czy reconciler w tle mają dane, które się zmieniają, i nie mają @Query, na którym mogłyby się oprzeć. Przed iOS 27 ci wywołujący subskrybowali powiadomienia o zapisie z ModelContext i pobierali dane ponownie ręcznie, co stanowi ręczną rekonstrukcję dokładnie tego, co @Query robi już wewnętrznie.

ResultsObserver to nazwana odpowiedź iOS 27. Deklaracja mówi, czym on jest:2

final class ResultsObserver<Element, SectionName> where Element : PersistentModel, SectionName : Hashable

Klasa automatycznie monitoruje zmiany modeli pasujących do określonych kryteriów pobrania i utrzymuje kolekcję pobranych wyników, co czyni ją narzędziem do utrzymywania synchronizacji dowolnego konsumenta z danymi trwałymi, a nie tylko widoku.2 Konfiguruje się ją na dwa sposoby: za pomocą kompletnego FetchDescriptor albo za pomocą pojedynczych predykatów filtrujących i deskryptorów sortowania.2 Dwie ścieżki SectionName mają znaczenie dla list pogrupowanych; gdy podział na sekcje nie jest potrzebny, jako parametr typu SectionName przekazuje się Never.2

Korzyścią względem dawnego ręcznego podejścia jest zgodność z Observable. ResultsObserver jest Observable, co pozwala widokom SwiftUI aktualizować się automatycznie, gdy wyniki się zmieniają, tak samo jak obiekty modelu @Observable napędzają unieważnianie widoku (omówione w wnętrzu @Observable).2 Koordynator synchronizacji, który trzyma ResultsObserver, otrzymuje powiadomienia o zmianach bez pisania ani jednej linii NotificationCenter, a każdy widok odczytujący wyniki obserwatora renderuje się ponownie za darmo.

W sesji 274 Apple przedstawia ResultsObserver dla przypadku, którego @Query obsłużyć nie potrafi, pobierając i obserwując magazyn z dowolnego miejsca w aplikacji poprzez Swift Observation, w tym z obiektu stanu lub z gry, która nigdy nie dotyka SwiftUI.5

Watch on Apple Developer ↗
ResultsObserver wnosi obserwację w stylu zapytania do kodu poza widokami SwiftUI.

import SwiftData
import Observation

@Observable
final class ShoppingListModel {
    let observer: ResultsObserver<ShoppingItem, Never>

    init(context: ModelContext) {
        let descriptor = FetchDescriptor<ShoppingItem>(
            sortBy: [SortDescriptor(\.sortOrder)]
        )
        // No sectioning, so SectionName is Never.
        observer = ResultsObserver(context: context, fetchDescriptor: descriptor)
    }
}

Opublikowana dokumentacja Apple potwierdza deklaracje klas oraz powierzchnię konfiguracji (FetchDescriptor albo predykaty filtrujące i deskryptory sortowania, Never dla nazwy sekcji, gdy nie dzieli się na sekcje), lecz w chwili pisania pomija dokładne sygnatury inicjalizatorów, dlatego kształty wywołań w tych przykładach należy traktować jako poglądowe i potwierdzić etykiety parametrów względem SDK.

Zmiana mentalna polega na tym, że pobranie staje się obiektem, który się posiada i przekazuje, zamiast property wrappera uwięzionego w body widoku. @Query odpowiada na pytanie „co pokazuje ten widok?”. ResultsObserver odpowiada na pytanie „jaki jest obecny stan tego pobrania, gdziekolwiek je trzymam?”. To drugie pytanie zadaje w rzeczywistości wywołujący spoza widoku.

Reagowanie na zmiany historii: HistoryObserver

Lukę, którą zarówno @Query, jak i ResultsObserver pozostawiają otwartą, jest zmiana powstająca poza pobraniem w procesie. Za każdym razem, gdy magazyn jest zapisywany, SwiftData rejestruje transakcję historii opisującą, co się zmieniło, skąd zmiana pochodziła, oraz token, który ją identyfikuje. Utrzymanie magazynu w synchronizacji z zewnętrznym serwerem albo reagowanie na zapisy z rozszerzenia aplikacji oznaczało samodzielne przechodzenie przez tę trwałą historię. iOS 27 nadaje temu zadaniu nazwanego obserwatora.3

HistoryObserver to uporządkowana odpowiedź:3

final class HistoryObserver

Obserwator śledzi trwałą historię SwiftData i pozwala kodowi reagować, gdy dodawane są nowe transakcje.3 Jeśli potrzebne są jedynie pewne rodzaje zmian, filtruje się po typie modelu i autorze transakcji, dzięki czemu reaguje się na zapisy, na których zależy, a nie na każdą mutację magazynu.3

Cała powierzchnia to jedna obserwowalna właściwość, eventCounter. Gdy nowe transakcje pojawiają się w trwałej historii, licznik inkrementuje się; obserwuje się go, a przy każdej inkrementacji wywołuje API ModelContext.fetchHistory, aby odczytać tylko najświeższe zmiany.3 Tokeny historii znajdują się na samych transakcjach, więc fetchHistory zwraca tylko to, co nowe, zamiast ponownie skanować magazyn. Dzięki temu obsługa synchronizacji pozostaje szybka w miarę narastania historii.

import SwiftData
import Observation

// Illustrative call shapes; confirm parameter labels against the SDK.
let historyObserver = HistoryObserver(container: modelContainer, authors: "App")

// Observe eventCounter; on each increment, fetch and process the new history.
let token = withContinuousObservation(of: historyObserver.eventCounter) {
    Task { await processChanges() }
}

Filtrowanie po autorze transakcji to szczegół, który czyni synchronizację z serwerem poprawną. W przykładzie Apple obserwator przekazuje "App" jako autora, więc reaguje wyłącznie na zmiany dokonane przez aplikację i nie odtwarza z powrotem na serwer zmian, które pochodziły z serwera.3 Przy każdej inkrementacji krok processChanges wywołuje ModelContext.fetchHistory, aby odczytać nowe transakcje i je przesłać. Wzorce wieloprocesowe i synchronizacji zewnętrznej, które aplikacje z tego klastra rozwiązywały ręcznie napisanym podpinaniem historii (zob. dyscyplina schematu SwiftData), zyskują framework’owy szew, na którym mogą się wesprzeć.

Lekkie odczyty współgrające z historią

Gdy HistoryObserver obudzi już obsługę synchronizacji, kolejnym pytaniem jest, ile pracy wykonać. Naiwna obsługa pobiera ponownie dotknięte modele przy każdej inkrementacji, co hydratuje pełne obiekty modeli, których możliwe, że się nie potrzebuje. SwiftData daje dwa odczyty, które odpowiadają na węższe pytania bez materializowania czegokolwiek, i oba naturalnie współgrają z obserwacją historii.

ModelContext.fetchCount(_:) zwraca liczbę modeli pasujących do deskryptora pobrania jako zwykły Int, bez ładowania pasujących obiektów:6

func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel

ModelContext.fetchIdentifiers(_:) zwraca dopasowania jako [PersistentIdentifier], ponownie bez ładowania stojących za nimi modeli. Przeciążenie z batchSize strumieniuje te identyfikatory w porcjach dla dużych zbiorów wyników:6

func fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModel

Grupowe laboratorium SwiftData zasugerowało wzorzec, który wiąże te odczyty bezpośrednio z obserwacją historii. Gdy nadchodzi zmiana historii, pobiera się dotknięte identyfikatory i porównuje je z tym, co widok faktycznie wyświetla, zanim podejmie się decyzję o przeładowaniu, dzięki czemu unika się hydratowania obiektów, których się nie potrzebuje.6 Zmiana wiersza, na który użytkownik nie patrzy, nie przesuwa żadnych pikseli, a porównanie identyfikatorów mówi o tym za cenę wyszukania klucza, a nie pełnego pobrania. fetchCount odpowiada na jeszcze tańsze pytanie, czy w ogóle cokolwiek pasowało, co wystarcza, by zdecydować, czy plakietka lub stan pusty powinny się przełączyć.

Czyste utrwalanie typu wartościowego: @Attribute(.codable)

Trzeci dodatek jest niewielki i praktyczny. SwiftData natywnie przechowuje prymitywne typy Swift i relacje @Model, lecz właściwość, której typem jest niestandardowa wartość Codable (struktura przechowująca kilka pól, wyliczenie z wartościami powiązanymi), wymagała ValueTransformer i atrybutu .transformable(by:), co jest ceremoniałem Core Data przeciekającym z powrotem przez powierzchnię makra.

iOS 27 dodaje opcję przechowywania codable:4

static var codable: Schema.Attribute.Option { get }

Opcja używa codable’owej reprezentacji właściwości do jej przechowywania, więc typ wartościowy Codable jest utrwalany poprzez własną zgodność z Encodable/Decodable bez transformera do zarejestrowania.4 Stosuje się ją tak samo jak każdą inną opcję atrybutu:

import SwiftData

struct Coordinate: Codable {
    var latitude: Double
    var longitude: Double
}

@Model
final class Place {
    var name: String

    // Persisted via Coordinate's own Codable conformance.
    @Attribute(.codable) var location: Coordinate
}

Praktyczna zasada to sięgać po .codable, gdy właściwość jest samodzielną wartością Codable, która nie zasługuje na własną tabelę @Model. Para współrzędnych, mała struktura ustawień, wyliczenie z ładunkiem: to są dane, a nie encje, i .codable przechowuje je w miejscu poprzez reprezentację, którą już definiują, zamiast wymuszać transformer lub sztuczną relację.

Apple jasno mówi o kompromisach. Zawartość atrybutu codable jest dla SwiftData nieprzejrzysta, więc nie można jej użyć w predykatach do filtrowania wyników ani w deskryptorach sortowania, a zmiana kształtu typu codable (dodanie lub usunięcie właściwości) nie wyzwoli migracji, dlatego jego implementacja Codable musi pozostać zgodna w przód i wstecz.5 Apple przedstawia .codable jako wyjście awaryjne dla typów, których się nie posiada; dla typów własnych modelowanie ich jako modeli SwiftData lub obsługiwanych typów wartościowych utrzymuje sortowanie, filtrowanie i indeksowanie na tabeli.5

Kiedy po który sięgnąć

Te trzy dodatki odpowiadają na trzy różne pytania, a pytanie podpowiada, którego użyć.

  • Po ResultsObserver sięga się, gdy wywołujący spoza widoku potrzebuje żywego pobrania. Model widoku, koordynator, zadanie eksportu, cokolwiek, co ma zmieniające się dane i nie jest body SwiftUI. Wewnątrz widoku @Query pozostaje lżejszym narzędziem; obserwator zasługuje na swoje miejsce w chwili, gdy konsumentem nie jest widok.2
  • Po HistoryObserver sięga się, gdy synchronizuje się magazyn z czymś poza aplikacją. Zewnętrzny serwer albo rozszerzenie aplikacji zapisujące do tego samego magazynu. Obserwuje się eventCounter, filtruje po typie modelu i autorze transakcji oraz wywołuje ModelContext.fetchHistory przy każdej inkrementacji, aby odczytać tylko nowe transakcje.3
  • Po @Attribute(.codable) sięga się, gdy właściwość jest wartością Codable, a nie encją. Małe struktury i wyliczenia, które podróżują wraz ze swoim właścicielem. Jeśli typ potrzebuje własnej tożsamości, relacji lub zapytań, woła o @Model; jeśli to jedynie dane w miejscu, .codable pomija transformer.4

Oba obserwatory komponują się ze sobą. ResultsObserver utrzymuje pobranie w procesie żywym; HistoryObserver mówi, kiedy zdalny push uzasadnia podjęcie działania wobec tego, co się zmieniło. Aplikacja realnie synchronizująca wiele urządzeń używa obu, a po drodze używa .codable, by utrzymać kolumny typów wartościowych w ryzach.

FAQ

Czym ResultsObserver różni się od @Query?

@Query to property wrapper SwiftUI, który żyje wewnątrz widoku i zasila ten widok tablicą. ResultsObserver to samodzielna klasa, którą tworzy się i trzyma gdziekolwiek, w tym poza hierarchią widoków, i która obserwuje oraz śledzi zmiany kolekcji modeli trwałych w kontekście modelu.2 Ponieważ obserwator jest Observable, widok SwiftUI, który go odczytuje, nadal aktualizuje się automatycznie, więc pokrywa zarówno przypadek wewnątrz widoku, jak i przypadek modelu widoku lub koordynatora, do którego @Query nie sięga.2

Co HistoryObserver w istocie obserwuje?

Obserwuje trwałą historię SwiftData, zapis transakcji, które SwiftData zapisuje za każdym razem, gdy magazyn jest zapisywany.3 Udostępnia jedną obserwowalną właściwość, eventCounter, która inkrementuje się, gdy dostępne są nowe transakcje; można filtrować po typie modelu i autorze transakcji, tak by licznik poruszały jedynie zmiany, na których zależy.3 Przy każdej inkrementacji kod wywołuje API ModelContext.fetchHistory, aby odczytać nowe transakcje, co czyni go uporządkowaną obsługą synchronizacji z zewnętrznym serwerem lub reagowania na zapisy rozszerzeń aplikacji.3

Czy mogę używać ResultsObserver i HistoryObserver razem?

Tak, a synchronizowana aplikacja zwykle powinna. ResultsObserver utrzymuje pobranie w procesie aktualnym w miarę zmian lokalnego kontekstu; HistoryObserver ujawnia zmiany zapisane w trwałej historii, filtrowane po typie modelu i autorze transakcji.23 Oba są obiektami obserwowalnymi, na które można reagować z widoku SwiftUI lub innego obserwatora, więc wpasowują się w ten sam reaktywny przepływ bez osobnej obsługi powiadomień.23

Kiedy używać @Attribute(.codable) zamiast relacji?

.codable używa się, gdy właściwość jest samodzielnym typem wartościowym Codable, który nie ma niezależnej tożsamości, ponieważ opcja przechowuje właściwość poprzez jej własną codable’ową reprezentację.4 Relacji @Model używa się, gdy wartość jest prawdziwą encją z własnym cyklem życia, tożsamością lub zapytaniami. Linią podziału jest to, czy dana rzecz to dane należące do swojego właściciela, czy encja, do której odwołują się inne wiersze.

Pełny klaster Apple Ecosystem: dyscyplina schematu SwiftData dla kosztu migracji, na którym obserwacja się opiera; przewodnik po migracjach SwiftData dla maszynerii VersionedSchema i MigrationPlan; wnętrze @Observable dla modelu obserwacji, do którego te klasy się podłączają; wnętrze SwiftUI dla framework’owego podłoża pod spodem. Centrum znajduje się w serii Apple Ecosystem. Dla szerszego kontekstu iOS z agentami AI zob. przewodnik po tworzeniu agentów na iOS.

Bibliografia


  1. Apple Developer Documentation: SwiftData. Dokumentacja frameworka obejmująca @Model, ModelContext, ModelContainer, zapytania oraz dodatki obserwacji z iOS 27. 

  2. Apple Developer Documentation: ResultsObserver (iOS 27.0 beta). „Observes and tracks changes to a collection of persistent models in a model context.” Zadeklarowana jako final class ResultsObserver<Element, SectionName> where Element : PersistentModel, SectionName : Hashable; konfigurowalna za pomocą FetchDescriptor lub predykatów filtrujących i deskryptorów sortowania; Observable, więc widoki SwiftUI aktualizują się automatycznie; jako SectionName przekazuje się Never, gdy podział na sekcje nie jest potrzebny. 

  3. Apple, WWDC26 sesja 274, What’s new in SwiftData, oraz Apple Developer Documentation: HistoryObserver (iOS 27.0 beta). Zadeklarowana jako final class HistoryObserver. Wedle sesji 274 obserwuje trwałą historię SwiftData i „has a single observable property” (eventCounter); „when new transactions are available in the persistent history, the eventCounter increments” oraz „your code can observe the eventCounter and when it increments, use ModelContext.fetchHistory API to fetch the latest changes.” „Lets you filter by model type and transaction author”; przykład z sesji przekazuje "App" jako autora, by zmiany pochodzące z aplikacji nie były odtwarzane z powrotem na zewnętrzny serwer. 

  4. Apple Developer Documentation: codable (iOS 27.0 beta). „Uses the property’s codable representation to store the property.” Zadeklarowana jako static var codable: Schema.Attribute.Option { get }

  5. Apple, WWDC26 sesja 274, What’s new in SwiftData. Apple przedstawia ResultsObserver, który „fetches data from your SwiftData store and then observes your store for changes”, lecz „works anywhere in your app (independent of SwiftUI views) using Swift Observation”, i wymienia obiekt stanu lub grę napisaną w SceneKit jako przypadki, do których @Query nie sięga. 

  6. Apple Developer Documentation: fetchCount(_:) oraz fetchIdentifiers(_:) na ModelContext. fetchCount zadeklarowano jako func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel i „returns the number of models that match the criteria of the specified fetch descriptor.” fetchIdentifiers zadeklarowano jako func fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModel i zwraca „an array of persistent identifiers, where each identifier represents a single model that satisfies the criteria”; przeciążenie fetchIdentifiers(_:batchSize:) zwraca identyfikatory partiami jako FetchResultsCollection<PersistentIdentifier>. Źródło wzorca pobierz-dotknięte-identyfikatory-a-następnie-porównaj-z-widokiem: sparafrazowane z lokalnie transkrybowanego nagrania grupowego laboratorium SwiftData z WWDC 2026; Apple nie publikuje oficjalnych napisów do laboratoriów. 

Powiązane artykuły

Migracje SwiftData: lightweight kontra custom oraz kiedy V2 nie jest potrzebne

Model migracji SwiftData opiera się na VersionedSchema, MigrationStage i SchemaMigrationPlan. Większość zmian schematu n…

12 min czytania

Prawdziwym kosztem SwiftData jest dyscyplina schematu

API SwiftData to dwa makra. Koszt to to, co dzieje się po wydaniu aplikacji. Pola opcjonalne to tania migracja; dodanie …

14 min czytania

The Robots Are Taking Exams in My Search Console

First-party GSC data: 91% of 3.8M impressions fail a human-query filter. Exam questions, pasted errors, and agent sweeps…

10 min czytania