Persistência local no iOS com SwiftUI: UserDefaults e introdução ao SwiftData/Core Data

Capítulo 8

Tempo estimado de leitura: 10 minutos

+ Exercício

O que é persistência local e quando usar cada abordagem

Persistência local é a capacidade do app de salvar informações no dispositivo para que elas continuem disponíveis após fechar o app, reiniciar o iPhone ou atualizar a interface. No iOS com SwiftUI, é comum separar em dois níveis:

  • Persistência simples (preferências e dados leves): ideal para configurações, flags, filtros e pequenos textos. Ferramentas: UserDefaults via @AppStorage e @SceneStorage.
  • Persistência estruturada (dados com relacionamento e histórico): ideal para listas de itens, registros, entidades e operações de CRUD. Ferramentas: SwiftData (moderno) ou Core Data (equivalente conceitualmente).

Uma regra prática: se você conseguir descrever o dado como “preferência do usuário” e ele for pequeno, use UserDefaults. Se o dado for “conteúdo do app” (itens, registros, histórico) e você precisar de consultas, consistência e evolução do modelo, use SwiftData/Core Data.

UserDefaults no SwiftUI com @AppStorage e @SceneStorage

Conceito: UserDefaults

UserDefaults é um armazenamento chave-valor simples. Ele funciona bem para tipos básicos como Bool, Int, Double, String, Data e coleções simples. No SwiftUI, @AppStorage faz a ponte: quando o valor muda, a View atualiza automaticamente.

Quando usar @AppStorage vs @SceneStorage

  • @AppStorage: persiste no dispositivo e permanece entre aberturas do app. Use para preferências do usuário (tema, nome, filtros padrão).
  • @SceneStorage: persiste o estado de uma cena (janela) e é restaurado quando a cena volta, útil para estado de navegação/seleção temporária. Não é a melhor escolha para preferências “globais” do app.

Passo a passo: salvar e aplicar um tema (claro/escuro) com @AppStorage

Exemplo de preferência simples: o usuário escolhe o tema do app. Vamos armazenar a escolha e aplicar no .preferredColorScheme.

import SwiftUI

enum AppTheme: String, CaseIterable {
    case system
    case light
    case dark

    var colorScheme: ColorScheme? {
        switch self {
        case .system: return nil
        case .light: return .light
        case .dark: return .dark
        }
    }

    var title: String {
        switch self {
        case .system: return "Sistema"
        case .light: return "Claro"
        case .dark: return "Escuro"
        }
    }
}

struct ThemeSettingsView: View {
    @AppStorage("app_theme") private var themeRawValue: String = AppTheme.system.rawValue

    private var theme: AppTheme {
        AppTheme(rawValue: themeRawValue) ?? .system
    }

    var body: some View {
        Form {
            Picker("Tema", selection: $themeRawValue) {
                ForEach(AppTheme.allCases, id: \ .rawValue) { theme in
                    Text(theme.title).tag(theme.rawValue)
                }
            }
        }
        .preferredColorScheme(theme.colorScheme)
    }
}

O que acontece aqui: o Picker escreve em @AppStorage (chave app_theme). Ao mudar, a View recalcula theme e aplica o esquema de cores.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Passo a passo: salvar o nome do usuário com @AppStorage

Para dados leves como um nome, @AppStorage é direto. Você pode usar isso para personalizar telas (“Olá, Ana”).

import SwiftUI

struct ProfileSettingsView: View {
    @AppStorage("user_name") private var userName: String = ""

    var body: some View {
        Form {
            TextField("Seu nome", text: $userName)
                .textInputAutocapitalization(.words)

            if !userName.isEmpty {
                Text("Olá, \(userName)!")
            }
        }
    }
}

Passo a passo: persistir filtros simples (ex.: mostrar apenas favoritos)

Filtros de lista são um caso clássico de persistência leve. Exemplo: um toggle “Somente favoritos”.

import SwiftUI

struct FilterSettingsView: View {
    @AppStorage("filter_only_favorites") private var onlyFavorites: Bool = false
    @AppStorage("filter_min_rating") private var minRating: Int = 0

    var body: some View {
        Form {
            Toggle("Somente favoritos", isOn: $onlyFavorites)

            Stepper("Nota mínima: \(minRating)", value: $minRating, in: 0...5)
        }
    }
}

Na tela que exibe a lista, você lê as mesmas chaves com @AppStorage e aplica o filtro sem precisar passar bindings por várias telas.

SceneStorage: lembrar seleção/aba temporária

Um uso comum é lembrar qual aba estava selecionada ou qual item estava expandido, sem transformar isso em “preferência do usuário”.

import SwiftUI

struct TabsView: View {
    @SceneStorage("selected_tab") private var selectedTab: String = "home"

    var body: some View {
        TabView(selection: $selectedTab) {
            Text("Home").tabItem { Text("Home") }.tag("home")
            Text("Ajustes").tabItem { Text("Ajustes") }.tag("settings")
        }
    }
}

Cuidados e limitações do UserDefaults

  • Não use para dados grandes (listas extensas, imagens, muitos registros).
  • Não é um banco de dados: não há consultas, índices, relacionamentos ou garantias fortes de consistência.
  • Chaves: padronize nomes (ex.: prefixo do app) e evite colisões.
  • Tipos complexos: se precisar salvar structs, você pode codificar em JSON (Codable) e salvar como Data, mas isso ainda não resolve consultas e evolução do modelo.

Introdução à persistência estruturada com SwiftData (conceitos equivalentes ao Core Data)

Por que SwiftData/Core Data

Quando seu app precisa armazenar uma coleção de itens (tarefas, notas, produtos) com operações de criar, ler, atualizar e excluir (CRUD), e você quer consistência e possibilidade de evolução do modelo, um banco local orientado a objetos como SwiftData/Core Data é mais adequado.

Conceitos essenciais (em nível introdutório)

ConceitoO que significaComo aparece no SwiftData/Core Data
ModeloDescrição do que será salvoClasses/entidades com propriedades
Entidade“Tipo” de dado persistidoEx.: TodoItem, Note
AtributosCampos da entidadeEx.: title, isDone, createdAt
ContextoAmbiente de trabalho para ler/escreverModelContext (SwiftData) / NSManagedObjectContext (Core Data)
PersistênciaSalvar alterações no armazenamentoSwiftData salva automaticamente em muitos casos; pode haver save() no Core Data
ConsultaBuscar dados com critérios@Query (SwiftData) / NSFetchRequest (Core Data)

Você pode pensar no contexto como uma “sessão” onde você cria/edita objetos e pede para persistir. No SwiftUI, o contexto é injetado no ambiente e usado nas Views.

CRUD de uma lista de itens com SwiftData (exemplo prático)

Objetivo do exemplo

Vamos persistir uma lista de itens (ex.: tarefas) com:

  • Create: adicionar item
  • Read: listar itens persistidos
  • Update: alternar concluído e editar título
  • Delete: remover item

Passo 1: criar o modelo (entidade) com @Model

No SwiftData, você define uma classe com @Model. Ela vira uma entidade persistida.

import Foundation
import SwiftData

@Model
final class TodoItem {
    var title: String
    var isDone: Bool
    var createdAt: Date

    init(title: String, isDone: Bool = false, createdAt: Date = .now) {
        self.title = title
        self.isDone = isDone
        self.createdAt = createdAt
    }
}

Dica: comece com atributos simples. Relacionamentos (1:N, N:N) podem ser adicionados depois, mas exigem mais cuidado com migração.

Passo 2: configurar o container do SwiftData no App

Você precisa informar ao app quais modelos serão persistidos. Isso cria o armazenamento e injeta o contexto no ambiente.

import SwiftUI
import SwiftData

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            TodoListView()
        }
        .modelContainer(for: [TodoItem.self])
    }
}

Passo 3: ler dados com @Query e exibir a lista

@Query busca automaticamente os itens persistidos e atualiza a UI quando há mudanças.

import SwiftUI
import SwiftData

struct TodoListView: View {
    @Environment(\ .modelContext) private var context

    @Query(sort: \TodoItem.createdAt, order: .reverse)
    private var items: [TodoItem]

    @State private var newTitle: String = ""

    var body: some View {
        VStack {
            HStack {
                TextField("Nova tarefa", text: $newTitle)
                    .textFieldStyle(.roundedBorder)

                Button("Adicionar") {
                    addItem()
                }
                .disabled(newTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
            }
            .padding()

            List {
                ForEach(items) { item in
                    TodoRow(item: item)
                }
                .onDelete(perform: deleteItems)
            }
        }
    }

    private func addItem() {
        let title = newTitle.trimmingCharacters(in: .whitespacesAndNewlines)
        let item = TodoItem(title: title)
        context.insert(item)
        newTitle = ""
    }

    private func deleteItems(at offsets: IndexSet) {
        for index in offsets {
            context.delete(items[index])
        }
    }
}

Read acontece via @Query. Create via context.insert. Delete via context.delete.

Passo 4: atualizar (Update) alternando concluído e editando título

No SwiftData, alterações em propriedades do modelo são rastreadas. Ao modificar item.isDone ou item.title, o sistema persiste conforme o ciclo de vida do contexto.

import SwiftUI
import SwiftData

struct TodoRow: View {
    @Bindable var item: TodoItem
    @State private var isEditing: Bool = false

    var body: some View {
        HStack {
            Button {
                item.isDone.toggle()
            } label: {
                Image(systemName: item.isDone ? "checkmark.circle.fill" : "circle")
            }
            .buttonStyle(.plain)

            if isEditing {
                TextField("Título", text: $item.title)
                    .textFieldStyle(.roundedBorder)
            } else {
                Text(item.title)
                    .strikethrough(item.isDone)
            }

            Spacer()

            Button(isEditing ? "OK" : "Editar") {
                isEditing.toggle()
            }
            .buttonStyle(.bordered)
        }
    }
}

Update aqui é “natural”: você altera a propriedade e a lista reflete a mudança. Em apps reais, você pode validar antes de permitir salvar (ex.: impedir título vazio).

Passo 5: adicionar um filtro persistido com UserDefaults + consulta

Um padrão útil é: preferências em @AppStorage e dados em SwiftData. Exemplo: um filtro “mostrar apenas pendentes” persistido no UserDefaults e aplicado na UI.

import SwiftUI
import SwiftData

struct TodoListView: View {
    @Environment(\ .modelContext) private var context

    @AppStorage("todo_show_only_pending") private var showOnlyPending: Bool = false

    @Query(sort: \TodoItem.createdAt, order: .reverse)
    private var items: [TodoItem]

    @State private var newTitle: String = ""

    private var filteredItems: [TodoItem] {
        showOnlyPending ? items.filter { !$0.isDone } : items
    }

    var body: some View {
        VStack {
            Toggle("Somente pendentes", isOn: $showOnlyPending)
                .padding([.horizontal, .top])

            HStack {
                TextField("Nova tarefa", text: $newTitle)
                    .textFieldStyle(.roundedBorder)
                Button("Adicionar") { addItem() }
                    .disabled(newTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
            }
            .padding(.horizontal)

            List {
                ForEach(filteredItems) { item in
                    TodoRow(item: item)
                }
                .onDelete(perform: deleteItems)
            }
        }
    }

    private func addItem() {
        let title = newTitle.trimmingCharacters(in: .whitespacesAndNewlines)
        context.insert(TodoItem(title: title))
        newTitle = ""
    }

    private func deleteItems(at offsets: IndexSet) {
        for index in offsets {
            context.delete(filteredItems[index])
        }
    }
}

Observação importante: ao deletar usando uma lista filtrada, você deve deletar o item correto (como no exemplo). Em cenários mais complexos, prefira deletar pelo id do item ou mapear índices com cuidado para evitar inconsistência.

Equivalência conceitual com Core Data (sem repetir implementação completa)

Se você encontrar projetos usando Core Data, a lógica é parecida:

  • Entidade: no Core Data costuma ser definida no arquivo de modelo (.xcdatamodeld) e representada por NSManagedObject.
  • Contexto: NSManagedObjectContext é o equivalente ao ModelContext.
  • Consultas: NSFetchRequest e predicados (NSPredicate) fazem o papel de @Query com filtros.
  • CRUD: criar (insert), ler (fetch), atualizar (alterar propriedades), deletar (delete) e salvar (context.save()).

Para iniciantes, o mais importante é entender o fluxo: modelo → contexto → operações CRUD → persistência.

Limitações, cuidados e consistência básica (migração e evolução do modelo)

Migração: o que é e por que importa

Migração é o processo de lidar com mudanças no modelo ao longo do tempo (ex.: você adiciona um campo novo, renomeia uma propriedade, muda um tipo). Em persistência estruturada, isso pode quebrar a leitura de dados antigos se não for tratado.

  • Mudanças simples (ex.: adicionar um atributo opcional) tendem a ser mais fáceis.
  • Mudanças destrutivas (ex.: renomear/remover atributo, mudar tipo) exigem estratégia de migração.

Boas práticas para reduzir dor de migração:

  • Evite renomear propriedades sem necessidade; prefira adicionar novas e manter compatibilidade quando possível.
  • Planeje campos opcionais para evoluções (ex.: notes: String?).
  • Teste atualização do app com dados existentes (simulador/dispositivo com dados já salvos).

Consistência básica: evitando estados inválidos

  • Validação: não insira itens com título vazio; normalize espaços.
  • Operações atômicas: ao fazer múltiplas alterações relacionadas, faça-as juntas no mesmo fluxo (ex.: criar item e ajustar campos antes de exibir).
  • Deleção em listas filtradas: garanta que o item deletado é o correto (como mostrado no exemplo).
  • Identidade: não dependa do índice como “identificador”; use o próprio objeto persistido (ou um id se necessário).

Quando NÃO usar SwiftData/Core Data

  • Para uma ou duas preferências simples (use @AppStorage).
  • Para cache de rede grande e descartável (considere arquivos/SQLite dedicado, dependendo do caso).
  • Quando você precisa sincronização complexa multi-dispositivo sem planejamento (exige arquitetura e regras de conflito).

Agora responda o exercício sobre o conteúdo:

Ao decidir entre @AppStorage (UserDefaults) e SwiftData/Core Data, qual critério melhor orienta a escolha?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

@AppStorage (UserDefaults) é indicado para preferências leves e simples. Para dados estruturados como itens, registros e histórico, que exigem CRUD, consultas e consistência, a melhor opção é SwiftData/Core Data.

Próximo capitúlo

Consumo de APIs no iOS com SwiftUI: URLSession, async/await e decodificação JSON

Arrow Right Icon
Capa do Ebook gratuito iOS para Iniciantes com SwiftUI: do zero ao primeiro app na App Store
57%

iOS para Iniciantes com SwiftUI: do zero ao primeiro app na App Store

Novo curso

14 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.