-
Atelier de code : ajoutez la persistance des données avec SwiftData
Découvrez SwiftData en action tandis que nous ajoutons la persistance des données à une app existante. Nous vous montrons comment définir vos modèles de données et intégrer de manière transparente des données persistantes avec SwiftUI. Vous apprenez également des compétences fondamentales pour gérer l'état de votre app grâce à cette API expressive et déclarative.
Chapitres
- 0:00 - Introduction
- 1:05 - Identifier l’état pertinent
- 3:17 - Définir vos schémas
- 9:41 - Définir les relations entre les modèles
- 13:33 - Mettre à jour la couche vue
- 21:47 - Étapes suivantes
Ressources
Vidéos connexes
WWDC26
WWDC25
-
Rechercher dans cette vidéo…
-
-
3:39 - Convert Activity to a persistent model with @Model
import Foundation import SwiftData // SwiftData automatically generates Observable conformance @Model class Activity { var name: String var isComplete: Bool = false var dateCreated = Date.now var dateEdited = Date.now } -
6:06 - Add Codable conformance to TripCollection
enum TripCollection: String, CaseIterable, RawRepresentable, Codable { case springEscapes case summerVibes case fallGetaways case winterRetreats } -
10:32 - Set up model relationships between Trip, TripImage, and Activity
import Foundation import SwiftData @Model class Trip { var name: String var collection: TripCollection var photo: TripImage var thumbnailData: Data? @Relationship(deleteRule: .cascade, inverse: \Activity.trip) var activities: [Activity] = [] private(set) var creationDate = Date.now var subtitle: String? var isComplete: Bool = false } -
13:21 - Enable interoperability between your schema and SwiftUI views
import SwiftUI import SwiftData @main struct WishlistApp: App { let container: ModelContainer = { do { let modelContainer = try ModelContainer(for: Trip.self, Activity.self, TripImage.self, Goal.self, TripGoal.self, ActivityGoal.self) try SampleData.seedIfNeeded(in: modelContainer.mainContext) return modelContainer } catch { fatalError("Could not create model container: \(error)") } }() var body: some Scene { WindowGroup { ContentView() .preferredColorScheme(.dark) } .modelContainer(container) } } -
16:27 - Fetch achieved and upcoming goals
@Query(filter: #Predicate<Goal> { $0.isAchieved }, sort: \Goal.dateAchieved, order: .reverse) private var achievedGoals: [Goal] @Query(filter: #Predicate<Goal> { !$0.isAchieved }, sort: \Goal.sortOrder) private var upcomingGoals: [Goal] -
16:49 - Fetch recent trips
import SwiftUI import SwiftData struct RecentTripsPageView: View { // Fetch most recent trips in reverse chronological order @Query(FetchDescriptor<Trip>(sortBy: [SortDescriptor(\Trip.creationDate, order: .reverse)], fetchLimit: 5)) private var trips: [Trip] @Namespace private var namespace var body: some View { TabView { ForEach(trips) { trip in NavigationLink { TripDetailView(trip: trip) .navigationTransition( .zoom(sourceID: trip.id, in: namespace)) } label: { TripImageView(trip: trip) .overlay(alignment: .bottomLeading) { VStack(alignment: .leading) { Text("RECENTLY ADDED") .font(.subheadline) .fontWeight(.bold) .foregroundStyle(.limeGreen) Text(trip.name) .font(.title) .fontWidth(.expanded) .fontWeight(.medium) .foregroundStyle(.primary) } .padding(.horizontal) .padding(.bottom, 54) } .matchedTransitionSource(id: trip.id, in: namespace) } .buttonStyle(.plain) } } .tabViewStyle(.page) .containerRelativeFrame([.horizontal, .vertical]) { length, axis in if axis == .vertical { return length / 1.3 } else { return length } } } } -
17:26 - Dynamically construct a query in the initializer of TripCollectionView
init(tripCollection: TripCollection, cardSize: TripCard.Size, namespace: Namespace.ID) { _trips = Query(filter: #Predicate<Trip> { $0.collection == tripCollection }, sort: \Trip.name) self.tripCollection = tripCollection self.cardSize = cardSize self.namespace = namespace } -
18:13 - Search for trips and activities by name
import SwiftUI import SwiftData private struct SearchResultsListView: View { @Query(sort: \Trip.name) private var trips: [Trip] @Query(sort: \Activity.name) private var activities: [Activity] var searchText: String var namespace: Namespace.ID init(searchText: String, namespace: Namespace.ID) { self.searchText = searchText self.namespace = namespace if searchText.isEmpty { _trips = Query(FetchDescriptor(sortBy: [SortDescriptor(\Trip.creationDate, order: .reverse)], fetchLimit: 3)) _activities = Query(filter: #Predicate<Activity> { _ in false }) } else { // All trips whose name matches searchText, sorted lexicographically let tripSearchPredicate = #Predicate<Trip> { $0.name.localizedStandardContains(searchText) } _trips = Query(filter: tripSearchPredicate, sort: \Trip.name) // All matching activities that belong to a trip let activitySearchPredicate = #Predicate<Activity> { $0.trip != nil && $0.name.localizedStandardContains(searchText) } _activities = Query(filter: activitySearchPredicate, sort: \Activity.name) } } var body: some View { List { if !trips.isEmpty { TripSearchSectionView(trips: trips, namespace: namespace, title: searchText.isEmpty ? "Recent Trips" : "Trips") } if !activities.isEmpty { ActivitySearchSectionView(activities: activities) } } .overlay { if trips.isEmpty && activities.isEmpty { ContentUnavailableView( "No results for “\(searchText)”", systemImage: "magnifyingglass", description: Text("Check spelling or try a new search.") ) } } .listStyle(.plain) } } -
19:42 - Capture and report errors from ActivityItemView
var body: some View { HStack(alignment: .firstTextBaseline, spacing: 17) { Group { if isEditing { rowContentWhenEditing } else { rowContentWhenNotEditing } } .transition(.opacity.animation(.snappy)) .animation(.snappy, value: isEditing) } .onDisappear { do { try updateGoalAchievements() } catch { updateError = error reportError(error) } } .alert(error: $updateError) { // Customize the presentation of the error } } -
21:04 - Update dateEdited and propagate side effects on property changes
init(activity: Activity, isLast: Bool, isEditing: Bool) { activity.token = withContinuousObservation(options: .didSet) { event in _ = activity.name _ = activity.isComplete if event.matches(\Activity.name) { activity.dateEdited = .now } if event.matches(\Activity.isComplete) { activity.dateEdited = .now activity.trip?.isComplete = activity.trip?.activities.isEmpty == false && activity.trip?.activities.allSatisfy { $0.isComplete } == true } } self.activity = activity self.isLast = isLast self.isEditing = isEditing }
-