SwiftUI Property Wrappers Simplificado

Que tal entender de forma simplificada o significado e casos de uso dos Property Wrappers?

João Gabriel
Accenture Digital Product Dev
9 min readApr 8, 2022

--

Existem vários Property Wrappers usados para facilitar o desenvolvimento com SwiftUI. Neste tutorial vamos entender de forma simplificada o significado e o caso de uso do @State, @StateObject, @EnvironmentObject, @ObservedObject, @Binding, e @Published (esse último do Combine).

Fluxo de Estados e Informações

O fluxo de informações dentro do App com SwiftUI influencia diretamente no comportamento da interface, pois a mesma sempre se adapta ao estado que estiver.

Fluxo de estados dentro de um SwiftUI App.

Como podemos observar a View reage ao estado que possui. O estado por sua vez é alterado por uma ação. A ação pode ser gerada por um usuário que interage com a aplicação ou um evento externo que publica alterações (podendo ser algo assíncrono).

Abaixo vamos entender como podemos utilizar os property wrappers para lidar com os estados dentro de nossas aplicações:

@State & @Binding

Podemos entender de uma forma prática que ambos @State & @Binding, são dedicados a armazenar e referenciar valores respectivamente. A diferença entre esses tipos se dá na forma como referenciam a informação:

  • @State = Propriedade que contem (é dona) do valor em si. Possui um valor atribuído diretamente a ela.
  • @Binding = Referência (e assiste) uma instância atribuída. Esse tipo de variável, por ser uma referência, possui o comportamento de retornar o valor da variável original atribuída e ao mesmo tempo propagar alterações para a variável original.

Ambas variáveis declaradas como @State & @Binding, podem influenciar no comportamento da View em que estiverem declaradas.

Vamos verificar alguns exemplos:

Exemplo prático: Comunicação entre 2 Views na mesma tela

Vamos verificar na prática, utilizando 2 Views que representam um sistema contador de números inteiros. Essas 2 Views vão representar:

  • View Status do Contador: Mostra o número total atual e possui uma outra View (View Ação do Contador).
  • View Ação do Contador: Possui um botão para aumentar o número inteiro. Possui um fundo azul (apenas com objetivo didático, para deixar visível a área dessa View).

Abaixo podemos ver o exemplo funcionando na prática:

Exemplo de contador.

Como podemos ver, nessa View principal temos um texto com o total, seguido de uma outra View filha (com fundo azul) contendo o botão para incrementar o contador. Visualizando as estruturas das duas Views separadamente temos o seguinte:

Visualização das 2 Views separadamente.

O código da primeira View (CounterStatusView) que contem o status é o seguinte:

Código da CounterStatusView

Referente a CounterStatusView, a variável do tipo Int com Property Wrapper @State é declarada na linha 7. Essa variável recebe um valor da propriedade de fato (no caso o inteiro 0). Essa View possui em sua estrutura vertical (VStack) os seguintes itens:

  • Texto de título "Counter"
  • Texto informando o total "Total is counter" (sendo counter uma referencia a variável marcada como @State)
  • Uma View filha ConterActionView que recebe um parâmetro para assistir com um @Binding referenciando a variável @State. O binding é passado como parâmetro utilizando o "$" na frente da variável, simbolizando que é apenas uma referencia a variável.

O código da View filha (CounterActionView) é o seguinte:

Código da CounterActionView

Na struct CounterActionView, podemos observar que a variável counter do tipo Int é declarada na linha 7, possuindo o Property Wrapper @Binding. Dessa forma essa variável passa a ser uma referência, ou seja, um observador da variável passada a essa View em sua inicialização. A referência a variável será obrigatória no momento de inicialização da View (como na linha 27 dentro do preview onde é passado uma constante apenas para visualizar).

Exemplo prático: Comunicação entre 2 Views em telas diferentes

Vamos verificar na prática, utilizando como exemplo a comunicação entre 2 telas que representam um sistema de ativação:

  • Tela de Status: Mostra se o estado é ativado ou não e um botão para a tela de gerenciamento do status.
  • Tela de Gerenciamento do Status: Mostra uma botão para alterar o status e um outro botão para voltar.

Abaixo podemos ver o exemplo funcionando na prática:

Exemplo de um sistema de ativação.

O código da primeira tela, que mostra o status da ativação é o seguinte:

Código da tela ActivationStatusView

O resultado visual da ActivationStatusView é o segunite:

Preview da ActivationStatusView

O resultado da ActivationStatusView é uma view que varia de acordo com o status da variável booleana isActivated que possui o property wrapper @State (declarada na linha 7) da seguinte forma:

  • Quando isActivated é verdadeiro: O texto apresenta "Status: ON" e o circulo fica verde.
  • Quando isActivated é falso: O texto apresenta "Status: OFF" e o circulo fica vermelho.

Além disso ao clicar em Change, o botão altera o valor de isShowingSheet, que por sua vez apresenta a tela ActivationManagerView, que vamos ver em seguida:

Código da tela ActivationManagerView

O resultado visual da ActivationManagerView é o segunite:

Preview da ActivationManagerView

O resultado da ActivationManagerView é uma view que varia de acordo com o status da variável booleana isActivated que possui o property wrapper @Binding (declarada na linha 7) que esta observando a variável do tipo @State declarada na tela anterior (ActivationStatusView) da seguinte forma:

  • Quando isActivated é verdadeiro: O botão apresenta o texto “Update to OFF”.
  • Quando isActivated é falso: O botão apresenta o texto “Update to ON”.

Ao clicar no botão Update to ON, o botão altera o valor de isActivated, que por sua vez é apenas uma referência a variável @State declarada na tela anterior (ActivationStatusView).

@StateObject & @ObservedObject

Esses dois Property Wrappers funcionam de forma semelhante aos explicados anteriormente. A diferença é que lidam com objetos customizados. Conforme criamos abstrações para nossa aplicação, podemos fazer que esses objetos sejam "observáveis", permitindo outras Views referenciarem ele como um estado. Para tornar seu objeto "observável", é necessário que ele esteja em conformidade com o protocolo ObservableObject. Dessa forma podemos observar as propriedades marcadas como @Published do nosso objeto. Vamos entender esses dois termos então:

  • @StateObject = Associado a variável que contem (é dona) da instância do objeto que conforma ObservableObject. Possui uma instância atribuída diretamente.
  • @ObservedObject = Referência (e assiste) uma instância atribuída.

Vejamos alguns exemplos abaixo:

Exemplo prático: Comunicação entre 2 Views na mesma tela

Vamos voltar ao nosso exemplo do sistema contador. Toda interface apresentada anteriormente vai se manter a mesma. Nesse caso para exemplificar o uso do @StateObject e @ObservedObject vamos criar uma nova classe que conforme com o protocolo ObservableObject.

Classe Something, conformando com o protocolo ObservableObject e contendo uma variável @Published.

A classe Something conforma com o protocolo ObservableObject e possui uma variável @Published. O funcionamento do @Published pode ser compreendido na documentação da Apple que diz que "quando a propriedade mudar, ocorre uma publicação no block willSet da propriedade, significando que os observadores recebem um novo valor antes de ser de fato setado na propriedade".

"When the property changes, publishing occurs in the property’s willSet block, meaning subscribers receive the new value before it’s actually set on the property."

Dessa forma podemos adaptar nosso projeto do contador para obter seus valores diretamente a partir do objeto do tipo Something. No caso da View que apresenta o status podemos implementar assim:

Implementação da View de status do contador.

Podemos notar que na linha 3 a variável something possui a notação @StateObject e a instancia do objeto de fato, ou seja, essa variável é a dona da instância. Além disso, na linha 10 temos um acesso a propriedade counter que está dentro de something. Na linha 11 o objeto esta sendo passada como parâmetro da inicialização da View interna de ação que contém o botão.

Implementação da View de ação do contador, contendo o botão de incrementar.

Na classe de ação do sistema contador temos na linha 3 uma variável @ObservedObject que é tipada e sem um valor, pois esta será a variável que vai referenciar o objeto Something que receber por parâmetro em sua inicialização. Podemos observar que na linha 10 a propriedade counter é acessada de dentro da something e na linha 7 é alterado o valor da variável interna counter de Something.

É possível também de forma alternativa utilizar a implementação da linha 8 (que esta comentada) executando o método de dentro do objeto Something, o que faz com que ele mesmo se atualize após um delay de 1 segundo (simulando um intervalo de tempo de uma requisição).

Exemplo prático: Comunicação entre 2 Views em telas diferentes

Vamos voltar ao nosso exemplo do sistema de ativação. Toda interface apresentada anteriormente vai se manter a mesma. Nesse caso para exemplificar o @StateObject e @ObservedObject vamos adaptar nossas Views para referenciar corretamente o nosso objeto ActiviationModel que conforma com ObservableObject.

Modelo ActivationModel que conforma com o protocolo ObservableObject.

Podemos observar que a variável isActivated agora esta dentro da classe que conforma com ObservableObject e possui @Published, que vai funcionar para notificar todas referencias dessa instancia quando seu valor for alterado.

Adaptado nossa View de status para receber esse novo modelo temos o seguinte resultado:

ActivationStatusView adaptada pra receber o modelo que conforma com o protocolo ObservableObject.

Na linha 3 que utilizamos o Property Wrapper @StateObject pois essa variável esta lidando com um objeto que conforma com o protocolo ObservableObject. Além disso podemos ver que essa variável recebe de fato a instância do objeto.

A adaptação da View de ação para alterar o status de ativação utilizando objeto que conforma ObservableObject fica assim:

ActivationManagerView com referencia para o modelo que esta na tela anterior (tela de status)

Nesse caso a variável model na linha 3 esta apenas referenciando um objeto que esta na tela anterior. Essa variável utiliza @ObservedObject por estar referenciando um objeto que conforma com ObservableObject. Dessa forma, sempre que houver alguma alteração na variável original, a variável observadora vai refletir as mudanças e consequentemente as Views vão reagir a esse novo estado.

@EnvironmentObject

Esse tipo de PropertyWrapper também é utilizado para referenciar uma classe que conforma o protocolo ObservableObject… Mas qual a diferença?

A diferença é que ao associar um environment object a uma View, esse objeto fica disponível dentro da hierarquia de views (da View que foi adicionado) para ser referenciado em qualquer uma das Views filhas, independente do nível em que a View filha estiver. A view que desejar utilizar o conteúdo, apenas deve criar uma variável utilizando @EnvironmentObject que seja do mesmo tipo do objeto passado para a pilha de Views. Podemos visualizar nesse esquema exatamente essa situação:

Exemplo didático para entendimento do funcionamento de um @EnvironmentObject.

Nesse caso vamos entender o que acontece em cada uma das Views:

  • View cinza possui a referencia para a instancia de um ObservableObject. A View cinza passou esse objeto para sua filha (a View azul) utilizando o método environmentObject().
  • View azul por sua vez não fez nada, nem mesmo referenciou o objeto.
  • View amarela (filha da View azul) por estar na pilha de Views, recebeu também o objeto, mas no caso a View amarela referenciou o mesmo com o @EnvironmentObject e utilizou a instância.

O código das 3 Views em sequencia é o seguinte:

Código das 3 Views, utilizando @EnvironmentObject para referenciar um ObservableObject quando necessário.

Podemos observar que nesse caso a nossa View Azul (IntermidiateView) nem sequer referencia o objeto ObservableObject. Mesmo assim a View Amarela (ChildView) por estar na hierarquia de Views, consegue referenciar utilizando @EnvironmentObject na declaração da variável appModel (linha 30). Dessa forma verificamos que todas as Views que estiverem na pilha da View Azul (que recebeu esse objeto na linha 14) poderão referenciar se necessário o objeto.

Boas práticas

Observe muito bem os casos para usar de forma conveniente esses tipos de Property Wrappers. Se for referenciar em poucos níveis (2 níveis) de Views, podemos aproveitar bem o @StateObject e @ObservedObject. Mas se for necessário referenciar um objeto a 2 níveis ou mais abaixo da View atual pode ficar cada vez mais difícil lidar com @StateObject e @ObservedObject pois seria necessário ficar passando o objeto de View para View… Nesse caso seria interessante considerar a utilização de @EnvironmentObject, que torna mais pratico lidar com esses objetos em 2 níveis ou mais.

Conclusão

Podemos aproveitar muito esses métodos de comunicação entre Views, seja na mesma tela ou em telas diferentes. Mas devemos sempre ficar atentos as boas práticas de utilização para aproveitar as ferramentas corretas nos casos de usos corretos e evitar assim trabalhos desnecessários e ter um código cada vez mais limpo.

Referências

--

--