@AppStorage w SwiftUI — property wrapper do pracy z UserDefaults, który automatycznie synchronizuje wartość z interfejsem użytkownika. Podczas zmiany właściwości zadeklarowanej przez @AppStorage nowa wartość jest natychmiast zapisywana w UserDefaults, a gdy UserDefaults zmienia się z zewnątrz — przez widget lub rozszerzenie — widok automatycznie się przerysowuje. Według Apple Developer Documentation (2025), @AppStorage obsługuje String, Int, Double, Bool, Data, URL i ich opcjonalne wersje, zapewniając reaktywne przechowywanie ustawień użytkownika bez ręcznego kodu obserwacji.
Najważniejsze
@AppStorage to property wrapper wprowadzony przez Apple w iOS 14, który łączy właściwość widoku z kluczem w UserDefaults. Podczas odczytu właściwości SwiftUI ładuje wartość z UserDefaults na podstawie podanego klucza. Podczas zapisu — zapisuje nową wartość i powiadamia widok o konieczności przerysowania.
Przed pojawieniem się @AppStorage programiści musieli ręcznie odczytywać UserDefaults w onAppear, subskrybować powiadomienie UserDefaults.didChangeNotification i aktualizować @State przy zmianach. @AppStorage automatyzuje cały cykl: deklaracja w jednej linii zastępuje 15–20 linii kodu boilerplate. Co więcej, @AppStorage zapewnia dwukierunkową synchronizację — jeśli wartość UserDefaults zostanie zmieniona z innego procesu (np. App Extension lub Widget), widok i tak otrzyma aktualizację.
Architektonicznie @AppStorage jest zaimplementowany jako DynamicProperty, co pozwala SwiftUI śledzić zależności i przerysowywać widok przy zmianie obserwowanej wartości. To czyni go idealnym do przechowywania ustawień użytkownika: język interfejsu, włączanie/wyłączanie funkcji, ostatnio wybrana zakładka, nazwa użytkownika.
Chociaż @AppStorage używa UserDefaults pod maską, podejścia do pracy z magazynem różnią się zasadniczo. UserDefaults to niskopoziomowe API wymagające ręcznego zarządzania odczytem, zapisem i powiadomieniami o zmianach. @AppStorage to abstrakcja SwiftUI zapewniająca reaktywne zachowanie od razu po wyjęciu z pudełka.
UserDefaults nadaje się do jednorazowych operacji: ładowania ustawień przy starcie aplikacji, zapisywania analityki, buforowania tokenów. @AppStorage — do ustawień, które powinny reaktywnie aktualizować UI: przełączniki motywów, wybór języka, zapisywanie stanu interfejsu. Bezpośrednie używanie UserDefaults wewnątrz widoku to antywzorzec, ponieważ widok nie dowiaduje się o zmianach bez dodatkowej subskrypcji.
| Parametr | @AppStorage | UserDefaults |
|---|---|---|
| Reaktywność | Automatyczna | Wymaga subskrypcji powiadomień |
| Boilerplate | 1 linia na właściwość | 15–20 linii na właściwość |
| Typy | String, Int, Double, Bool, Data, URL | Wszystkie typy + obiekty archiwalne |
| Typy niestandardowe | Przez RawRepresentable | Przez NSKeyedArchiver |
| App Extension | Automatyczna synchronizacja | Ręczna subskrypcja |
Dla prostych ustawień z reaktywnym UI @AppStorage jest preferowanym wyborem. Dla złożonych danych (tablice, słowniki, niestandardowe obiekty) użyj kombinacji UserDefaults z @State i ręczną subskrypcją zmian lub przejdź na SwiftData / Core Data do strukturalnego przechowywania.
@AppStorage obsługuje standardowe typy, które UserDefaults może serializować bezpośrednio: String, Int, Double, Bool, Data, URL. Dla każdego typu istnieje wersja opcjonalna (String?, Int?, Double?, Bool?, Data?, URL?), pozwalająca rozróżnić „nie ustawiono“ i „pusta wartość“.
Do przechowywania niestandardowych typów zgodnych z protokołem RawRepresentable, @AppStorage również działa automatycznie. Jeśli enum ma rawValue typu String lub Int, można go używać bezpośrednio: @AppStorage("theme") var theme: AppTheme = .system. SwiftUI automatycznie serializuje/deserializuje wartość przez rawValue.
enum AppTheme: String {
case system, light, dark
}
struct SettingsView: View {
@AppStorage("username") var username: String = "Guest"
@AppStorage("launchCount") var launchCount: Int = 0
@AppStorage("isDarkMode") var isDarkMode: Bool = false
@AppStorage("appTheme") var theme: AppTheme = .system
@AppStorage("lastOpened") var lastOpened: Date? = nil
var body: some View {
Form {
TextField("Username", text: $username)
Toggle("Dark mode", isOn: $isDarkMode)
Text("Uruchomiono \(launchCount) razy")
}
}
}
W przykładzie użyto różnych typów @AppStorage: String z domyślną wartością „Guest“, Int do licznika uruchomień, Bool do ciemnego motywu, enum AppTheme z rawValue typu String i opcjonalny Date? do czasu ostatniego otwarcia. Każda właściwość jest powiązana z kluczem UserDefaults podanym jako pierwszy argument. Wartość domyślna jest używana, jeśli klucz nie istnieje w magazynie przy pierwszym uruchomieniu.
Jedną z kluczowych zalet @AppStorage jest automatyczne obserwowanie zmian UserDefaults z dowolnego źródła. Jeśli App Extension lub Widget zmieni wartość, @AppStorage w nadrzędnej aplikacji otrzymuje powiadomienie i przerysowuje widok. Jest to osiągane przez mechanizm KVO (Key-Value Observing), który @AppStorage automatycznie ustawia na UserDefaults.didChangeNotification.
W praktyce oznacza to, że jeśli użytkownik zmieni ustawienie w Widget (np. włączy ciemny motyw), aplikacja natychmiast przechwyci tę zmianę. Podobnie działa synchronizacja między główną aplikacją a Share Extension, Watch App lub Today Widget. Deweloper nie musi pisać kodu do międzyprocesowej wymiany danych — @AppStorage robi to automatycznie.
struct ThemeSettingView: View {
@AppStorage("isDarkMode") var isDarkMode: Bool = false
var body: some View {
VStack {
Toggle("Dark Mode", isOn: $isDarkMode)
.onChange(of: isDarkMode) { oldValue, newValue in
print("Tryb ciemny zmieniony na \(newValue)")
}
}
}
}
Toggle jest powiązany z $isDarkMode przez @AppStorage. Przy przełączeniu wartość jest automatycznie zapisywana w UserDefaults pod kluczem „isDarkMode“. Modyfikator .onChange pozwala wykonać dodatkową akcję przy zmianie — na przykład wysłać analitykę lub zaktualizować UI innych ekranów. Jeśli Widget zmieni ten sam klucz, @AppStorage również wywoła onChange, zapewniając spójność stanu.
Rozważmy pełny ekran ustawień aplikacji używający @AppStorage do przechowywania wszystkich konfiguracji. Formularz zawiera sekcje z różnymi typami ustawień: pola tekstowe, przełączniki, liczniki — wszystkie wartości są automatycznie zapisywane w UserDefaults.
struct AppSettingsView: View {
@AppStorage("displayName") var displayName = ""
@AppStorage("notificationsEnabled") var notificationsEnabled = true
@AppStorage("maxResults") var maxResults = 25
@AppStorage("selectedTab") var selectedTab = "home"
var body: some View {
NavigationStack {
Form {
Section(header: Text("Profil")) {
TextField("Display name", text: $displayName)
}
Section(header: Text("Preferencje")) {
Toggle("Enable notifications",
isOn: $notificationsEnabled)
Stepper("Max results: \(maxResults)",
value: $maxResults,
in: 10...100,
step: 5)
}
Section {
Button("Resetuj ustawienia") {
UserDefaults.standard.removePersistentDomain(
forName: Bundle.main.bundleIdentifier!)
}
.tint(.red)
}
}
.navigationTitle("Settings")
}
}
}
Formularz zawiera cztery właściwości @AppStorage różnych typów: String dla nazwy, Bool dla powiadomień, Int dla liczby wyników i String dla wybranej zakładki. Wszystkie kontrolki są powiązane z właściwościami przez Binding ($displayName, $notificationsEnabled itd.). Przycisk „Reset settings“ resetuje wszystkie UserDefaults, usuwając domenę aplikacji — po tym @AppStorage automatycznie wróci do wartości domyślnych.
struct SharedSettingsView: View {
let sharedDefaults = UserDefaults(suiteName: "group.com.example.app")
@AppStorage("widgetTheme", store: UserDefaults(suiteName: "group.com.example.app")!)
var widgetTheme: String = "system"
@AppStorage("widgetColor", store: UserDefaults(suiteName: "group.com.example.app")!)
var widgetColor: String = "niebieski"
var body: some View {
Form {
Picker("Widget theme", selection: $widgetTheme) {
Text("System").tag("system")
Text("Jasny").tag("jasny")
Text("Ciemny").tag("ciemny")
}
Picker("Accent color", selection: $widgetColor) {
Text("Niebieski").tag("blue")
Text("Zielony").tag("zielony")
Text("Czerwony").tag("czerwony")
}
}
}
}
Dla App Group (wspólnego magazynu między aplikacją a rozszerzeniami) @AppStorage przyjmuje parametr store: UserDefaults(suiteName:). Wartości są zapisywane we wspólnym kontenerze dostępnym dla głównej aplikacji, Widget, Watch App i innych rozszerzeń tej samej grupy. Widget może odczytywać te ustawienia, a przy zmianie w aplikacji — Widget automatycznie aktualizuje się przez mechanizm obserwacji UserDefaults.
Często zadawane pytania
@State przechowuje wartość tylko w pamięci i resetuje się przy ponownym uruchomieniu aplikacji. @AppStorage zapisuje wartość w UserDefaults i przywraca ją przy następnym uruchomieniu. Używaj @State do tymczasowych danych ekranu, @AppStorage — do ustawień, które powinny przetrwać ponowne uruchomienie.
Tak, jeśli Enum implementuje protokół RawRepresentable z rawValue typu String lub Int. Przykład: @AppStorage("theme") var theme: AppTheme = .system. SwiftUI automatycznie serializuje enum przez rawValue i przywraca przy ładowaniu.
Wywołaj UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) dla standardowego magazynu lub removeObject(forKey:) dla konkretnego klucza. Po wyczyszczeniu wszystkie właściwości @AppStorage wrócą do domyślnych wartości podanych w deklaracji.
Tak, do synchronizacji między aplikacją a rozszerzeniami użyj App Group: @AppStorage("key", store: UserDefaults(suiteName: "group.com.example.app")!). Widget, Share Extension i Watch App mogą odczytywać i zapisywać w tym samym UserDefaults, a zmiany są automatycznie śledzone.
@AppStorage używa UserDefaults, który jest przeznaczony do niewielkich ilości danych: ustawień, tokenów, liczników. Zalecany limit — do 100 KB na aplikację. Do strukturalnych lub dużych danych (tablice obiektów, pliki multimedialne) używaj SwiftData, Core Data lub systemu plików.
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ż