UserDefaults — важный функционал в разработке iOS для хранения данных, которые сохраняются даже при перезапуске приложения. Раньше в UIKit для чтения и записи данных обращались к UserDefaults напрямую. Однако этот подход кажется громоздким в SwiftUI, особенно учитывая декларативный характер последнего.
Это различие четко показывается в @State, одной из основных оберток свойств SwiftUI: когда меняется сохраненное в @State значение, представление автоматически переотрисовывается. Но имеется недостаток: значения @State не сохраняются — когда приложение перезапускается, данные сбрасываются.
UserDefaults было бы неплохо перенять поведение @State: легко считывать и записывать свойства, переотрисовывая представление при изменении значений, но при этом длительно сохраняя данные.

Эту задачу в Apple решили появлением @AppStorage в iOS 14.
Теперь при изменении сохраняемых значений представление обновляется автоматически. Но, в отличие от @State, с @AppStorage эти значения сохраняются — данные остаются даже после перезапуска приложения.
Благодаря беспроблемному взаимодействию с @Binding, @AppStorage полезна и для работы с базовыми компонентами вроде Toggle или TextField, которым требуются параметры Binding<Type>
.
Несмотря на удобство, у @AppStorage имеется потенциальная проблема с самим SwiftUI: когда в одно представление упаковано слишком много свойств, сложность увеличивается и управлять зависимостями становится труднее.

Чтобы смягчить последствия этой проблемы, разработчики прибегают к шаблону MVVM, который стал популярен в SwiftUI. Они создают специальный класс ViewModel для конкретных задач, в котором содержатся необходимые свойства и методы.
Также, стремясь упростить применение UserDefaults в SwiftUI, создают пользовательские обертки свойств (как показано ниже) или задействуют свойства @Published с didSet во ViewModel.

Но такой подход обычно тяжеловесен и зачастую не нужен. А вы знали, что @AppStorage используется непосредственно во ViewModel, который соответствует ObservableObject? @State не применяется во ViewModel, а вот @AppStorage прекрасно работает вне представления.
Выполним рефакторинг созданного выше представления, у которого слишком много свойств @AppStorage. Начнем с класса UserDefaultsViewModel
, которым управляются UserDefaults.

Просто объявляем @AppStorage во ViewModel так же, как в представлениях.
Жестко задаем уникальные ключи для @AppStorage или, чтобы повысить сопровождаемость, управляем ими с помощью вложенного перечисления.

Этот UserDefaultsViewModel
располагается на верхнем уровне приложения со @StateObject. Теперь у нас имеется источник истины со всеми UserDefaults приложения.
Чтобы обеспечить легкий доступ к этой ViewModel во всех дочерних представлениях, добавляем ее в иерархию представлений как EnvironmentObject
.

В этом-то и преимущество @EnvironmentObject — ViewModel при необходимости доступна любому дочернему представлению.
Теперь свойства @AppStorage во ViewModel легко считываются и записываются. Аналогично @State и @Published, подключается и binding($).
При таком подходе UserDefaults более адаптированы к SwiftUI благодаря соответствующим чистым API.
Но у этого метода имеются ограничения. Как и с UserDefaults, в @AppStorage не сохраняются пользовательские типы. Проблема решается применением Codable и обработкой процессов кодирования/декодирования, но это уже тема другой статьи.
Читайте также:
- Как вернуть контроль над состоянием данных с RemoteResult
- Темная сторона однонаправленных архитектур Swift
- Дорожная карта iOS для разработчиков Android: основы
Читайте нас в Telegram, VK и Дзен
Перевод статьи Jaeho Yoo: [SwiftUI] Unlocking the Potential of @AppStorage: Managing UserDefaults with ViewModels