Descomplicando o Swift Data: CRUD fácil

Lucas Daniel
Apple Developer Academy | UFPE
7 min readApr 11, 2024

Utilizando a nova biblioteca da Apple para persistência de dados de forma simples

Apesar de já ter desenvolvido para outros dispositivos, utilizando outras bibliotecas, ter adentrado no ecossistema de desenvolvimento da Apple Developer Academy | UFPE me trouxe uma nova perspectiva do quão diferente e simples pode ser codificar coisas que não são tão corriqueiras com outras tecnologias. Observei isso ao criar meu primeiro CRUD utilizando o framework SwiftUI e a biblioteca Swift Data.

Definição de CRUD

Um CRUD nada mais é do que um acrônimo para Create, Read, Update e Delete. Logo, o CRUD pode ser considerado como uma sequência de funções de um sistema que trabalha com banco de dados, seja ele na sua máquina ou na nuvem.

Contexto e Container

Para efeitos de exemplificação, não vou abordar neste artigo os conceitos de contexto e contêiner, que são essenciais para a utilização desta biblioteca. Ao invés disso, deixarei outro artigo (em inglês) que explica perfeitamente a definição e utilização destes dois conceitos na implementação de um código utilizando o Swift Data, que podem ser acessado ao clicar aqui.

Criando o nosso Model

Antes de mais nada, devemos efetuar nossos procedimentos do CRUD sobre um model, que pode ser considerada como uma representação abstrata do mundo real. Para este artigo, nós faremos um pequeno aplicativo que liste todos os livros que você queria ler ou comprar, por exemplo. Logo, precisaremos apenas de uma model, que no caso será o próprio livro. Desse modo, podemos criar nosso model do seguinte jeito:

import Foundation
import SwiftData

@Model
class Book {
var title: String
var author: String
var publisher: String
var summary: String

init(title: String, author: String, publisher: String, summary: String = "") {
self.title = title
self.author = author
self.publisher = publisher
self.summary = summary
}
}

Portanto, cada livro em nosso app terá quatro atributos: título, autor(a), editora e resumo. Além disso, ao inicializar o projeto, também é necessário indicar um modifier, o .modelContainer, isto é, um método na nossa grupo de janelas (WindowGroup) da nossa struct que inicializa o nosso app (usamos @main na SwiftData_CrupApp para sinalizar a struct raiz que inicializa!).

Ainda sobre o .modelContainer vale salientar que este método adiciona um container de modelo de persistência específico (Book) ao ambiente da aplicação, permitindo que as views dentro desse grupo de janelas acessem e manipulem os dados de Book. Isso fica da seguinte forma:

import SwiftUI
import SwiftData

@main
struct SwiftData_CRUDApp: App {
var body: some Scene {
WindowGroup {
BookListView()
}
.modelContainer(for: Book.self)
}
}

Criando as views e manipulando nosso model através das operações de CRUD

Read: Na tela inicial, podemos ter a listagem de todos os nossos livros já cadastrados. Portanto, a primeira operação que utilizaremos do CRUD será o read, isto é, a visualização dos itens que estão armazenados.

(PS: Além dessa parte de UI, usaremos partes do código que estão descritas abaixo, caso queira rodar precisará copiá-las também)

Para a view de listagem dos livros, temos esse trecho que detalharei funcionamento em seguida:

import SwiftUI
import SwiftData

struct BookListView: View {
@Environment(\.modelContext) private var context
@Query(sort: \Book.title) private var books: [Book]
@State private var createNewBook = false

var body: some View {
NavigationStack{
Group{
if books.count != 0 {
List{
ForEach(books) { book in
NavigationLink{
EditBookView(book: book)
} label: {
HStack(spacing: 16) {
VStack(alignment: .leading) {
Text(book.title).font(.title2)
HStack(spacing: 8) {
Text(book.author).foregroundStyle(.secondary)
Text("Editora " + book.publisher).foregroundStyle(.secondary)
}
}
}
}
}
.onDelete{ indexSet in
indexSet.forEach{ index in
let book = books[index]
context.delete(book)
}
}
}
}
else {
ContentUnavailableView("Insira o seu primeiro livro!", systemImage: "book")
}
}
.listStyle(.plain)
.navigationTitle("Meus Livros")
.toolbar {
Button {
createNewBook = true
} label: {
Image(systemName: "plus.circle.fill")
.imageScale(.large)
}
}
.sheet(isPresented: $createNewBook) {
NewBookView()
}
}
}
}

#Preview {
BookListView()
.modelContainer(for: Book.self, inMemory: true)
}

De antemão, observe que temos a mesma view sem livro algum e com mais de um livro (dois estados):

A operação de Read, do nosso CRUD, ou seja, a ler os dados do nosso model, é feita pelo trecho de código abaixo, onde o property wrapper @Query é utilizado para selecionar todos os livros que já foram inseridos na base de dados do nosso app. Além disso, o parâmetro sort é utilizado para ordenar a coleção de livros que é retornada pelo property wrapper, onde o título do livro é utilizado como valor de ordenação do parâmetro.

@Query(sort: \Book.title) private var books: [Book]

Em relação a operação Create, no caso da tela inicial não ter nenhum livro, iremos colocar somente uma mensagem e imagem de um livro na tela (“Insira o seu primeiro livro”) a partir de uma View que o SwiftUI nos fornece, a ContentUnavailableView.

Por outro lado, ao listar ao menos um livro passamos a utilizar a propriedade Read vista acima. No caso da lista fazia, utilizaremos a operação Create, que pode ser feita através da view NewBookView, codificada abaixo e que é chamada na BookListView pelo botão que tem o símbolo de mais (+), o nosso toolbar. Segue abaixo o código da tela que é chamada, sobre a BookListView (a principal), ao tocar nesse botão de mais:

(PS: A nossa variável summary da model, é inicializada vazia ao colocarmos um novo livro, ela só é modificada dentro da tela de Edição, que veremos em breve!)

import SwiftUI

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

@State private var title = ""
@State private var author = ""
@State private var publisher = ""
var body: some View {
NavigationStack {
Form {
TextField("Título do livro", text: $title)
TextField("Autor do livro", text: $author)
TextField("Editora do livro", text: $publisher)
Button("Criar"){
let newBook = Book(title: title, author: author, publisher: publisher)
context.insert(newBook)
dismiss()
}
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
.disabled(title.isEmpty || author.isEmpty || publisher.isEmpty)
.navigationTitle("Novo livro")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading){
Button("Cancelar"){
dismiss()
}
}
}

}
}
}
}

#Preview {
NewBookView()
}

Para inserir um livro utilizamos o método insert, que existe dentro do contexto do model, cujo instanciamos na primeira linha e que utiliza o property wrapper @Environment. A chamada do método fica context.insert(newBook). Logo, para adicionar no banco uma instância de um livro, sempre chamamos o método insert, que neste código pertence a uma ação de um botão.

Operação Update: Para atualizar algum dos livros, basta clicar no livro desejado e alterar algum campo do model, que aparece na view. Nesse caso, não utilizamos nenhum método, pois o próprio SwiftData abstrai a alteração dos dados do model e entende que se você modifica diretamente aquele model, você teve a intenção. Logo, não há a necessidade de utilizar algum método para o update dos valores. Isso é feito tacitamente.

Abaixo temos a nossa struct que nos permite editar um livro, uma vez que adicionado:

import SwiftUI

struct EditBookView: View {
@Environment(\.dismiss) private var dismiss
let book: Book

@State private var title = ""
@State private var author = ""
@State private var publisher = ""
@State private var summary = ""

var body: some View {
VStack(alignment: .leading){
LabeledContent {
TextField("", text: $title)
} label: {
Text("Título").foregroundStyle(.secondary)
}
LabeledContent {
TextField("", text: $author)
} label: {
Text("Autor(a)").foregroundStyle(.secondary)
}
LabeledContent {
TextField("", text: $publisher)
} label: {
Text("Editora").foregroundStyle(.secondary)
}
Divider()
Text("Summary").foregroundStyle(.secondary)
TextEditor(text: $summary)
.padding(5)
.overlay(RoundedRectangle(cornerRadius: 25).stroke(Color(uiColor: .tertiarySystemFill), lineWidth: 2))
}
.padding()
.textFieldStyle(.roundedBorder)
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
if updated {
Button("Atualizar") {
book.title = title
book.author = author
book.publisher = publisher
book.summary = summary
dismiss()
}
.buttonStyle(.borderedProminent)
}
}
.onAppear() {
title = book.title
author = book.author
publisher = book.publisher
summary = book.summary
}
}

var updated: Bool {
title != book.title
|| author != book.author
|| publisher != book.publisher
|| summary != book.summary
}
}

No código acima, a atualização ocorre quando o botão com o texto Atualizar é clicado.

Operação Delete: Para deletar algum livro, temos o método contexto.delete(), que pode ser utilizado para deletar a instância do model no banco de dados utilizando a instância do model no código.

No mesmo código da operaçãoRead, na nossa struct BookListView, temos um trecho onde é utilizado o modifier .onDelete para deletar algum elemento da lista de livros. Ele possui como parâmetro da closure uma lista de índices, e para deletar um model específico encontramos o índice do livro que desejamos excluir e passamos a instância daquele model para a função de delete, presente no contexto do model.

List{
ForEach(books) { book in
NavigationLink{
EditBookView(book: book)
} label: {
HStack(spacing: 16) {
VStack(alignment: .leading) {
Text(book.title).font(.title2)
HStack(spacing: 8) {
Text(book.author).foregroundStyle(.secondary)
Text("Editora " + book.publisher).foregroundStyle(.secondary)
}
}
}
}
}
.onDelete{ indexSet in
indexSet.forEach{ index in
let book = books[index]
context.delete(book)
}
}
}

Com isso, é notória a simplicidade para construir um CRUD básico utilizando essa nova ferramenta desenvolvida pela Apple, com o intuito de abstrair ainda mais estas operações utilizando um banco de dados local, de forma que seja intuitiva e de fácil codificação para pessoas que não precisam ser especialistas na área de banco de dados.

Link para o repositório: https://github.com/ldcss/SwiftData_CRUD

--

--

Lucas Daniel
Apple Developer Academy | UFPE

Estudante de Engenharia da Computação no Cin/UFPE e Desenvolvimento iOS na Apple Developer Academy/UFPE