Pular para conteúdo

Aula 19 - Persistência Local com SwiftData e Testes Unitários 💾

Objetivo Pedagógico

Objetivo: Implementar persistência de dados local com SwiftData utilizando a macro @Model, relacionamentos complexos, migrações de esquema e suíte de testes unitários com containers em memória.


📑 1. Fundamentos Teóricos & Análise Técnica

A Apple lançou o SwiftData para substituir a complexidade legada do Core Data, oferecendo uma API nativa em Swift focada em segurança de tipos, concorrência moderna e integração perfeita com SwiftUI.

A estrutura do SwiftData opera através de conceitos elegantes e expressivos: 1. A Macro @Model: - Converte diretamente qualquer classe Swift convencional em um esquema de dados relacional persistível, dispensando editores visuais de modelos .xcdatamodeld. - Gera automaticamente metadados para controle de integridade, transformando propriedades em colunas e mapeando relacionamentos (@Relationship) com regras de deleção em cascata (Cascade Delete). 2. ModelContainer e ModelContext: - ModelContainer: A espinha dorsal que gerencia o armazenamento físico no disco (usando SQLite sob o capô) e o controle de versões de esquema. - ModelContext: O espaço de trabalho (Scratchpad) onde objetos são criados, modificados e removidos antes da confirmação atômica no banco. 3. Estratégia de Testes Unitários com Containers em Memória: - Para garantir testes unitários rápidos e isolados, cria-se uma configuração de ModelContainer configurada com isStoredInMemoryOnly: true. Isso permite instanciar e destruir bancos SQLite na memória RAM sem deixar resquícios no disco do simulador.

📐 Arquitetura Conceitual & Diagrama de Fluxo

graph TD
    View["SwiftUI View (@Query)"] --> Context["ModelContext (Scratchpad em Memória)"]
    Context --> Container["ModelContainer (Schema & Config)"]
    Container --> SQLite["Armazenamento SQLite em Disco"]
    TestRunner["XCTest Suite"] --> TestContainer["ModelContainer (inMemoryOnly: true)"]
    TestContainer --> TestContext["ModelContext Isolado"]
    style View fill:#e1f5fe,stroke:#01579b
    style Context fill:#fff3e0,stroke:#e65100
    style Container fill:#f3e5f5,stroke:#7b1fa2
    style SQLite fill:#e8f5e9,stroke:#2e7d32
    style TestRunner fill:#ffebee,stroke:#c62828

🔍 Pilares e Diretrizes Técnicas

Nesta unidade, aprofundamos os seguintes conceitos fundamentais: - Macro Declarativa @Model: Elimina o código de mapeamento relacional transformando classes puras em esquemas SQLite tipados. - Propriedade @Query no SwiftUI: Alimenta automaticamente a interface do usuário mantendo-a atualizada sempre que há inserções no banco. - ModelContainer em Memória para Testes: Permite rodar centenas de testes unitários em paralelo sem contaminação de estado persistido. - Relacionamentos Fortemente Tipados: Declaração de cardinalidade 1-N e N-N com verificação estrita pelo compilador.


🛠️ 2. Implementação Prática em Persistência Nativa Apple, SwiftData e XCTest

Abaixo está a implementação técnica de referência, estruturada com padrões de engenharia de software e foco em robustez:

// AccountSwiftDataTests.swift (Modelo SwiftData e Suíte de Testes Unitários Isolados)
import XCTest
import SwiftData
@testable import SeuApp

@Model
final class UserAccount {
    @Attribute(.unique) var email: String
    var username: String
    var balance: Double

    init(email: String, username: String, balance: Double = 0.0) {
        self.email = email
        self.username = username
        self.balance = balance
    }
}

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

    @MainActor
    override func setUp() {
        super.setUp()
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        container = try! ModelContainer(for: UserAccount.self, configurations: config)
        context = container.mainContext
    }

    @MainActor
    func test_insercaoConta_devePersistirComSucesso() throws {
        let novaConta = UserAccount(email: "dev@apple.com", username: "AppleDev", balance: 500.0)
        context.insert(novaConta)
        try context.save()

        let descriptor = FetchDescriptor<UserAccount>(predicate: #Predicate { $0.email == "dev@apple.com" })
        let resultados = try context.fetch(descriptor)

        XCTAssertEqual(resultados.count, 1)
        XCTAssertEqual(resultados.first?.username, "AppleDev")
        XCTAssertEqual(resultados.first?.balance, 500.0)
    }
}

💡 Análise Passo a Passo do Código

  1. Configuração isStoredInMemoryOnly: true: Executa a suíte de persistência integralmente na memória RAM, garantindo velocidade extrema e determinismo.
  2. Predicado Tipado #Predicate: Macro que valida condições lógicas de consulta SQL em tempo de compilação.
  3. Anotação @Attribute(.unique): Impõe restrição de unicidade na coluna diretamente a nível de banco relacional.

🎯 3. Próximos Passos & Sequência Didática