Swift: Operações de Arquivo e Dados do Swift
A persistência de dados é uma capacidade central dos apps -- desde salvar configurações do usuário até armazenar em cache dados de rede. Dominar a E/S de arquivos e a codificação/decodificação garante que seus dados sobrevivam ao reinício do app.
1. O Que Você Vai Aprender
- Usar FileManager para criar, ler, escrever e gerenciar arquivos
- Codificação/decodificação JSON para tipos personalizados usando o protocolo Codable
- Configuração avançada de JSONEncoder e JSONDecoder
- Armazenar preferências do usuário com UserDefaults
- O mecanismo de sandbox de arquivos e a obtenção de caminhos de diretórios comuns
2. Uma História Real de uma Desenvolvedora Independente
(1) Problema: Todos os Dados Perdidos Após Reiniciar o App
Alice lançou seu primeiro app iOS de lista de tarefas. Os usuários adoraram, mas havia uma falha fatal: feche o app e reabra, e todos os itens da lista desapareciam. Os usuários deixaram avaliações de uma estrela na App Store.
// Wrong approach: data only lives in memory
var todos: [TodoItem] = [
TodoItem(title: "Buy coffee"),
TodoItem(title: "Write weekly report"),
]
Os dados da lista de Alice eram armazenados em um array. Quando o app fechava, a memória era liberada e os dados eram perdidos.
(2) A Solução com Codable + FileManager
struct TodoItem: Codable {
let title: String
var isDone: false
}
let encoder = JSONEncoder()
if let data = try? encoder.encode(todos) {
let url = FileManager.default.documentsDirectory.appendingPathComponent("todos.json")
try? data.write(to: url)
}
Na próxima inicialização, leia do arquivo -- os dados são salvos permanentemente.
(3) Benefícios: Dados do Usuário Nunca Perdidos
| Dimensão | Armazenamento em Memória | Persistência em Arquivo |
|---|---|---|
| Reinício do app | Dados perdidos | Dados restaurados |
| Capacidade de armazenamento | RAM limitada | Espaço em disco |
| Compartilhamento de dados | Não suportado | Exportável/backup |
| Uso offline | Não dependente | Totalmente offline |
| Velocidade de restauração | Instantânea | Nível de milissegundos |
3. Gerenciamento de Arquivos com FileManager
FileManager é a interface do sistema de arquivos do Swift, fornecendo operações como criar, ler, mover e excluir arquivos.
(1) Diretórios Comuns
graph TB
A["FileManager.default"] --> B["documentsDirectory"]
A --> C["cachesDirectory"]
A --> D["temporaryDirectory"]
B --> E["Dados do usuário, backup no iCloud"]
C --> F["Arquivos de cache, sistema pode limpar"]
D --> G["Arquivos temporários, excluídos a qualquer momento"]
| Diretório | Finalidade | Backup? | Sistema Limpa? |
|---|---|---|---|
| documentsDirectory | Documentos e dados do usuário | Sim | Não |
| cachesDirectory | Arquivos de cache | Não | Sim |
| temporaryDirectory | Arquivos temporários | Não | Sim, a qualquer momento |
▶ Exemplo: Obtendo e Criando Diretórios
// ============================================
// Getting common directory paths and creating subdirectories
// ============================================
import Foundation
let fm = FileManager.default
// 1. Get the documents directory
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
print("Documents directory: \(docs.path)")
// 2. Get the caches directory
let caches = fm.urls(for: .cachesDirectory, in: .userDomainMask).first!
print("Caches directory: \(caches.path)")
// 3. Create a subdirectory
let dataDir = docs.appendingPathComponent("MyAppData")
if !fm.fileExists(atPath: dataDir.path) {
try? fm.createDirectory(at: dataDir, withIntermediateDirectories: true)
print("Created directory: \(dataDir.path)")
}
Saída:
TEXT 📖 Somente leituraDocuments directory: /Users/alice/Library/Developer/.../Documents Caches directory: /Users/alice/Library/Developer/.../Caches Created directory: /Users/alice/Library/Developer/.../Documents/MyAppData
(2) Escrevendo e Lendo Arquivos
FileManager fornece métodos básicos para operações de arquivo, combinados com write e init(contentsOf:) de Data para E/S de arquivo.
▶ Exemplo: Escrevendo e Lendo um Arquivo de Texto
// ============================================
// Writing and reading a text file
// ============================================
import Foundation
let fm = FileManager.default
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
// 1. Write text
let fileURL = docs.appendingPathComponent("note.txt")
let content = "Hello, Swift File I/O!"
try? content.write(to: fileURL, atomically: true, encoding: .utf8)
print("Write successful: \(fileURL.lastPathComponent)")
// 2. Read text
if let readContent = try? String(contentsOf: fileURL, encoding: .utf8) {
print("Read content: \(readContent)")
}
// 3. Check if file exists
print("File exists: \(fm.fileExists(atPath: fileURL.path))")
// 4. Get file attributes
if let attrs = try? fm.attributesOfItem(atPath: fileURL.path) {
let size = attrs[.size] as? Int ?? 0
print("File size: \(size) bytes")
}
Saída:
TEXT 📖 Somente leituraWrite successful: note.txt Read content: Hello, Swift File I/O! File exists: true File size: 21 bytes
4. Codable e JSON
Codable é uma combinação de protocolos (Encodable + Decodable) introduzida no Swift 4.0 que torna a codificação e decodificação JSON simples para tipos personalizados.
(1) O Protocolo Codable
graph LR
A["Codable"] --> B["Encodable: codificar para dados"]
A --> C["Decodable: decodificar de dados"]
B --> D["JSONEncoder().encode(obj)"]
C --> E["JSONDecoder().decode(Tipo.self, from: dados)"]
D --> F["Data -> escrever no arquivo"]
E --> G["Data do arquivo -> objeto tipado"]
| Protocolo | Método | Finalidade |
|---|---|---|
| Encodable | func encode(to: Encoder) |
Codificar um objeto para Data |
| Decodable | init(from: Decoder) |
Decodificar de Data para um objeto |
| Codable | Ambos combinados | Codificável e decodificável |
▶ Exemplo: Codificação e Decodificação JSON de um Objeto Personalizado
// ============================================
// Using Codable to JSON encode/decode a Person
// ============================================
import Foundation
struct Person: Codable {
let name: String
let age: Int
let email: String
}
// 1. Encode object to JSON
let alice = Person(name: "Alice", age: 28, email: "alice@example.com")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let jsonData = try? encoder.encode(alice),
let jsonString = String(data: jsonData, encoding: .utf8) {
print("Encoded JSON:")
print(jsonString)
}
// 2. Decode from JSON to object
let json = """
{"name":"Bob","age":35,"email":"bob@example.com"}
""".data(using: .utf8)!
let decoder = JSONDecoder()
if let bob = try? decoder.decode(Person.self, from: json) {
print("\nDecoded object: \(bob.name), age \(bob.age)")
}
Saída:
TEXT 📖 Somente leituraJSON codificado: { "nome" : "Alice", "idade" : 28, "email" : "alice@exemplo.com" } Objeto decodificado: Bob, idade 35
(2) Objetos Aninhados e Chaves Personalizadas
JSON do mundo real frequentemente tem estruturas aninhadas e nomes de chave diferentes. O Codable lida com isso usando CodingKeys e tipos aninhados.
▶ Exemplo: Análise de JSON Complexo
// ============================================
// Nested object + custom JSON key name mapping
// ============================================
import Foundation
struct User: Codable {
let id: Int
let fullName: String
let address: Address
let tags: [String]
// Custom key name mapping (JSON snake_case -> Swift camelCase)
enum CodingKeys: String, CodingKey {
case id
case fullName = "full_name"
case address
case tags
}
}
struct Address: Codable {
let city: String
let country: String
}
let json = """
{
"id": 1001,
"full_name": "Charlie Wang",
"address": { "city": "Shanghai", "country": "China" },
"tags": ["developer", "swift"]
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
if let user = try? decoder.decode(User.self, from: json) {
print("User: \(user.fullName) (ID: \(user.id))")
print("City: \(user.address.city)")
print("Tags: \(user.tags.joined(separator: ", "))")
}
Saída:
TEXT 📖 Somente leituraUser: Charlie Wang (ID: 1001) City: Shanghai Tags: developer, swift
5. UserDefaults e Persistência de Dados
UserDefaults é adequado para armazenar pequenas quantidades de preferências do usuário. Ele se integra automaticamente com a sincronização do iCloud e não requer gerenciamento manual de caminho de arquivo.
(1) Armazenando Dados no UserDefaults
| Tipo de Armazenamento | Tipo Swift | Método UserDefaults |
|---|---|---|
| Inteiro | Int | set(_:forKey:) + integer(forKey:) |
| Float | Double | set(_:forKey:) + double(forKey:) |
| Booleano | Bool | set(_:forKey:) + bool(forKey:) |
| String | String | set(_:forKey:) + string(forKey:) |
| Array | [Any] | set(_:forKey:) + array(forKey:) |
| Dicionário | [String: Any] | set(_:forKey:) + dictionary(forKey:) |
| Data | Data | set(_:forKey:) + data(forKey:) |
▶ Exemplo: Salvando e Lendo Configurações do Usuário
// ============================================
// Saving user preferences with UserDefaults
// ============================================
import Foundation
let defaults = UserDefaults.standard
// 1. Save settings
defaults.set("Alice", forKey: "username")
defaults.set(true, forKey: "isLoggedIn")
defaults.set(3, forKey: "launchCount")
defaults.set(23.5, forKey: "lastTemperature")
// 2. Read settings
let username = defaults.string(forKey: "username") ?? "Guest"
let isLoggedIn = defaults.bool(forKey: "isLoggedIn")
let launchCount = defaults.integer(forKey: "launchCount")
let temperature = defaults.double(forKey: "lastTemperature")
print("User: \(username)")
print("Logged in: \(isLoggedIn)")
print("Launch count: \(launchCount)")
print("Last temperature: \(temperature)°C")
// 3. Remove a key
defaults.removeObject(forKey: "lastTemperature")
print("After removal: \(defaults.double(forKey: "lastTemperature"))")
Saída:
TEXT 📖 Somente leituraUser: Alice Logged in: true Launch count: 3 Last temperature: 23.5°C After removal: 0.0
(2) Salvando Objetos Complexos no UserDefaults com Codable
UserDefaults não pode armazenar diretamente objetos Codable, mas pode armazenar Data -- codifique primeiro, depois armazene.
▶ Exemplo: Armazenando um Tipo Personalizado no UserDefaults
// ============================================
// Codable + UserDefaults for storing complex objects
// ============================================
import Foundation
struct Settings: Codable {
var theme: String = "light"
var fontSize: Int = 14
var enableNotifications: Bool = true
}
let defaults = UserDefaults.standard
let encoder = JSONEncoder()
let decoder = JSONDecoder()
// Save
var settings = Settings()
settings.theme = "dark"
settings.fontSize = 16
if let data = try? encoder.encode(settings) {
defaults.set(data, forKey: "appSettings")
print("Settings saved")
}
// Read
if let data = defaults.data(forKey: "appSettings"),
let loaded = try? decoder.decode(Settings.self, from: data) {
print("Theme: \(loaded.theme)")
print("Font size: \(loaded.fontSize)")
print("Notifications: \(loaded.enableNotifications)")
}
Saída:
TEXT 📖 Somente leituraSettings saved Theme: dark Font size: 16 Notifications: true
6. Exemplo Completo: Gerenciador de Lista de Tarefas
// ============================================
// Full example: Persistent to-do list manager
// Features: CRUD + JSON file persistence
// ============================================
import Foundation
// 1. To-do item model
struct TodoItem: Codable {
let id: UUID
var title: String
var isCompleted: Bool
let createdAt: Date
init(title: String) {
self.id = UUID()
self.title = title
self.isCompleted = false
self.createdAt = Date()
}
}
// 2. To-do manager
class TodoManager {
private var items: [TodoItem] = []
private let fileURL: URL
init() {
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
fileURL = docs.appendingPathComponent("todos.json")
load()
}
func add(title: String) {
items.append(TodoItem(title: title))
save()
}
func toggle(id: UUID) {
if let index = items.firstIndex(where: { $0.id == id }) {
items[index].isCompleted.toggle()
save()
}
}
func showAll() {
for item in items {
let status = item.isCompleted ? "Done" : "Todo"
print("[\(status)] \(item.title)")
}
}
private func save() {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let data = try? encoder.encode(items) {
try? data.write(to: fileURL)
}
}
private func load() {
if let data = try? Data(contentsOf: fileURL),
let decoded = try? JSONDecoder().decode([TodoItem].self, from: data) {
items = decoded
}
}
}
// 3. Usage example
let manager = TodoManager()
manager.add(title: "Learn Swift Codable")
manager.add(title: "Write file I/O tutorial")
manager.add(title: "Publish app update")
manager.showAll()
print("--- One completed ---")
// In practice, you'd toggle an item here
> Saída:
> ```text
> [Todo] Learn Swift Codable
> [Todo] Write file I/O tutorial
> [Todo] Publish app update
> --- One completed ---
> ```
---
## ❓ Perguntas Frequentes
> **P: Como escolher entre UserDefaults e armazenamento em arquivo?** **R: UserDefaults é adequado para pares chave-valor simples abaixo de 100KB (preferências do usuário, tokens). Use armazenamento em arquivo (JSON/banco de dados) para dados estruturados ou acima de 100KB. UserDefaults tem desempenho ruim e não suporta atualizações parciais.**
> **P: Como controlar o formato de saída do JSONEncoder?** **R: Defina encoder.outputFormatting como .prettyPrinted (legível por humanos), .sortedKeys (ordenação estável) ou .withoutEscapingSlashes (escape reduzido).**
> **P: Devo usar String ou URL para caminhos de arquivo?** **R: Sempre use URL. URL suporta concatenação de caminho com `appendingPathComponent`, recuperação de atributos de arquivo e compatibilidade multiplataforma. Caminhos String são propensos a erros e não têm esses recursos.**
> **P: Como devo usar try? e try! em operações de arquivo?** **R: Operações de arquivo têm alta probabilidade de falha (disco cheio, permissão negada, arquivo não encontrado). Sempre use `try?` ou do-catch. `try!` trava o app completamente em caso de falha.**
> **P: Quais informações o attributesOfItem do FileManager pode fornecer?** **R: Tamanho do arquivo, data de criação, data de modificação, permissões, proprietário, se é um diretório e mais. Acesse os valores com chaves como `attrs[.size]`, `attrs[.creationDate]`, etc.**
> **P: O que é o mecanismo de sandbox do iOS?** **R: Cada app só pode acessar seus próprios diretórios de sandbox (Documents, Caches, tmp) e não pode ler/escrever nos sistemas de arquivos de outros apps. URLs obtidas via FileManager estão dentro do escopo da sandbox.**
---
## 📖 Resumo
- FileManager fornece a interface do sistema de arquivos; os diretórios comuns são documents, caches e temporary
- Codable (Encodable + Decodable) permite codificação/decodificação JSON para tipos personalizados
- JSONEncoder/JSONDecoder suportam saída formatada e mapeamento de nomes de chave personalizados
- UserDefaults é adequado para armazenamento de pequenas preferências, mas não para dados complexos ou grandes
- Objetos complexos podem ser codificados para Data primeiro, depois armazenados no UserDefaults
- Operações de arquivo devem tratar erros -- disco cheio, permissão negada, etc. são sempre possíveis
---
## 📝 Exercícios
1. **Básico:** Use FileManager para criar um diretório "MinhasNotas" em documentDirectory, escreva um arquivo saudacao.txt com o conteúdo "Olá, E/S de Arquivos!" e depois leia e imprima.
2. **Intermediário:** Crie uma struct Livro (com titulo, autor, ano, isbn), salve-a em um arquivo usando JSONEncoder, depois leia de volta com JSONDecoder e verifique a consistência dos dados.
3. **Desafio:** Implemente um mecanismo simples de armazenamento KV `ArmazenamentoSimples` baseado em um arquivo JSON, suportando métodos genéricos `definir(chave: String, valor: Codable)` e `obter<T>(chave: String) -> T?`, com dados persistidos como um dicionário no arquivo.