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:
UserDefaultsvia@AppStoragee@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.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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 comoData, 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)
| Conceito | O que significa | Como aparece no SwiftData/Core Data |
|---|---|---|
| Modelo | Descrição do que será salvo | Classes/entidades com propriedades |
| Entidade | “Tipo” de dado persistido | Ex.: TodoItem, Note |
| Atributos | Campos da entidade | Ex.: title, isDone, createdAt |
| Contexto | Ambiente de trabalho para ler/escrever | ModelContext (SwiftData) / NSManagedObjectContext (Core Data) |
| Persistência | Salvar alterações no armazenamento | SwiftData salva automaticamente em muitos casos; pode haver save() no Core Data |
| Consulta | Buscar 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 porNSManagedObject. - Contexto:
NSManagedObjectContexté o equivalente aoModelContext. - Consultas:
NSFetchRequeste predicados (NSPredicate) fazem o papel de@Querycom 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
idse 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).