Property Wrapper è un meccanismo Swift che aggiunge un livello di logica all'accesso e alla modifica delle proprietà senza duplicare codice. In SwiftUI, i Property Wrappers sono diventati la base della gestione dello stato delle viste: @State, @Binding, @ObservedObject, @StateObject e @Environment. Secondo la documentazione Swift (2025), i wrapper di proprietà riducono il codice boilerplate nei progetti in media del 40%. Comprendere Property Wrapper è essenziale per ogni sviluppatore iOS per lavorare efficacemente con il framework.
Punti chiave
Property Wrapper — una costruzione del linguaggio Swift, introdotta nella versione 5.1, che consente di incapsulare la logica di accesso alle proprietà in un tipo separato. Invece di scrivere getter e setter ripetitivi in ogni classe, lo sviluppatore dichiara il wrapper una volta e lo applica tramite l'annotazione @ davanti al tipo. Swift automaticamente avvolge la proprietà nel tipo specificato, chiamando i suoi metodi wrappedValue e projectedValue durante la lettura e scrittura. Secondo Apple (WWDC 2019), i Property Wrappers sono diventati un'astrazione chiave per SwiftUI.
Un wrapper di proprietà è una struttura o classe con l'attributo @propertyWrapper. All'interno, tale tipo deve implementare la proprietà wrappedValue, che restituisce e imposta il valore effettivo. Il compilatore Swift sostituisce gli accessi alla proprietà originale con chiamate a wrappedValue, nascondendo completamente l'implementazione dal codice chiamante. Inoltre, si può definire projectedValue — una proiezione accessibile tramite il simbolo $.
Il vantaggio dei Property Wrappers risiede nella riutilizzabilità della logica. Ad esempio, si può creare un wrapper per la validazione email, la memorizzazione nella cache di valori o la sincronizzazione con l'archivio — e applicarlo a qualsiasi proprietà del progetto. In SwiftUI, questo concetto è usato ovunque: ogni meccanismo di gestione dello stato è implementato come un Property Wrapper separato.
Dichiarando una proprietà con l'annotazione @WrapperType var value: T, il compilatore Swift trasforma il codice. Crea un'istanza di WrapperType e genera l'accesso alla proprietà tramite wrappedValue. Il codice sorgente let x = value diventa let x = _value.wrappedValue, e value = newValue diventa _value.wrappedValue = newValue. Questa trasformazione avviene in fase di compilazione, senza overhead a runtime.
@propertyWrapper
struct Capitalized {
private var text: String
var wrappedValue: String {
get { text }
set { text = newValue.capitalized }
}
init(initialValue: String) {
text = initialValue.capitalized
}
}
Il listing mostra il wrapper Capitalized, che converte automaticamente una stringa in formato con lettera maiuscola. Nell'assegnare un valore, il setter chiama capitalized prima di salvare. Ora qualsiasi proprietà con l'annotazione @Capitalized memorizzerà solo testo correttamente formattato. Questo approccio elimina completamente la duplicazione del codice di validazione e formattazione.
La proiezione (projectedValue) — un canale di comunicazione aggiuntivo accessibile tramite il prefisso $. In SwiftUI, questa funzionalità è usata ovunque: $state fornisce Binding
SwiftUI include cinque Property Wrappers incorporati per la gestione dello stato: @State, @Binding, @ObservedObject, @StateObject e @Environment. Ognuno risolve un compito specifico e viene usato in diversi scenari. @State è progettato per dati locali semplici, @Binding — per passare un riferimento ai dati alle viste figlie, @ObservedObject e @StateObject — per oggetti complessi, @Environment — per valori di sistema dalla gerarchia.
| Wrapper | Scopo | Possesso |
|---|---|---|
| @State | Stato locale di una singola vista | Vista corrente |
| @Binding | Connessione bidirezionale con il genitore | Vista padre |
| @ObservedObject | Osservazione di un oggetto esterno | Proprietario esterno |
| @StateObject | Creazione di ObservableObject | Vista corrente |
| @Environment | Valori di sistema dalla gerarchia | Ambiente SwiftUI |
La scelta di uno specifico Property Wrapper dipende dalla fonte dei dati e dal loro ciclo di vita. Se i dati appartengono a una singola vista e non sono necessari ai componenti figli — usa @State. Se una vista figlia deve modificare i dati del genitore — usa @Binding. Per oggetti utilizzati in più viste, sono adatti @ObservedObject e @StateObject.
@State è un Property Wrapper per memorizzare lo stato locale all'interno di una singola vista. SwiftUI gestisce automaticamente la memoria per le proprietà @State e ridisegna la vista a ogni modifica. @State è adatto a tipi semplici (String, Int, Bool, enum) e strutture che appartengono esclusivamente alla vista corrente. Quando il valore cambia, SwiftUI riesegue la proprietà body.
struct CounterView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("Conteggio: \(count)")
Button("Incrementa") {
count += 1
}
}
}
}
Nell'esempio, la proprietà @State count memorizza il valore corrente del contatore. SwiftUI crea un'area di memorizzazione per questa proprietà nello heap e la lega al ciclo di vita di CounterView. Quando il pulsante viene premuto, count aumenta di 1, SwiftUI rileva il cambiamento e riesegue body, mostrando il nuovo valore. Importante: @State non dovrebbe essere usato per tipi di riferimento complessi — a questo servono @StateObject e @ObservedObject.
@Binding crea un riferimento a una fonte di dati appartenente a un'altra vista. Binding non memorizza un valore da solo — legge e scrive dati tramite @State, @StateObject o un altro Binding passato dal genitore. Questo permette ai componenti figli di modificare lo stato dell'antenato senza possedere direttamente i dati e senza callback.
struct ToggleSwitch: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
Nel listing, ToggleSwitch riceve @BindingBool dalla vista padre. Il genitore crea @State var isToggleOn = false e passa $isToggleOn all'inizializzatore di ToggleSwitch. Quando l'utente aziona l'interruttore all'interno della vista figlia, la modifica si riflette immediatamente nel @State del genitore. Il meccanismo Binding elimina completamente la necessità di delegati o closures per propagare le modifiche verso l'alto nella gerarchia.
@ObservedObject è un Property Wrapper per osservare un'istanza di ObservableObject passata dall'esterno. La vista non possiede questo oggetto — viene creato nel componente padre o iniettato tramite Environment. Quando qualsiasi proprietà @Published all'interno di ObservableObject cambia, SwiftUI ridisegna tutte le viste sottoscritte tramite @ObservedObject.
@StateObject — un wrapper per creare e possedere un ObservableObject direttamente nella vista. A differenza di @ObservedObject, @StateObject garantisce una singola istanza dell'oggetto per l'intero ciclo di vita della vista. Anche se SwiftUI ricrea la struttura della vista (cosa che accade frequentemente), @StateObject preserva l'oggetto esistente e non chiama l'inizializzatore di nuovo.
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
}
struct ProfileView: View {
@StateObject var settings = UserSettings()
var body: some View {
ChildProfileView(settings: settings)
}
}
struct ChildProfileView: View {
@ObservedObject var settings: UserSettings
var body: some View {
Text("Ciao, \(settings.username)")
}
}
Nell'esempio, ProfileView crea UserSettings tramite @StateObject, diventando proprietario dell'oggetto. ChildProfileView riceve la stessa istanza tramite @ObservedObject — osserva ma non gestisce il ciclo di vita. Quando username cambia, entrambe le viste vengono aggiornate. Se ChildProfileView usasse @StateObject invece di @ObservedObject, a ogni rendering verrebbe creata una nuova istanza con il valore iniziale.
La regola chiave: @StateObject viene usato nella vista che crea l'oggetto (fonte di verità), mentre @ObservedObject viene usato nella vista che riceve un oggetto già creato dal genitore. Violare questa regola porta alla perdita dello stato o a ricreazioni impreviste dei dati.
Swift permette di creare Property Wrappers personalizzati per qualsiasi logica ripetitiva di accesso alle proprietà. Basta dichiarare una struttura o classe con l'attributo @propertyWrapper e implementare wrappedValue. Qui sotto è mostrato il wrapper UserDefaultsWrapper, che sincronizza automaticamente il valore con UserDefaults.
@propertyWrapper
struct UserDefaultsWrapper<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) }
}
}
struct AppConfig {
@UserDefaultsWrapper(key: "theme", defaultValue: "light")
var theme: String
}
Il wrapper UserDefaultsWrapper usa un generico T per funzionare con qualsiasi tipo di dato supportato da UserDefaults. Il getter legge il valore per chiave, il setter lo scrive. Applicare @UserDefaultsWrapper(key:defaultValue:) alla proprietà theme la lega automaticamente all'archivio — tutta la logica di UserDefaults è nascosta all'interno del wrapper. Questo è un tipico esempio di riduzione del codice boilerplate con i Property Wrappers.
Quando si creano wrapper personalizzati, è importante considerare le prestazioni. Poiché il getter e il setter vengono chiamati a ogni accesso alla proprietà, non si dovrebbero inserire operazioni I/O pesanti in wrappedValue. Per l'archiviazione asincrona dei dati, è meglio combinare i Property Wrappers con ObservableObject e @Published.
Domande frequenti
@State è progettato per tipi semplici (String, Int, Bool) e strutture, mentre @StateObject è per tipi di riferimento che implementano ObservableObject. @State memorizza il valore direttamente in SwiftUI, @StateObject gestisce un'istanza di classe nello heap.
Sì, @Binding può essere creato da @StateObject, @ObservedObject o da un altro Binding tramite la proiezione $. Binding può anche essere inizializzato da ObservableObject tramite $object.$publishedProperty o da InlineBinding tramite Binding.constant(value).
Per dati globali, usa @EnvironmentObject o inietta ObservableObject tramite EnvironmentValues. @StateObject è adatto per la vista radice con successiva trasmissione tramite @ObservedObject ai componenti figli.
@ObservedObject non possiede l'oggetto — se la vista padre viene ricreata e passa una nuova istanza, @ObservedObject passerà a quella. Per evitare la perdita di stato, la vista proprietaria dovrebbe usare @StateObject.
Sì, ma è più facile usare una combinazione di ObservableObject con @Published e funzioni asincrone all'interno della classe. Property Wrapper è sincrono per natura — wrappedValue viene calcolato a ogni accesso, il che non è adatto per operazioni di lunga durata.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche