Você sabe o que são Property Wrappers? (Parte 2)

Marcos Ferreira
5 min readJul 15, 2022

--

Please, tell me more!

Sejam bem vindos de volta, recentemente publiquei a primeira parte desta série Você sabe o que são Property Wrappers? (Parte 1) e agora vamos dar sequência adentrando um pouco mais nessa mar de possibilidades que temos a nossa frente quando falamos sobre o poder das anotações @.

Algumas limitações que encontramos ao utilizarmos Property Wrappers

Achei interessante trazer este "complemento" do primeiro artigo, trazendo algumas das "desvantagens" ou "limitações" que encontramos quando usamos as Property Wrappers, vale ressaltar que, apesar dos pontos "negativos", isso não significa que não podemos ou não devemos usa-las. Então sem mais delongas, vamos aos pontos:

  • Podem ser aplicadas somente a variáveis var

Sim, infelizmente não é possível aplicarmos uma notação construída em uma propriedade let e a explicação é bem simples, uma Property Wrapper, nada mais é, do que uma variável computada computed property e dessa forma, tem um valor dinâmico, tornando dessa forma impossível que seja utilizada com uma propriedade constante.

  • Só podemos utilizar uma property wrapper por propriedade

Outro ponto "ruim" na minha opinião, contrário de outras linguagens, como Java por exemplo, onde é permitido utilizarmos mais de uma annotation em uma propriedade, as Property Wrappers são limitadas a apenas uma por propriedade. Sendo assim, não seria possível usar uma Property Wrapper para realizar uma operação de captalized e outra para salvar esse valor no UserDefaults por exemplo.

Mas calma, nem tudo esta perdido, existe uma proposta de evolução da linguagem que permite realizar esse aninhamento de anotações, empregando um recurso bastante conhecido, chamado composição. De forma bastante resumida, ele argumenta que se utilizarmos composição é possível aninhar várias anotações em uma mesma propriedade, basicamente, a estratégia consiste em criar um protocolo que encapsula um valor abstrato para o segundo tipo, e pro terceiro, pro quarto e por ai vaí.

  • Uma propriedade anotada com um Property Wrapper não pode ser sobrescrita em uma classe filha

Isso significa, que se houver uma propriedade declarada em A marcada com uma anotação qualquer, ela não poderá sofrer um override em B, mesmo B sendo um filho direto de A.

  • Uma propriedade marcada com uma Property Wrapper não pode estar anotada com lazy, @NSCopying, @NSManaged, weak ou unowned
  • Uma propriedade marcada com uma Property Wrapper não pode conter métodos set e get customizados.
  • Uma propriedade marcada com uma Property Wrapper não pode ser declarada em um protocolo ou uma extensão
  • wrappedValue, init(wrappedValue:) e projectedValue devem obrigatoriamente conter o mesmo nível de acesso para que o gerenciamento da sua propriedade funcione corretamente.

Agora que já demos as "más" notícias, vamos ao assunto de hoje.

Photo by YingYi Dai on Unsplash

Para iniciarmos nossos estudos vamos introduzir outro "problema" onde podemos ter uma Property Wrapper para resolver. Pensando no cenário onde desejemos salvar um dado qualquer na API disponível no framework Foundation, o UserDefaults, para fazer isso sem uma Property Wrapper seria necessário um código semelhante ao a seguir:

Agora imagine um projeto mais robusto que realiza uma série de consultas/escritas no UserDefaults para dados mais "leves".

ATENÇÃO: Nunca salve dados sensíveis no UserDefaults além de ser uma má prática de desenvolvimento, torna seu app mais vulnerável à ataques.

Considerando o que foi dito, imagine quantas e quantas vezes esse código seria utilizado, e se você não tivesse um "centralizador" (por exemplo, um Singleton), você teria que replicar esse código várias e várias vezes, tornando assim o código mais fragmentado e consequentemente dificultando ainda mais a manutenção do mesmo.

Agora imagine, se você pudesse construir uma Property Wrapper que iria centralizar e generalizar essa implementação? Um sonho não?!

Então vamos ver como isso pode se tornar realidade. O primeiro passo é criar uma Property Wrapper capaz de gerenciar de forma genérica todas as interações com o UserDefaults. Veja o código a seguir:

Vamos analisar algumas diferenças peculiares em relação a Property Wrapper que construímos na parte 1 desta série.

  • Presença de um Value após o nome da Property Wrapper

Nesta declaração estamos pedindo ao utilizador da nossa anotação que nos informe qual é o tipo de dado que será manipulado aqui, desta forma tornamos nossa implementação mais abstrata e com isso não é necessário que construamos uma anotação para String, um para Int e por ai vai.

  • Outra diferença peculiar é a presença de uma constante chamada key

Basicamente, é graças a ela que poderemos informar qual será a chave usada para armazenar/recuperar nossa informação do UserDefaults

  • Também declaramos uma variável do tipo UserDefaults

Essa variável será a responsável por prover acesso a instância standard do UserDefaults e por estarmos realizando operações de escrita com ela nos é "forçado" declara-la como uma var e não podendo ser um let

  • Por fim, realizamos as operações de escrita e leitura no get e set customizados

Graças a essa implementação é possível que possamos recuperar e/ou escrever um valor no UserDefaults usando apenas a atribução = de valores as variáveis anotadas com nossa Property Wrapper.

Assim como qualquer outra struct, nosso tipo UserDefaultsManager, que declaramos acima, receberá automaticamente um inicializador memberwise com argumentos para todas as propriedades que não têm um valor padrão definid — o que significa que poderemos inicializar instâncias dele simplesmente especificando por qual chave do UserDefaults queremos gerenciar nossa propriedade anotada. Vamos ver um exemplo de utilização:

Repare que não colocamos de forma explícita o tipo das propriedades que declaramos, isso não se fez necessário pois o compilador vai inferir o tipo da variável sendo o mesmo tipo atribuído a nossa anotação @UserDefaultsManager, dessa forma, o compilador saberá que a propriedade userHasLogged é um boolean e que userAge é um Int, ambos opcionais, pois nosso Value declarado é opcional.

Repare também que através do atributo key informamos qual é a chave que será usada para recuperar/gravar o valor no UserDefaults.

Dito isso, para escrever um dado na chave user-age bastaria fazer:

Agora que você já viu como é poderoso e como pode ser fácil encapsular operações repetitivas em um único ponto, você pode avaliar a implementação das Property Wrappers em seus projetos.

Vou deixar este excelente artigo de um amigo John Sundell sobre o tema ele explora algumas outras aplicações de Property Wrappers tornando ainda mais robusta sua utilização.

Espero que tenham gostado deste artigo, comentários e sugestões são sempre bem vindos! No próximo e último artigo dessa série irei abordar uma utilização um pouco mais complexa, Injeção de Dependências usando Property Wrappers. A idéia será construir um mecanismo que resolva dependências do seu projeto semelhante ao que o Swinject faz de forma abstrata.

Te espero lá, valeu!!!

--

--