Migration Core Data vers SwiftData : guide étape par étape 2026

Guide complet pour migrer une application iOS de Core Data vers SwiftData avec des exemples pratiques, stratégies de coexistence et bonnes pratiques.

Guide de migration Core Data vers SwiftData pour développeurs iOS

SwiftData représente l'avenir de la persistance des données sur les plateformes Apple. Introduit à la WWDC 2023, ce framework offre une syntaxe Swift native et une intégration transparente avec SwiftUI. Pour les applications existantes basées sur Core Data, la migration constitue une étape stratégique vers un code plus moderne et maintenable.

Ce que couvre cet article

Ce guide détaille le processus complet de migration de Core Data vers SwiftData : analyse de compatibilité, conversion des modèles, stratégies de migration des données, et patterns de coexistence pour une transition progressive.

Évaluer la faisabilité de la migration

Avant de commencer la migration, une évaluation approfondie permet d'identifier les obstacles potentiels. Core Data et SwiftData partagent le même moteur de persistance SQLite, ce qui rend les données entièrement compatibles.

MigrationAssessment.swiftswift
// Checklist d'évaluation pour la migration

/*
 FONCTIONNALITÉS SUPPORTÉES PAR SWIFTDATA :
 ✅ Modèles simples avec propriétés basiques
 ✅ Relations one-to-one et one-to-many
 ✅ Propriétés optionnelles et valeurs par défaut
 ✅ Transformable attributes (via Codable)
 ✅ CloudKit synchronization (basic)
 ✅ Lightweight migrations automatiques
 ✅ Héritage de classe (iOS 26+)

 FONCTIONNALITÉS NÉCESSITANT ATTENTION :
 ⚠️ NSFetchedResultsController → @Query + observation manuelle
 ⚠️ NSCompoundPredicate → #Predicate avec logique combinée
 ⚠️ Prédicats dynamiques → Solutions de contournement nécessaires

 FONCTIONNALITÉS NON SUPPORTÉES :
 ❌ CloudKit Sharing avancé
 ❌ Derived attributes
 ❌ Fetched properties
*/

// Exemple de modèle Core Data typique à migrer
import CoreData

// Entité Core Data existante
class CDTask: NSManagedObject {
    @NSManaged var id: UUID
    @NSManaged var title: String
    @NSManaged var isCompleted: Bool
    @NSManaged var createdAt: Date
    @NSManaged var priority: Int16
    @NSManaged var category: CDCategory?
}

class CDCategory: NSManagedObject {
    @NSManaged var id: UUID
    @NSManaged var name: String
    @NSManaged var color: String
    @NSManaged var tasks: NSSet?
}

La compatibilité au niveau des données signifie que les utilisateurs conservent leurs informations existantes après la migration. Aucune perte de données ne survient si le processus est correctement exécuté.

Conversion des modèles Core Data en SwiftData

La première étape concrète consiste à convertir les entités Core Data en classes SwiftData. Xcode propose un outil automatique, mais comprendre le processus manuel reste essentiel.

TaskModel.swiftswift
import SwiftData

// Équivalent SwiftData de CDTask
@Model
final class Task {
    // Propriétés avec valeurs par défaut
    var id: UUID = UUID()
    var title: String = ""
    var isCompleted: Bool = false
    var createdAt: Date = Date()
    var priority: Int = 0

    // Relation optionnelle vers Category
    var category: Category?

    // Initializer explicite recommandé
    init(
        id: UUID = UUID(),
        title: String,
        isCompleted: Bool = false,
        createdAt: Date = Date(),
        priority: Int = 0,
        category: Category? = nil
    ) {
        self.id = id
        self.title = title
        self.isCompleted = isCompleted
        self.createdAt = createdAt
        self.priority = priority
        self.category = category
    }
}

Les différences clés avec Core Data incluent l'utilisation du macro @Model au lieu de NSManagedObject, et des types Swift natifs plutôt que des types Objective-C.

CategoryModel.swiftswift
import SwiftData

@Model
final class Category {
    var id: UUID = UUID()
    var name: String = ""
    var color: String = "blue"

    // Relation inverse avec delete rule
    @Relationship(deleteRule: .cascade, inverse: \Task.category)
    var tasks: [Task] = []

    init(id: UUID = UUID(), name: String, color: String = "blue") {
        self.id = id
        self.name = name
        self.color = color
    }
}
Mapping des types

Les types Core Data se convertissent directement : Int16 devient Int, NSSet devient [Model], et Date reste Date. Les attributs Transformable nécessitent l'adoption de Codable.

Configurer le ModelContainer

Le ModelContainer SwiftData remplace le NSPersistentContainer de Core Data. La configuration détermine où et comment les données sont stockées.

ModelContainerSetup.swiftswift
import SwiftData
import SwiftUI

@main
struct TaskManagerApp: App {
    // Configuration du container SwiftData
    var sharedModelContainer: ModelContainer = {
        // Schéma incluant tous les modèles
        let schema = Schema([
            Task.self,
            Category.self
        ])

        // Configuration avec options de stockage
        let modelConfiguration = ModelConfiguration(
            schema: schema,
            isStoredInMemoryOnly: false,
            // Utiliser le même store que Core Data
            url: URL.applicationSupportDirectory
                .appending(path: "TaskManager.sqlite")
        )

        do {
            return try ModelContainer(
                for: schema,
                configurations: [modelConfiguration]
            )
        } catch {
            fatalError("Impossible de créer ModelContainer: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}

Le point crucial réside dans l'URL du store : utiliser le même fichier SQLite que Core Data permet à SwiftData de lire les données existantes.

Stratégie de coexistence Core Data et SwiftData

Pour les applications complexes, une migration progressive via la coexistence des deux frameworks représente l'approche la plus sûre. Les deux stacks peuvent accéder au même fichier SQLite.

CoexistenceSetup.swiftswift
import CoreData
import SwiftData

// Configuration pour la coexistence
class PersistenceController {
    static let shared = PersistenceController()

    // Store partagé entre Core Data et SwiftData
    private let storeURL: URL = {
        let appSupport = FileManager.default
            .urls(for: .applicationSupportDirectory, in: .userDomainMask)
            .first!
        return appSupport.appending(path: "TaskManager.sqlite")
    }()

    // MARK: - Core Data Stack (existant)

    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "TaskManager")

        // Configurer pour utiliser le store partagé
        let description = NSPersistentStoreDescription(url: storeURL)
        description.setOption(
            true as NSNumber,
            forKey: NSPersistentHistoryTrackingKey
        )
        container.persistentStoreDescriptions = [description]

        container.loadPersistentStores { _, error in
            if let error = error as NSError? {
                fatalError("Core Data error: \(error)")
            }
        }

        return container
    }()

    // MARK: - SwiftData Stack (nouveau)

    lazy var swiftDataContainer: ModelContainer = {
        let schema = Schema([Task.self, Category.self])

        let config = ModelConfiguration(
            schema: schema,
            url: storeURL,
            // Désactiver les migrations automatiques en coexistence
            allowsSave: true
        )

        do {
            return try ModelContainer(for: schema, configurations: [config])
        } catch {
            fatalError("SwiftData error: \(error)")
        }
    }()
}
Synchronisation des modifications

En mode coexistence, les modifications effectuées par un framework ne sont pas immédiatement visibles par l'autre. Un rechargement explicite ou un redémarrage de l'application peut être nécessaire.

Migration des requêtes : de NSFetchRequest à @Query

La différence la plus significative concerne la façon de récupérer les données. SwiftUI utilise le property wrapper @Query pour remplacer @FetchRequest.

QueryMigration.swiftswift
import SwiftUI
import SwiftData

// ❌ Ancien pattern avec Core Data
struct OldTaskListView: View {
    @FetchRequest(
        sortDescriptors: [
            NSSortDescriptor(keyPath: \CDTask.createdAt, ascending: false)
        ],
        predicate: NSPredicate(format: "isCompleted == NO")
    )
    private var tasks: FetchedResults<CDTask>

    var body: some View {
        List(tasks) { task in
            Text(task.title)
        }
    }
}

// ✅ Nouveau pattern avec SwiftData
struct NewTaskListView: View {
    // @Query avec tri et filtre intégrés
    @Query(
        filter: #Predicate<Task> { !$0.isCompleted },
        sort: \Task.createdAt,
        order: .reverse
    )
    private var tasks: [Task]

    var body: some View {
        List(tasks) { task in
            TaskRowView(task: task)
        }
    }
}

struct TaskRowView: View {
    let task: Task

    var body: some View {
        HStack {
            // Indicateur de priorité
            Circle()
                .fill(priorityColor)
                .frame(width: 8, height: 8)

            VStack(alignment: .leading) {
                Text(task.title)
                    .font(.headline)

                if let category = task.category {
                    Text(category.name)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }

            Spacer()

            // Badge de date
            Text(task.createdAt, style: .date)
                .font(.caption2)
                .foregroundStyle(.tertiary)
        }
    }

    private var priorityColor: Color {
        switch task.priority {
        case 3: return .red
        case 2: return .orange
        case 1: return .yellow
        default: return .gray
        }
    }
}

Prêt à réussir tes entretiens iOS ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Gestion des prédicats dynamiques

Un défi majeur de SwiftData concerne les prédicats dynamiques. Contrairement à Core Data où les prédicats peuvent être modifiés à la volée, @Query nécessite des approches alternatives.

DynamicPredicates.swiftswift
import SwiftUI
import SwiftData

// Solution 1 : Utiliser @Query avec init personnalisé
struct FilteredTasksView: View {
    @Query private var tasks: [Task]

    // Créer la vue avec un filtre spécifique
    init(showCompleted: Bool, categoryId: UUID?) {
        // Construire le prédicat selon les paramètres
        var predicates: [Predicate<Task>] = []

        if !showCompleted {
            predicates.append(#Predicate { !$0.isCompleted })
        }

        if let categoryId {
            predicates.append(#Predicate { task in
                task.category?.id == categoryId
            })
        }

        // Combiner les prédicats
        let combinedPredicate: Predicate<Task>?
        if predicates.isEmpty {
            combinedPredicate = nil
        } else if predicates.count == 1 {
            combinedPredicate = predicates[0]
        } else {
            // Combiner manuellement pour AND logic
            combinedPredicate = #Predicate<Task> { task in
                !task.isCompleted && task.category?.id == categoryId
            }
        }

        _tasks = Query(
            filter: combinedPredicate,
            sort: \Task.createdAt,
            order: .reverse
        )
    }

    var body: some View {
        List(tasks) { task in
            TaskRowView(task: task)
        }
    }
}

// Solution 2 : Filtrage côté vue avec tous les résultats
struct SmartTaskListView: View {
    // Récupérer toutes les tâches
    @Query(sort: \Task.createdAt, order: .reverse)
    private var allTasks: [Task]

    // État du filtre
    @State private var searchText = ""
    @State private var showCompleted = false
    @State private var selectedCategory: Category?

    // Filtrage calculé
    private var filteredTasks: [Task] {
        allTasks.filter { task in
            // Filtre par texte
            let matchesSearch = searchText.isEmpty ||
                task.title.localizedCaseInsensitiveContains(searchText)

            // Filtre par statut
            let matchesStatus = showCompleted || !task.isCompleted

            // Filtre par catégorie
            let matchesCategory = selectedCategory == nil ||
                task.category?.id == selectedCategory?.id

            return matchesSearch && matchesStatus && matchesCategory
        }
    }

    var body: some View {
        NavigationStack {
            List(filteredTasks) { task in
                TaskRowView(task: task)
            }
            .searchable(text: $searchText)
            .toolbar {
                FilterMenu(
                    showCompleted: $showCompleted,
                    selectedCategory: $selectedCategory
                )
            }
        }
    }
}

Migrations de schéma versionnées

Lorsque le modèle de données évolue, SwiftData utilise VersionedSchema pour gérer les migrations complexes.

VersionedSchemas.swiftswift
import SwiftData

// Version 1 : Schéma initial
enum TaskSchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)

    static var models: [any PersistentModel.Type] {
        [Task.self, Category.self]
    }

    @Model
    final class Task {
        var id: UUID = UUID()
        var title: String = ""
        var isCompleted: Bool = false
        var createdAt: Date = Date()
        var category: Category?

        init(title: String) {
            self.title = title
        }
    }

    @Model
    final class Category {
        var id: UUID = UUID()
        var name: String = ""

        @Relationship(deleteRule: .cascade, inverse: \Task.category)
        var tasks: [Task] = []

        init(name: String) {
            self.name = name
        }
    }
}

// Version 2 : Ajout du champ priority et notes
enum TaskSchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)

    static var models: [any PersistentModel.Type] {
        [Task.self, Category.self]
    }

    @Model
    final class Task {
        var id: UUID = UUID()
        var title: String = ""
        var isCompleted: Bool = false
        var createdAt: Date = Date()
        // Nouvelles propriétés avec valeurs par défaut
        var priority: Int = 0
        var notes: String = ""
        var category: Category?

        init(title: String, priority: Int = 0) {
            self.title = title
            self.priority = priority
        }
    }

    @Model
    final class Category {
        var id: UUID = UUID()
        var name: String = ""
        // Nouvelle propriété
        var color: String = "blue"

        @Relationship(deleteRule: .cascade, inverse: \Task.category)
        var tasks: [Task] = []

        init(name: String, color: String = "blue") {
            self.name = name
            self.color = color
        }
    }
}

Le plan de migration définit l'ordre des versions et les éventuelles migrations personnalisées.

MigrationPlan.swiftswift
import SwiftData

enum TaskMigrationPlan: SchemaMigrationPlan {
    // Ordre chronologique des schémas
    static var schemas: [any VersionedSchema.Type] {
        [TaskSchemaV1.self, TaskSchemaV2.self]
    }

    // Étapes de migration
    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }

    // Migration V1 → V2 : lightweight (propriétés avec défauts)
    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: TaskSchemaV1.self,
        toVersion: TaskSchemaV2.self
    )
}

// Configuration du container avec migration
@main
struct TaskManagerApp: App {
    var sharedModelContainer: ModelContainer = {
        do {
            return try ModelContainer(
                for: TaskSchemaV2.Task.self, TaskSchemaV2.Category.self,
                migrationPlan: TaskMigrationPlan.self
            )
        } catch {
            fatalError("Migration échouée: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}
Migrations lightweight vs custom

SwiftData gère automatiquement les migrations lightweight (ajout de propriétés avec défauts, renommage, suppression). Les migrations complexes nécessitant une transformation de données utilisent MigrationStage.custom.

Remplacement de NSFetchedResultsController

Pour les listes avec sections ou observation fine des changements, @Query combiné avec l'extraction de données remplace NSFetchedResultsController.

SectionedResults.swiftswift
import SwiftUI
import SwiftData

struct SectionedTaskListView: View {
    @Query(sort: \Task.createdAt, order: .reverse)
    private var tasks: [Task]

    // Groupement par catégorie
    private var tasksByCategory: [(Category?, [Task])] {
        Dictionary(grouping: tasks) { $0.category }
            .map { ($0.key, $0.value) }
            .sorted { first, second in
                // Tâches sans catégorie en dernier
                guard let firstName = first.0?.name else { return false }
                guard let secondName = second.0?.name else { return true }
                return firstName < secondName
            }
    }

    var body: some View {
        List {
            ForEach(tasksByCategory, id: \.0?.id) { category, categoryTasks in
                Section(header: SectionHeader(category: category)) {
                    ForEach(categoryTasks) { task in
                        TaskRowView(task: task)
                    }
                }
            }
        }
    }
}

struct SectionHeader: View {
    let category: Category?

    var body: some View {
        HStack {
            if let category {
                Circle()
                    .fill(Color(category.color))
                    .frame(width: 12, height: 12)
                Text(category.name)
            } else {
                Text("Sans catégorie")
                    .foregroundStyle(.secondary)
            }
        }
    }
}

// Alternative : Groupement par date
struct DateGroupedTasksView: View {
    @Query(sort: \Task.createdAt, order: .reverse)
    private var tasks: [Task]

    private var tasksByDate: [(Date, [Task])] {
        let calendar = Calendar.current

        let grouped = Dictionary(grouping: tasks) { task in
            calendar.startOfDay(for: task.createdAt)
        }

        return grouped
            .map { ($0.key, $0.value) }
            .sorted { $0.0 > $1.0 }
    }

    var body: some View {
        List {
            ForEach(tasksByDate, id: \.0) { date, dateTasks in
                Section(header: Text(date, style: .date)) {
                    ForEach(dateTasks) { task in
                        TaskRowView(task: task)
                    }
                }
            }
        }
    }
}

Opérations CRUD avec ModelContext

Le ModelContext remplace le NSManagedObjectContext pour toutes les opérations de création, lecture, mise à jour et suppression.

CRUDOperations.swiftswift
import SwiftUI
import SwiftData

struct TaskManagementView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var tasks: [Task]
    @Query private var categories: [Category]

    @State private var newTaskTitle = ""
    @State private var selectedCategory: Category?

    var body: some View {
        NavigationStack {
            VStack {
                // Formulaire d'ajout
                AddTaskForm(
                    title: $newTaskTitle,
                    category: $selectedCategory,
                    categories: categories,
                    onAdd: addTask
                )

                // Liste des tâches
                List {
                    ForEach(tasks) { task in
                        TaskRowView(task: task)
                            .swipeActions(edge: .trailing) {
                                Button(role: .destructive) {
                                    deleteTask(task)
                                } label: {
                                    Label("Supprimer", systemImage: "trash")
                                }
                            }
                            .swipeActions(edge: .leading) {
                                Button {
                                    toggleCompletion(task)
                                } label: {
                                    Label(
                                        task.isCompleted ? "À faire" : "Terminé",
                                        systemImage: task.isCompleted ? "circle" : "checkmark"
                                    )
                                }
                                .tint(task.isCompleted ? .orange : .green)
                            }
                    }
                }
            }
            .navigationTitle("Tâches")
        }
    }

    // CREATE
    private func addTask() {
        guard !newTaskTitle.isEmpty else { return }

        let task = Task(
            title: newTaskTitle,
            category: selectedCategory
        )

        // Insertion dans le contexte
        modelContext.insert(task)

        // Sauvegarde explicite (optionnel - autosave activé par défaut)
        do {
            try modelContext.save()
        } catch {
            print("Erreur de sauvegarde: \(error)")
        }

        // Reset du formulaire
        newTaskTitle = ""
        selectedCategory = nil
    }

    // UPDATE
    private func toggleCompletion(_ task: Task) {
        // Modification directe - SwiftData track automatiquement
        task.isCompleted.toggle()

        // La sauvegarde automatique gère la persistance
    }

    // DELETE
    private func deleteTask(_ task: Task) {
        modelContext.delete(task)
    }
}

struct AddTaskForm: View {
    @Binding var title: String
    @Binding var category: Category?
    let categories: [Category]
    let onAdd: () -> Void

    var body: some View {
        VStack(spacing: 12) {
            TextField("Nouvelle tâche...", text: $title)
                .textFieldStyle(.roundedBorder)

            HStack {
                Picker("Catégorie", selection: $category) {
                    Text("Aucune").tag(nil as Category?)
                    ForEach(categories) { cat in
                        Text(cat.name).tag(cat as Category?)
                    }
                }
                .pickerStyle(.menu)

                Button("Ajouter", action: onAdd)
                    .buttonStyle(.borderedProminent)
                    .disabled(title.isEmpty)
            }
        }
        .padding()
    }
}

Tests unitaires avec SwiftData

Une stratégie de test robuste facilite la validation de la migration. SwiftData permet de créer des containers en mémoire pour les tests.

SwiftDataTests.swiftswift
import XCTest
import SwiftData
@testable import TaskManager

final class TaskModelTests: XCTestCase {
    var container: ModelContainer!
    var context: ModelContext!

    override func setUpWithError() throws {
        // Container en mémoire pour les tests
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        container = try ModelContainer(
            for: Task.self, Category.self,
            configurations: config
        )
        context = ModelContext(container)
    }

    override func tearDownWithError() throws {
        container = nil
        context = nil
    }

    func testCreateTask() throws {
        // Given
        let task = Task(title: "Test Task")

        // When
        context.insert(task)
        try context.save()

        // Then
        let descriptor = FetchDescriptor<Task>()
        let tasks = try context.fetch(descriptor)

        XCTAssertEqual(tasks.count, 1)
        XCTAssertEqual(tasks.first?.title, "Test Task")
    }

    func testTaskCategoryRelationship() throws {
        // Given
        let category = Category(name: "Work", color: "blue")
        let task = Task(title: "Meeting", category: category)

        // When
        context.insert(category)
        context.insert(task)
        try context.save()

        // Then
        XCTAssertEqual(task.category?.name, "Work")
        XCTAssertTrue(category.tasks.contains(task))
    }

    func testDeleteCategoryCascade() throws {
        // Given
        let category = Category(name: "Personal")
        let task1 = Task(title: "Task 1", category: category)
        let task2 = Task(title: "Task 2", category: category)

        context.insert(category)
        context.insert(task1)
        context.insert(task2)
        try context.save()

        // When
        context.delete(category)
        try context.save()

        // Then - cascade delete should remove tasks
        let descriptor = FetchDescriptor<Task>()
        let remainingTasks = try context.fetch(descriptor)

        XCTAssertEqual(remainingTasks.count, 0)
    }

    func testFilteredFetch() throws {
        // Given
        let task1 = Task(title: "Completed", isCompleted: true)
        let task2 = Task(title: "Pending", isCompleted: false)
        let task3 = Task(title: "Also Pending", isCompleted: false)

        [task1, task2, task3].forEach { context.insert($0) }
        try context.save()

        // When
        var descriptor = FetchDescriptor<Task>(
            predicate: #Predicate { !$0.isCompleted }
        )
        let pendingTasks = try context.fetch(descriptor)

        // Then
        XCTAssertEqual(pendingTasks.count, 2)
    }
}

Checklist de migration complète

Voici un résumé des étapes pour une migration réussie :

MigrationChecklist.swiftswift
/*
 PHASE 1 : PRÉPARATION
 □ Auditer les fonctionnalités Core Data utilisées
 □ Identifier les fonctionnalités non supportées par SwiftData
 □ Créer une branche de migration dédiée
 □ Sauvegarder les données de test

 PHASE 2 : CONVERSION DES MODÈLES
 □ Convertir les entités NSManagedObject en @Model
 □ Adapter les relations avec @Relationship
 □ Configurer les delete rules appropriées
 □ Ajouter les valeurs par défaut requises

 PHASE 3 : CONFIGURATION
 □ Créer le ModelContainer avec l'URL du store existant
 □ Configurer le schéma versionné si nécessaire
 □ Définir le plan de migration
 □ Tester en mode coexistence si applicable

 PHASE 4 : MIGRATION DU CODE
 □ Remplacer @FetchRequest par @Query
 □ Adapter les prédicats en #Predicate
 □ Migrer NSFetchedResultsController vers groupement manuel
 □ Convertir les opérations CRUD vers ModelContext

 PHASE 5 : VALIDATION
 □ Exécuter tous les tests unitaires
 □ Tester la migration avec des données réelles
 □ Vérifier les performances avec Instruments
 □ Valider la synchronisation CloudKit (si applicable)

 PHASE 6 : DÉPLOIEMENT
 □ Documenter les changements breaking
 □ Préparer un plan de rollback
 □ Déployer en TestFlight
 □ Monitorer les crashs post-déploiement
*/

Conclusion

La migration de Core Data vers SwiftData représente une évolution naturelle pour les applications iOS modernes. La compatibilité au niveau du store SQLite garantit la préservation des données utilisateur, tandis que la syntaxe Swift native simplifie considérablement le code.

Points clés à retenir

  • ✅ SwiftData et Core Data partagent le même moteur SQLite
  • ✅ La coexistence permet une migration progressive
  • @Query remplace @FetchRequest avec une syntaxe plus simple
  • ✅ Les prédicats dynamiques nécessitent des patterns alternatifs
  • VersionedSchema gère les évolutions de schéma
  • ✅ Les tests en mémoire facilitent la validation
  • ✅ iOS 26 apporte l'héritage de classes
  • ✅ Commencer les nouveaux projets avec SwiftData sauf besoins spécifiques Core Data

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#swiftdata
#core-data
#ios
#migration
#swift

Partager

Articles similaires