
Imagine que você precisa salvar o ID do usuário em UserDefaults . Qual será o primeiro passo?
Normalmente, um negócio começa adicionando uma constante para uma chave e verificando sua singularidade. Isso é verdade para a maioria dos outros armazenamentos de valores-chave. E as conseqüências do design primitivo de tais repositórios não se limitam às chaves; uma interface na forma de um conjunto de métodos não sistemático leva a vários problemas possíveis:
- Erros ao escrever chaves : chaves diferentes podem ser usadas para ler e escrever a mesma entidade.
- Tipo de valor não corrigido : por exemplo, pela mesma chave, você pode escrever um número e ler uma string.
- Colisão de chaves : diferentes entidades com a mesma chave podem ser registradas em diferentes partes do projeto.
Em um mundo ideal, o compilador deve se proteger contra esses problemas, não permitindo que você construa um projeto se houver um conflito de chave ou se o tipo de valor não corresponder. Para implementar esse armazenamento seguro, você pode usar contêineres para valores, e este passo a passo o ajudará a prepará-los usando UserDefaults
como exemplo.
Protocolo de armazenamento
Portanto, a primeira coisa que precisamos é de um protocolo para o próprio repositório, o que ajudará a abstrair de seu tipo. Portanto, teremos uma interface única para trabalhar com UserDefaults
e com um chaveiro e com algum outro armazenamento de valor-chave. Este protocolo parece bem simples:
protocol KeyValueStorage { func value<T: Codable>(forKey key: String) -> T? func setValue<T: Codable>(_ value: T?, forKey key: String) }
Portanto, qualquer armazenamento que esteja em conformidade com o protocolo KeyValueStorage
deve implementar dois métodos genéricos : getter e setter de valores-chave na forma de uma string. Ao mesmo tempo, os próprios valores correspondem ao protocolo Codable
, que permite armazenar instâncias de tipos que possuem uma representação universal (por exemplo, JSON ou PropertyList ).
As implementações padrão do armazém de dados não suportam o tipo Codable
para valores, por exemplo, os mesmos UserDefaults
. Portanto, essa distinção de tipo é um bônus colateral que permite armazenar primitivas do Swift (números, seqüências de caracteres etc.) e estruturas de dados inteiras, mantendo a interface de armazenamento simples.
Implementação de protocolo
Existem duas maneiras de implementar o protocolo KeyValueStorage
:
- Assine o armazenamento existente sob o protocolo e adicione os métodos necessários:
extension UserDefaults: KeyValueStorage { func value<T: Codable>(forKey key: String) -> T? {
- Embrulhe o armazenamento em um tipo separado, ocultando seus campos para uso externo:
class PersistentStorage: KeyValueStorage { private let userDefaults: UserDefaults let suiteName: String? let keyPrefix: String init?(suiteName: String? = nil, keyPrefix: String = "") { guard let userDefaults = UserDefaults(suiteName: suiteName) else { return nil } self.userDefaults = userDefaults self.suiteName = suiteName self.keyPrefix = keyPrefix } func value<T: Codable>(forKey key: String) -> T? {
O segundo método parece mais complicado, mas dimensiona melhor, por exemplo, você pode definir o prefixo da chave para cada instância de armazenamento. Além disso, o wrapper oculta campos extras e deixa apenas os campos de protocolo disponíveis, resolvendo possíveis problemas de conflitos de assinatura.
Para implementar os métodos value(forKey:)
e setValue(:forKey:)
é importante fornecer compatibilidade de dados. Isso é necessário para que os valores armazenados pelas ferramentas padrão UserDefaults
possam ser recuperados pelos métodos do KeyValueStorage
e vice-versa.
Um exemplo completo da classe PersistentStorage
pronto para uso está disponível aqui .
Contentor de valor
Agora que abstraímos do tipo de armazenamento, adicionaremos um contêiner para o valor por chave. Será útil para encapsular todos os campos necessários em uma entidade conveniente, que pode ser transferida e usada separadamente do próprio armazenamento. Esse contêiner é implementado como uma pequena classe genérica :
class KeyValueContainer<T: Codable> { let storage: KeyValueStorage let key: String var value: T? { get { storage.value(forKey: key) } set { storage.setValue(newValue, forKey: key) } } init(storage: KeyValueStorage, key: String) { self.storage = storage self.key = key } }
O tipo de valor para o contêiner é limitado pelo protocolo Codable
da mesma maneira que nos métodos do próprio armazenamento, portanto, a propriedade value
calculada simplesmente procura por proxies chamadas com uma chave fixa e um tipo de valor.
Se desejar, você pode expandir a funcionalidade do contêiner, por exemplo, adicione um valor padrão que será usado se não houver dados para a chave na loja.
Exemplo de implementação de um contêiner com um valor padrão class KeyValueContainer<T: Codable> { let storage: KeyValueStorage let key: String let defaultValue: T? var value: T? { get { storage.value(forKey: key) ?? defaultValue } set { storage.setValue(newValue, forKey: key) } } init(storage: KeyValueStorage, key: String, defaultValue: T? = nil) { self.storage = storage self.key = key self.defaultValue = defaultValue } }
O contêiner resolve nossos dois primeiros problemas - ele corrige a chave e o tipo dos dados que estão sendo lidos e gravados. Portanto, qualquer tentativa de escrever um valor de um tipo incorreto será interrompida pelo compilador no estágio de construção:
func doSomething(with container: KeyValueContainer<Int>) { container.value = "Text"
Resta resolver o último problema - garantir a exclusividade das chaves de armazenamento. Essa preocupação também pode ser "pendurada" no compilador usando um truque bastante interessante.
Exclusividade chave
Para resolver o problema do conflito, usamos o nome da propriedade computada na qual o próprio contêiner será criado como chave. Para fazer isso, adicione uma extensão simples ao nosso protocolo KeyValueStorage
:
extension KeyValueStorage { func makeContainer<T: Codable>(key: String = #function) -> KeyValueContainer<T> { KeyValueContainer(storage: self, key: key) } }
Portanto, em todas as implementações do protocolo de armazenamento, é adicionado um método genérico que retorna um contêiner com a chave especificada. De particular interesse nesse método é o parâmetro key
, que por padrão tem um valor igual à expressão especial #function
( documentation ). Isso significa que, no estágio de construção, o literal #function
substituído pelo nome da declaração a partir da qual o makeContainer(key:)
foi chamado.
Essa construção permite declarar contêineres em extensões de armazenamento, e seus nomes serão os nomes das propriedades calculadas se o método makeContainer()
for chamado sem o parâmetro- key
:
extension PersistentStorage { var foobar: KeyValueContainer<Int> { makeContainer() } }
No exemplo, as instâncias de armazenamento PersistentStorage
receberão a propriedade foobar
do contêiner com a chave foobar
com o mesmo nome, cujo tipo de valor será um número inteiro. Tentar adicionar um segundo contêiner foobar
para armazenamento resultará em um erro de compilação, o que nos garante a exclusividade das chaves.

Resumir
Os contêineres de valor resolvem todos os problemas da interface de armazenamento mencionados e não se limitam aos UserDefaults
. É suficiente assinar (agrupar) qualquer armazenamento de valor-chave no protocolo KeyValueStorage
com a implementação correspondente e já pode ser usado para criar contêineres seguros.
Separadamente, vale a pena notar a conveniência de adicionar e usar esses recipientes; tudo se limita a projetos simples e compreensíveis:
extension PersistentStorage {
Você só precisa se acostumar com o fato de que o nome da propriedade calculada é a chave do valor, o que é especialmente importante ao refatorar o código. Além disso, não se esqueça da migração do repositório, se você ainda não conseguiu renomear.
Só isso. Ficarei feliz em comentar nos comentários. Tchau!