Quem desenvolve com SwiftUI digita @State e @Published dezenas de vezes por dia. Mas, quando perguntamos exatamente o que essa arroba faz, as respostas variam. «Não é sintaxe do SwiftUI?» é um erro comum. Não. Um wrapper de propriedade (property wrapper) é um recurso da linguagem incluído no Swift 5.1 pela proposta SE-0258 do Swift Evolution; o SwiftUI é apenas um usuário famoso desse recurso.
Este é o episódio 7 da série intermediária. Vamos entender o funcionamento dos wrappers de propriedades implementando um deles e resumir o que são wrappedValue e projectedValue($), além de como evitar seu uso excessivo.
Motivação — lógica de encapsulamento repetida em cada propriedade
No episódio sobre propriedades, vimos o padrão de validar valores com didSet. Mas e se várias propriedades precisarem da mesma validação? Se você quiser limitar volume, brilho e progresso ao intervalo 0…1, acabará copiando três blocos didSet.
var volume: Double = 0.5 {
didSet { volume = min(max(volume, 0), 1) }
}
var brightness: Double = 0.5 {
didSet { brightness = min(max(brightness, 0), 1) }
}
// O mesmo código continua...
É a mesma lógica reescrita para cada propriedade: a versão aplicada a propriedades do problema «duplicação ou segurança?» visto no episódio sobre genéricos. Um wrapper de propriedade dá nome a essa lógica de encapsulamento para que ela possa ser reutilizada.
@propertyWrapper
struct Clamped {
private var value: Double = 0
var wrappedValue: Double {
get { value }
set { value = min(max(newValue, 0), 1) }
}
}
struct Player {
@Clamped var volume: Double
@Clamped var brightness: Double
}
Mesmo atribuindo player.volume = 1.5, na prática 1.0 será armazenado. A lógica de validação fica apenas em Clamped.
Como funciona — a tradução feita pela arroba
Para confirmar que @Clamped não é magia, basta observar o resultado da tradução do compilador. @Clamped var volume: Double é expandido aproximadamente assim.
private var _volume = Clamped() // Armazenamento real: instância do wrapper
var volume: Double { // O nome que usamos: propriedade computada
get { _volume.wrappedValue }
set { _volume.wrappedValue = newValue }
}
O essencial está em duas linhas. O que realmente é armazenado é a instância do wrapper com sublinhado, e o nome que acessamos é uma propriedade computada que aponta para o wrappedValue do wrapper. Dá para ver como a distinção entre armazenamento e computação, trabalhada no episódio sobre propriedades, é usada diretamente como matéria-prima. No fim, o wrapper empacota «propriedade armazenada + propriedade computada + lógica repetida» em um único tipo e a disponibiliza por meio da arroba.
Ao entender essa tradução, as restrições dos wrappers também ficam naturais. O comportamento confuso ao adicionar didSet a uma propriedade com wrapper, assim como a impossibilidade de aplicá-lo a variáveis locais ou propriedades computadas (variáveis locais são permitidas desde o Swift 5.5), tudo se resume a «onde o armazenamento com sublinhado é criado».
projectedValue — o que representa o cifrão
No SwiftUI, você provavelmente já viu um cifrão, como em $text. Isso também é um recurso de wrapper. Quando você declara uma propriedade chamada projectedValue no tipo wrapper, o compilador cria um terceiro caminho chamado $이름.
volume→ wrappedValue (o valor em si)_volume→ a instância do wrapper (acessível apenas dentro do tipo que o declarou)$volume→ projectedValue (algo que o wrapper expõe adicionalmente)
O que é esse “algo que o wrapper expõe adicionalmente” fica a critério de quem o projeta. O @State do SwiftUI expõe um Binding —um canal de leitura e escrita— por meio de $, enquanto o @Published do Combine expõe um Publisher, um fluxo de mudanças. A mesma sintaxe $ produz objetos diferentes porque isso é uma decisão de design de cada wrapper, não uma regra da linguagem. O mesmo vale para wrappers criados por você. Se você adicionar ao Clamped um projectedValue que informe se o valor foi limitado, $volume se torna uma API para verificar se a atribuição anterior ficou fora do intervalo.
Receita prática — Aprendendo design com um wrapper de UserDefaults
Um dos padrões de wrapper personalizado mais usados em produção é o acesso ao UserDefaults. Vamos criar um enquanto confirmamos o princípio.
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
enum Settings {
@UserDefault(key: "hasSeenOnboarding", defaultValue: false)
static var hasSeenOnboarding: Bool
}
A lógica repetitiva de erros de digitação nas chaves, conversão de tipos e tratamento de valores padrão fica dentro do wrapper, e o ponto de uso vira uma única linha: Settings.hasSeenOnboarding = true. Outro ponto deste exemplo é que o genérico <T> e os parâmetros de init (key, defaultValue) também se aplicam ao wrapper sem alterações. Os parênteses depois do @ chamam o init do wrapper.
Esses são os cenários naturais para um wrapper: mudar o armazenamento (UserDefaults, chaveiro), envolver o acesso (locks de thread, logging) e ajustar valores (limitar intervalos, fazer trim). O ponto em comum é serem preocupações técnicas de armazenamento e acesso, sem relação com o significado do valor. Pela perspectiva da separação de responsabilidades, o wrapper é uma ferramenta para separar preocupações técnicas da declaração da propriedade.
Limites do abuso — A complexidade escondida atrás do @
O risco de um wrapper vem diretamente de seu ponto forte: código arbitrário pode se esconder atrás de uma única atribuição. É um recurso em tensão direta com o princípio da menor surpresa.
Proponho três critérios. Primeiro, dentro de um wrapper, faça apenas o que for previsível. Ajustar valores e mudar o armazenamento é válido, mas esconder efeitos colaterais pesados, como requisições de rede ou transições de tela, atrás de uma atribuição transforma a depuração em um inferno. Segundo, use apenas wrappers conhecidos pela equipe. Padrões do ecossistema, como @State, e wrappers documentados na base de código são ativos; mas, quando surge um @ novo em cada arquivo, a revisão de código vira um jogo de procurar definições de wrappers. Terceiro, deixe a lógica usada uma única vez em didSet. Wrappers existem para reutilização, então o correto é promovê-la quando surgir um segundo uso. É o princípio YAGNI (You Aren’t Gonna Need It — não crie até precisar).
Por fim, uma direção recente. À medida que Swift e SwiftUI avançam para uma abordagem baseada em macros, surgiram @ que são macros, e não wrappers, como @Observable do framework Observation. Um @ já não significa necessariamente um property wrapper. O que são macros e como diferem de wrappers será tratado em uma série avançada.
Resumo
- Property wrappers não são exclusivos do SwiftUI; são um recurso da linguagem do Swift 5.1 (SE-0258) que reúne em um tipo reutilizável a lógica de encapsulamento repetida de cada propriedade.
- O princípio é uma tradução. @Wrapper var x se expande para “uma instância do wrapper com sublinhado (armazenamento) + uma propriedade computada chamada x (canal de acesso)”.
- $x é um terceiro canal de acesso chamado projectedValue, e o que ele fornece é definido pelo projetista do wrapper (@State fornece Binding, enquanto @Published fornece Publisher).
- Eles são adequados para responsabilidades técnicas como alterar o local de armazenamento, encapsular o acesso e ajustar valores; efeitos colaterais pesados e lógica de uso único não devem ser colocados em wrappers.
O próximo capítulo é KeyPath, o último da série intermediária. Ele explica como a sintaxe de barra invertida \.name trata propriedades como valores e por que map(.name) se tornou possível.

![Imagem de capa de [Swift #7] Wrappers: por que @State não é magia](/assets/images/posts/33440ae0-f781-4765-a6c7-694f65980263/swift-property-wrapper-1.jpg)