Property Wrapper to mechanizm Swifta, który dodaje warstwę logiki do dostępu i modyfikacji właściwości bez powielania kodu. W SwiftUI Property Wrappers stały się podstawą zarządzania stanem widoków: @State, @Binding, @ObservedObject, @StateObject i @Environment. Według dokumentacji Swifta (2025), obwoluty właściwości redukują kod boilerplate w projektach średnio o 40%. Zrozumienie Property Wrapper jest niezbędne dla każdego iOS developera do efektywnej pracy z frameworkiem.
Najważniejsze
Property Wrapper — to konstrukcja językowa Swifta wprowadzona w wersji 5.1, która pozwala enkapsulować logikę dostępu do właściwości w osobny typ. Zamiast pisać powtarzające się gettery i settery w każdej klasie, programista deklaruje obwolutę raz i stosuje ją poprzez adnotację @ przed typem. Swift automatycznie opakowuje właściwość w określony typ, wywołując jego metody wrappedValue i projectedValue podczas odczytu i zapisu. Według Apple (WWDC 2019), Property Wrappers stały się kluczową abstrakcją dla SwiftUI.
Obwoluta właściwości to struktura lub klasa z atrybutem @propertyWrapper. Wewnątrz taki typ musi zaimplementować właściwość wrappedValue, która zwraca i ustawia aktualną wartość. Kompilator Swifta zastępuje odwołania do oryginalnej właściwości wywołaniami wrappedValue, całkowicie ukrywając implementację przed kodem wywołującym. Dodatkowo można zdefiniować projectedValue — projekcję dostępną przez symbol $.
Zaletą Property Wrappers jest wielokrotne użycie logiki. Na przykład można stworzyć obwolutę do walidacji emaila, buforowania wartości lub synchronizacji z magazynem — i stosować ją do dowolnej właściwości w projekcie. W SwiftUI ta koncepcja jest używana wszechobecnie: każdy mechanizm zarządzania stanem jest zaimplementowany jako osobny Property Wrapper.
Przy deklaracji właściwości z adnotacją @WrapperType var value: T kompilator Swifta transformuje kod. Tworzy instancję WrapperType i generuje dostęp do właściwości przez wrappedValue. Kod źródłowy let x = value zamienia się w let x = _value.wrappedValue, a value = newValue — w _value.wrappedValue = newValue. Taka transformacja zachodzi na etapie kompilacji, bez narzutu w runtime.
@propertyWrapper
struct Capitalized {
private var text: String
var wrappedValue: String {
get { text }
set { text = newValue.capitalized }
}
init(initialValue: String) {
text = initialValue.capitalized
}
}
Listing pokazuje obwolutę Capitalized, która automatycznie przekształca tekst na format z wielką literą. Przy przypisaniu wartości setter wywołuje capitalized przed zapisaniem. Teraz każda właściwość z adnotacją @Capitalized będzie przechowywać tylko poprawnie sformatowany tekst. Takie podejście całkowicie eliminuje powielanie kodu walidacji i formatowania.
Projekcja (projectedValue) — dodatkowy kanał komunikacji, dostępny przez prefiks $. W SwiftUI ta możliwość jest używana wszechobecnie: $state daje Binding
SwiftUI zawiera pięć wbudowanych Property Wrappers do zarządzania stanem: @State, @Binding, @ObservedObject, @StateObject i @Environment. Każdy z nich rozwiązuje konkretne zadanie i jest używany w różnych scenariuszach. @State jest przeznaczony do prostych danych lokalnych, @Binding — do przekazywania referencji do danych w widokach potomnych, @ObservedObject i @StateObject — do złożonych obiektów, @Environment — do wartości systemowych z hierarchii.
| Wrapper | Przeznaczenie | Posiadanie |
|---|---|---|
| @State | Stan lokalny jednego widoku | Bieżący widok |
| @Binding | Dwukierunkowe połączenie z rodzicem | Widok nadrzędny |
| @ObservedObject | Obserwacja zewnętrznego obiektu | Zewnętrzny właściciel |
| @StateObject | Tworzenie ObservableObject | Bieżący widok |
| @Environment | Wartości systemowe z hierarchii | Środowisko SwiftUI |
Wybór konkretnego Property Wrapper zależy od źródła danych i ich cyklu życia. Jeśli dane należą do jednego widoku i nie są potrzebne komponentom potomnym — użyj @State. Jeśli widok potomny musi modyfikować dane rodzica — zastosuj @Binding. Dla obiektów używanych w wielu widokach odpowiednie są @ObservedObject i @StateObject.
@State — to Property Wrapper do przechowywania stanu lokalnego wewnątrz jednego widoku. SwiftUI automatycznie zarządza pamięcią dla @State-właściwości i przerysowuje widok przy każdej zmianie. @State nadaje się do prostych typów (String, Int, Bool, enum) i struktur, które należą wyłącznie do bieżącego widoku. Przy zmianie wartości SwiftUI ponownie uruchamia właściwość body.
struct CounterView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("Liczba: \(count)")
Button("Zwiększ") {
count += 1
}
}
}
}
W przykładzie @State-właściwość count przechowuje bieżącą wartość licznika. SwiftUI tworzy obszar przechowywania dla tej właściwości na stercie i wiąże go z cyklem życia CounterView. Po naciśnięciu przycisku count zwiększa się o 1, SwiftUI wykrywa zmianę i ponownie uruchamia body, wyświetlając nową wartość. Ważne: @State nie należy używać dla złożonych typów referencyjnych — do tego służą @StateObject i @ObservedObject.
@Binding tworzy referencję do źródła danych należącego do innego widoku. Binding nie przechowuje wartości samodzielnie — odczytuje i zapisuje dane przez @State, @StateObject lub inny Binding przekazany od rodzica. Pozwala to komponentom potomnym modyfikować stan przodka bez bezpośredniego posiadania danych i bez callbacków.
struct ToggleSwitch: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
Na listingu ToggleSwitch otrzymuje @BindingBool od widoku nadrzędnego. Rodzic tworzy @State var isToggleOn = false i przekazuje $isToggleOn do inicjalizatora ToggleSwitch. Gdy użytkownik przełącza Toggle wewnątrz widoku potomnego, zmiana natychmiast odbija się w nadrzędnym @State. Mechanizm Binding całkowicie eliminuje potrzebę delegatów lub domknięć do przekazywania zmian w górę hierarchii.
@ObservedObject — to Property Wrapper do obserwacji instancji ObservableObject przekazanej z zewnątrz. Widok nie jest właścicielem tego obiektu — jest on tworzony w komponencie nadrzędnym lub wstrzykiwany przez Environment. Gdy jakakolwiek właściwość @Published wewnątrz ObservableObject ulega zmianie, SwiftUI przerysowuje wszystkie widoki subskrybujące przez @ObservedObject.
@StateObject — obwoluta do tworzenia i posiadania ObservableObject bezpośrednio w widoku. W przeciwieństwie do @ObservedObject, @StateObject gwarantuje pojedynczą instancję obiektu na cały cykl życia widoku. Nawet jeśli SwiftUI odtworzy strukturę widoku (co zdarza się często), @StateObject zachowa istniejący obiekt i nie wywoła inicjalizatora ponownie.
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("Witaj, \(settings.username)")
}
}
W przykładzie ProfileView tworzy UserSettings przez @StateObject, stając się właścicielem obiektu. ChildProfileView otrzymuje tę samą instancję przez @ObservedObject — obserwuje, ale nie zarządza cyklem życia. Przy zmianie username oba widoki się aktualizują. Gdyby ChildProfileView używał @StateObject zamiast @ObservedObject, przy każdym renderze tworzyłby się nowa instancja z początkową wartością.
Kluczowa zasada: @StateObject stosuje się w widoku, który tworzy obiekt (źródło prawdy), a @ObservedObject — w widoku, który otrzymuje gotowy obiekt od rodzica. Naruszenie tej zasady prowadzi do utraty stanu lub nieoczekiwanego odtwarzania danych.
Swift pozwala tworzyć własne Property Wrappers dla dowolnej powtarzającej się logiki dostępu do właściwości. Wystarczy zadeklarować strukturę lub klasę z atrybutem @propertyWrapper i zaimplementować wrappedValue. Poniżej pokazana jest obwoluta UserDefaultsWrapper, która automatycznie synchronizuje wartość z 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
}
Obwoluta UserDefaultsWrapper używa generyka T do pracy z dowolnym typem danych obsługiwanym przez UserDefaults. Getter odczytuje wartość po kluczu, setter zapisuje. Zastosowanie @UserDefaultsWrapper(key:defaultValue:) do właściwości theme automatycznie wiąże ją z magazynem — cała logika pracy z UserDefaults jest ukryta wewnątrz obwoluty. To typowy przykład redukcji kodu boilerplate za pomocą Property Wrappers.
Przy tworzeniu własnych obwolut należy uwzględnić wydajność. Ponieważ getter i setter są wywoływane przy każdym dostępie do właściwości, nie należy umieszczać w wrappedValue ciężkich operacji I/O. Do asynchronicznego przechowywania danych lepiej łączyć Property Wrappers z ObservableObject i @Published.
Często zadawane pytania
@State jest przeznaczony do prostych typów (String, Int, Bool) i struktur, a @StateObject — do typów referencyjnych implementujących ObservableObject. @State przechowuje wartość bezpośrednio w SwiftUI, @StateObject zarządza instancją klasy na stercie.
Tak, @Binding można utworzyć z @StateObject, @ObservedObject lub z innego Binding za pomocą projekcji $. Binding inicjalizuje się również z ObservableObject przez $object.$publishedProperty lub z InlineBinding przez Binding.constant(value).
Do danych globalnych użyj @EnvironmentObject lub wstrzykuj ObservableObject przez EnvironmentValues. @StateObject nadaje się dla widoku głównego z późniejszym przekazywaniem przez @ObservedObject do komponentów potomnych.
@ObservedObject nie jest właścicielem obiektu — jeśli widok nadrzędny zostanie odtworzony i przekaże nową instancję, @ObservedObject przełączy się na nią. Aby uniknąć utraty stanu, widok będący właścicielem powinien używać @StateObject.
Tak, ale łatwiej użyć kombinacji ObservableObject z @Published i funkcjami async wewnątrz klasy. Property Wrapper jest synchroniczny z natury — wrappedValue jest obliczane przy każdym dostępie, co nie nadaje się do długotrwałych operacji.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również