Aprendendo Cloud Kit
O API de banco de dados da Apple traz uma dinâmica simples, mas um pouco diferente para os programadores acostumados em programar em outros bancos, como o Firebase, da Google.
Por ser um pouco mais específico, é difícil encontrar materiais para aprender CloudKit, de maneira fácil e objetiva (e em português principalmente). Então decidi montar um tutorial aprofundado para explorar essa aplicação. Bora lá?
O Projeto
O nosso objetivo é desenvolver um app parecido com o twitter (só que muito mais simples), onde você pode adicionar um usuário, que pode escrever e postar tweets.
Clone o projeto base neste link:
Configurações iniciais
Antes de começar a programar o app, precisamos configurar o projeto de forma que seja possível a integração com o CloudKit.
- Em
Signing & Capabilities
, você precisa definir o seuTeam
e modificar oBundle Identifier
para um novo e único identifier.
Depois disso, você deve adicionar a capability iCloud
, na aba superior, para aparecer a nova região abaixo. É necessário que você marque as caixas de Key-value storage
e CloudKit
. Assim, ao clicar aonde a seta aponta, você já pode adicionar um container especial para esse aplicativo. Lembre-se de clicar no botão de recarregar após criá-lo e de selecionar a caixa desse container.
Pronto, já terminamos de fazer as configurações iniciais no Xcode. Para configurarmos o banco, aperta no botão CloudKit Dashboard
para redirecionar ao site do CloudKit.
CloudKit Dashboard
Para ser capaz de salvar informações no iCloud, algumas coisas têm de serem configuradas primeiro. Mas, antes disso, é bom a gente se familiarizar com a interface do dashboard.
Na face da esquerda, nós temos, dispostos em uma lista, todos os Containers
que ativamos em algum projeto no Xcode. Já na área de Development
temos acesso aos dados que são salvos no banco, às taxas de uso e de usuários ativos, entre outros artifícios.
Nesse tutorial, vamos focar na aba de Schema
, onde realizaremos a criação da estrutura das entidades e relacionamentos do banco, que, no CloudKit, são chamadas de Records.
No começo, para mim, foi um pouco complicado entender o conceito de entidades e relacionamentos, mas esse entendimento é essencial para a construção de um bom banco de dados. Para ajudar vocês, se precisar desse contexto básico, explico um pouco nesse texto do Medium:
Agora que já temos noção de como funciona, como seria a estrutura do nosso banco de twitter?
Pensei no seguinte minimundo:
Existem N usuários
Um usuário possui nome
Um usuário pode escrever X tweets
Um tweet pode e deve ser escrito por apenas um usuário
Um tweet possui texto e data de criação
Assim, ficamos com essa modelagem:
Mas como colocamos isso no Dashboard?
Dentro da área Schema
aparece uma tela parecida com a de baixo. Para adicionar um novo Record, você deve clicar no botão New Type
e digitar o nome da entidade, que no nosso caso é "TwitterUser".
Na área de Custom Fields
nós podemos adicionar nossos atributos, que, para TwitterUser
, seria apenas "name". Lembre-se de determinar o tipo do atributo sempre! E não se esqueça de salvar as modificações, no canto inferior direito.
Agora que já criamos o primeiro Record, montar o segundo não é nenhum mistério:
Na verdade, tenho certeza que você está curioso com duas coisas:
- Cadê o atributo "createdAt" ???
Essa resposta é facil… Não precisa! Um dos atributos de sistema, que existe em toda Record é o createdAt
, para facilitar a nossa vida. Além dele, existem vários outros, como modifiedAt, recordName, createdBy, etc…
2. Por que tem um atributo chamado "tweeter"?
É um pouco mais complicado, mas vamos lá! Ao criar a estrutura de um relacionamento, temos que ter bastante cuidado. Eu poderia ter criado um Record específico chamado Write onde possuiria dois atributos: A referência para o Tweet e a referência para o escritor do Tweet. E isso não está errado! Seria a solução ideal se mais de um usuário pudessem ser donos do mesmo tweet, mas não é o caso, então usar essa tabela é repetitivo.
A solução mais prática é a gente colocar a referência ao usuário que escreveu o tweet como um novo atributo em Tweet! Entendeu?
Agora que já esclarecemos tudo, falta configurarmos uma coisinha nos Records: os Indexes. Logo embaixo de Custom Field
, que você acabou de preencher, tem um botão escrito Edit Indexes
, em azul. Clique nele que você será levado para a seguinte tela:
Aqui se configura os índices dos Records. Os índices servem para quando formos pesquisar e recuperar algum dado no banco de dados para o aplicativo.
No CloudKit, existem três tipos de index:
Queriable
— Ele é usado se você deseja consultar um Record por meio de um atributo específico;Sortable
— É usado se você precisa ordenar os resultados de uma busca em base de algum atributo;Searchable
— Quando se faz uma pesquisa usando CONTAINS, quero saber se tal atributo(de tipo string) da entidade contém o termo de pesquisa, para retornar uma lista com todos os objetos que possuem.
Então vamos configurar nossos indexes:
Para Tweet, queremos que recordName seja Queriable
, createdAt seja Queriable e Sortable
, text seja Queriable, Sortable e Searchable
e tweeter seja Queriable
.
Para TweetUser, queremos que recordName seja Queriable
, createdAt seja Queriable e Sortable
e name seja Queriable, Sortable e Searchable
.
Não se esqueça de salvar.
Acabamos a parte de configuração, hora de partir para o código, finalmente!
O Código
Lembrando que, como o foco é CloudKit, não vou aprofundar no desenvolvimento de UIKit do projeto, já que se contempla basicamente de UIAlertController
e TableViewController
:) Vamos lá!
Observações
- Não esqueça de colocar
import CloudKit
no começo do arquivoCloudViewController.swift
, para usarmos as funções da biblioteca. - Para termos acesso ao container que montamos no CloudKit Dashboard, precisamos adicionar
let privateDatabase = CKContainer(identifier: NomeDoContainerQueVoceCriou).privateCloudDatabase
nas variáveis do controller.
Realizando essas modificações, o começo do arquivo deve ficar desse jeito:
Agora, a primeira coisa que deveríamos aprender seria como escrever no banco de dados. Então vamos começar com a ação acionada quando o botão createNewUserButton
é pressionado.
Cadastrando o usuário
Primeiro, precisamos recolher o nome do novo usuário para cadastrá-lo no banco e, para isso, criei uma função getNameAlert(title: String, message: String, action: @escaping (String)->Void)
, que cria um alerta com um espaço de text field para escrever o nome e que, quando o botão OK
for pressionado, realiza-se a ação action
.
Obs.: Fiz a criação dessa função desse jeito para, no futuro, podermos reutilizar ela ao pesquisar o nome do usuário para realizar outra função.
Desse jeito, a estrutura inicial da função buttonAction
é da seguinte forma:
Para finalizar, só falta desenvolvermos a função que recebe o nome do usuário e cadastra ele na nuvem. Vamos chamar ela de createNewUser
, como você já deve ter notado no código acima.
createNewUser
- Linhas 3–5. Primeiro, antes de cadastrar o usuário no banco temos que considerar o seguinte: não quero que seja possível que dois usuários possuam o mesmo nome de usuário. Assim, a gente precisa fazer uma Query de
TwitterUser
, uma Pesquisa, no banco, antes de escrever o novo usuário. Especificamos essa pesquisa, usando oNSPredicate
, para que todos os resultados da nossa pesquisa possuam o atributo "name" de mesmo valor que a stringTweeterName
. - Linha 8. Para realizar a operação da Query e recuperar seus resultados, precisamos associar ela a uma
CKQueryOperation
. - Linha 10. Essa variável serve para me dizer se existe algum usuário já cadastrado com esse nome no banco.
- Linha 11. Esse bloco é onde a gente recupera os dados da pesquisa. Ele é executado para todo resultado que aparece. (se existir algum resultado, convertemos
existed
paratrue
. - Linha 18. Quando toda a query já é lida, esse bloco é executado.
- Linha 19. Importante: Como esses blocos não são síncronos, precisamos abrir um bloco
DispatchQueue.main.async
para poder realizar alguma operação de UI sem nenhum erro. - Linhas 21–29. Se já existir um usuário no banco, crio uma alerta e chamo a função
getNameAlert
de novo. - Linha 31. É o record de tipo "TwitterUser". Essa variável possui a estrutura e os atributos que definimos no
CloudKit Dashbord
. - Linha 32. Nessa função você ESCREVE o valor do atributo dentro do record.
- Linha 34. Para salvar na nuvem esse
Record
que criamos localmente, precisamos escrever essa função deSave
. - Linha 37–47. Aqui é o código que escrevi para lidar com possíveis erros ao salvar (e apresentar ao usuário).
- Linha 59. Aqui a gente adiciona a operação de
Query
para começar a ser realizada no nosso banco.
Nesse momento já conseguimos salvar quantos usuários quisermos no banco! Agora bora partir para escrever os tweets?
Escrevendo o Tweet
Primeiro, vamos entender um pouco o que pensei nessa função: A gente escreve o nome do usuário e, se ele existir no database
, a gente escreve o tweet!
A gente vai começar poupando trabalho. Lembra que comentei que daria para reutilizar a função getNameAlert
? É agora. Vamos criar apenas a função pós escrita do nome! A nossa função buttonAction
é pra ficar desse jeito agora:
Agora, a gente vai desenvolver a função WriteTweet
, que vai, primeiramente, checar se existe um usuário com o nome que escrevemos e, se existir, partimos pra escrever o tweet.
writeTweet
- Linhas 2–17. É repetido o que acontece com a função
createNewUser
, só que, se existir um RecordTwitterUser
no banco com o atributo "name" igual aTwitterName
, então a gente armazena tal Record nessa nova variáveluser
. - Linhas 23–31. Se não existir um usuário na
query
, crio uma alerta e chamo a funçãogetNameAlert
de novo. - Linhas 34–40. Se existir, apresentamos um novo
UIAlertController
, que contém umUITextView
para a escrita do texto do tweet. - Linhas 42–47. Ao pressionar o botão
OK
é criado um novo Record do tipoTweet
e é escrito no atributo "text" o conteúdo do text view. - Linhas 49–50. Lembra que, no
CloudKit Dashboard
determinamos que o RecordTweet
possui também uma variável chamada "tweeter", que é uma referência ao usuário que escreveu o tweet? Então, para escrevermos essa referência, precisamos criar umCKRecord.Reference
, que contém o valor do Record que queremos fazer referência(user)
. Outra coisa importante:action
, nesse contexto, significa 'O que devo fazer com esseTweet
, caso esseTwitterUser
for apagado do banco?'..none
significa que nada é para acontecer, enquanto.deleteSelf
significa que quero queTweet
seja apagado do banco também, um caso de Delete Cascade. - Linha 51. Para salvar na nuvem esse
Record
que criamos localmente, precisamos escrever essa função deSave
.
O resto desta função é só a implementação de algumas features de UI, como o UITextView
.
Já fizemos muita coisa, não foi? Pelo menos só faltamais um pouco. A gente já consegue cadastrar os usuários e escrever os tweets perfeitamente. Só falta a gente conseguir visualizar isso tudo, né?
Leitura dos dados
Como que vai funcionar essa visualização? Queremos mostrar os tweets dispostos em uma Table View
, que seriam organizados pela data de criação mais recente.
Então, o que precisamos fazer primeiro é adicionar duas variáveis para o View Controller
que conterão os dados dos tweets e usuários.
- A Lista
tweets
armazenará os Records de tweets que serão dispostos naTable View
. - O Dicionário
usersByID
armazenará os usuários atráves da sua identificação únicarecordName
.
Vamos criar uma nova função updateConsole()
e modificaremos mais uma vez a função buttonAction
para o seguinte:
updateConsole
A função vai ter a seguinte estrutura:
- Linhas 3–4. Toda vez que o botão for pressionado, precisamos limpar a lista e dicionário, para poder recolher tudo de novo.
- Linha 8. A pesquisa que vamos fazer vai ser uma
Query
, sem nenhuma limitação (por isso que o valor do predicate étrue
). - Linha 10. Um
Sort Descriptor
tem a função de descrever de que maneira você quer a organização daQuery
. No nosso caso, queremos que ela seja organizada apenas pelo atributocreationDate
, de mais recente para mais antigo. - Linha 14. Esse bloco será executado para cada
Record
daQuery
. - Linhas 18–19. A gente já tem acesso ao valor do
Record
de tipo Tweet, então já podemos adicioná-lo naTable View
e atualizá-la. - Linha 21. Como sabemos que record é do tipo
Tweet
, sabemos que ele possui a variável de referênciatweeter
. Mas, lembrando: possuímos a referência apenas. Não temos acesso ainda aos atributos deTwitterUser
. - Linha 23. Aqui é apenas para otimização: Se já fizemos a leitura do record de um usuário, para quê fazermos de novo?
- Linha 25. Aqui é onde a gente vai fazer a leitura dos dados do usuário, a partir da referência. Precisamos fazer uma
Fetch
no banco de dados, para recolher a referência. - Linhas 29–30. Agora só falta escrever o usuário no dicionário e atualizar a
Table View
.
Alterações na Table View
Só falta a gente associar a leitura da lista e do dicionário à Table View
. As modificações na Extension serão assim:
- Linha 4. Precisamos que essa função retorne a quantidade de itens da
Table View
, que seria equivalente atweets.count
. - Linhas 18–20. Aqui a gente vai definir um formato e colocar a data de criação do tweet na label
dateLabel
da célula. - Linha 22. Escrevo na label
tweetText
o valor do texto do tweet. - Linhas 23–28. Simplificando, se os dados do
TwitterUser
já estiverem disponíveis, atualizamos a labeluserName
da célula
Com essas mudanças, a gente já consegue visualizar perfeitamente todos os tweets no Console!
Apagando o banco
Só falta desenvolver a função de apagar o banco da nuvem. Antes de apagar, solicitaremos a confirmação da ação, com um AlertController
. A função buttonAction
vai ficar desse jeito (finalmente haha):
Note que, se o botão de apagar for pressionado, a função deleteRecord
vai ser executada para apagar todos os dados tanto de Tweet
quanto de TwitterUser
.
deleteRecord
- Linha 2. Essa lista serve para armazenar os ids que vamos deletar.
- Linha 10. Eu adiciono o id de cada item da
Query
nessa lista. - Linha 14–17. Quando terminar de armazenar os ids nessa lista, fazemos a exclusão de cada Record usando a função
self.privateDatabase.delete(withRecordID: CKRecord.ID)
.
Tenha cuidado ao deletar, porque esses dados vão ser apagados definitivamente!
Notas gerais
O que se pode fazer com CloudKit (e banco de dados, em geral) é realmente enorme mas muito massa. Tentei explorar alguns conceitos que tive bastante dificuldade para encontrar e aprender, para ajudar principalmente quem tem dificuldade de ler conteúdos em inglês :)
É bastante coisa para fazer, né? Não fica intimidado com a quantidade de informação que tem, tá ok? Minha sugestão é ir digerindo em partes. Salva esse texto aqui no Medium e não tenha pressa para acompanhá-lo.
Outra coisa que é importante para o CloudKit funcionar no dispositivo é que o usuário esteja conectado ao iCloud no seu dispositivo. Sem isso, o aplicativo não funciona. Quando estiver trabalhando com um app de verdade, tenta fazer a checagem se o usuário está conectado ao iCloud ao abrir o app.
E é isso, gente!
Muito obrigado por ter lido e que ótimo que você deu uma chance ao CloudKit :)
Se precisar de alguma ajuda ou quiser comentar alguma coisa, não tenha vergonha, pode me mandar uma mensagem no instagram ou comentar nesse post!