Я в замешательстве. Видел один обучающий ролик, где вроде бы серьёзный программист применяет @AppStorage обёртку и создаёт для той же переменной дубликат с обёрткой @Published, дабы данные не только сохранялись на следующую сессию, но и интерфейс перерисовывался в зависимости от значения переменной (что даёт обёртка @Published). Потом следовали пляски с бубном, чтобы одной переменной присвоить значение другой при инициализации и обратно. В общем, сложно и даже как-то тупо. В связи с чем у меня вопрос. Можно ли как-то создать одну переменную, объединяющую оба свойства? Если нет, то как проще всего добиться подобного результата? Вот конкретная задача. У меня на странице “Настройки” в приложении есть переключатель, допустим, переключения звука “вкл.” - “выкл.”. Хочу, чтобы значение привязанной переменной сохранялось с выключением приложения. А также хочу, чтобы на самом выключателе соответственно менялась надпись “вкл.” - “выкл.”. Есть ли какие-то здравые идеи, как это сделать? Заранее спасибо.
@AppStorage и @Published - как скомбинировать?
Любые “обёртки” (property wrapper) при изменении значения автоматически перерисовывают интерфейс. Ничего дополнительно придумывать не надо.
struct ContentView: View {
@AppStorage("sound") var sound: Bool = false
var body: some View {
Toggle(sound ? "ON" : "OFF", isOn: $sound)
.padding()
}
}
И обычная запись без Апстореджа и с вьюмоделью
class ViewModel: ObservableObject {
init() { sound = UserDefaults.standard.bool(forKey: "sound1") }
@Published var sound: Bool {
didSet {
UserDefaults.standard.set(sound, forKey: "sound1")
}
}
}
struct ContentView: View {
@ObservedObject var vm = ViewModel()
var body: some View {
Toggle(vm.sound ? "ON" : "OFF", isOn: $vm.sound)
.padding()
}
}
Спасибо за такой развёрнутый ответ. А вот в первом случае можно как-то получить доступ к этой переменной @AppStorage(“sound”) var sound из других классов? У меня, собственно, в этом загвоздка. То есть она должна быть доступна извне для разных калькуляций и т. д. Или саму эту переменную убрать в отдельный класс и вызывать её из ContentView (у меня не получалось, Toggle не показывал изменение “ON” : “OFF”). Или всё же придётся прибегать к более сложной записи, используя UserDefaults?
- Если вам надо работать с данной переменной только в представлениях (вью), то @AppStorage отлично подходит - она автоматом загружает из дефолтс нужные данные по указаномму ключу при инициализации и перерисовке интерфейса. То есть данную переменную размещайте в разных вью и она получит данные из одного ключа из дефолтс.
Если вы хотите работать с данной переменной из одного класса (вьюмодель), то можно только инициализировать вью модель при старте приложения и обращаться через окружение (@EnvironmentObject
) из любого вью
@main
struct TestApp: App {
let vm = ViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(vm)
}
}
}
struct ContentView: View {
@EnvironmentObject var vm: ViewModel
var body: some View {
...
}
}
-
Другой момент если вам нужно доступ из других классов/файлов (не вьюшек), то достаточно сделать синглтон и обращаться через него.
class ViewModel1: ObservableObject { init() { sound = UserDefaults.standard.bool(forKey: "sound1") } static let shared = ViewModel1() @Published var sound: Bool { didSet { UserDefaults.standard.set(sound, forKey: "sound1") } } } @main struct TestApp: App { let vm = ViewModel1.shared var body: some Scene { WindowGroup { ContentView() .environmentObject(vm) } } } class ViewModel2: ObservableObject { @Published var newSound = ViewModel1.shared.sound }
Благодарю. Применил, работает. Снёс класс Settings, запихнул все настройки во ViewModel для простоты. Буду пытаться создавать по минимуму классов, пока нет понятия, что, откуда и куда.
Воспользовался последним способом с созданием синглтона. Всё-таки сеттинг перенёс в отдельный класс. Передал объекты во вью таким образом:
@main
struct WeatherAppApp: App {
let viewModel = ViewModel.shared
let locationVM = LocationVM.shared
let settingsVM = SettingsVM.shared
var body: some Scene {
WindowGroup {
NavigationView() {
if !LocationVM.shared.locationWasFoundOnAppFirstStart {
FirstRunView()
.environmentObject(viewModel)
.environmentObject(locationVM)
.environmentObject(settingsVM)
} else {
MainView()
.environmentObject(viewModel)
.environmentObject(locationVM)
.environmentObject(settingsVM)
}
}
}
}
}
Но возникает вопрос, как передать environmentObject-ы во вьюшку, которая появляется впоследствии. Попробовал сделать по аналогии:
.sheet(isPresented: $showSettingsView) {
SettingsView()
.environmentObject(ViewModel.shared)
.environmentObject(LocationVM.shared)
.environmentObject(SettingsVM.shared)
}
Теперь в самой вьюшке SettingsView он ругается на байндинги, что мол не может найти $SettingsVM in scope в том самом коде регулировки настройки вкл-выкл:
Toggle(SettingsVM.shared.isSensitiveToWind ? "Sensitive" : "Normal", isOn: $SettingsVM.shared.isSensitiveToWind)
Не подскажете, как с этой бедой разобраться?
Вам нужно разобраться/почитать/посмотреть как работать с объектами в окружении. Они (объекты) подключаются в окружение один раз при старте приложения (передаётся инициализированный объект):
MainView()
.environmentObject(ViewModel.shared)
Потом внутри вью достаются при помощи:
@EnvironmentObject var vm: ViewModel
Здесь вы повторно передали инициализированный объект из синглтона, а не забрали из окружения уже существующий. В целом да, вроде это класс и там и там ссылка на один объект, но при работе с вью и тем более окружением, требуется брать объекты, инициализированные внутри вью:
.sheet(isPresented: $showSettingsView) {
SettingsView()
.environmentObject(ViewModel.shared)
}
Из любых вью переменные из окружения достаются при помощи @EnvironmentObject
с указанием типа необходимой переменной (переменную обозвать можно как угодно).
Но при работе с .sheet есть нюанс (читай баг):
SwiftUI 1.0 - действительно требовал отдельно подключать переменные к вью внутри шита, иначе всё падало.
SwiftUI 2.0 - хотели исправить, но не доработали: теперь шит появляется норм без специального повторного подключения переменных в окружение вью в шите, но при дисмисе всё упадёт, если не подключить. То есть в теории работает без подключения, если шит в последующем не будет дисмиснут. В реале это 0,1% всех ситуаций ))
SwiftUI 3.0 - вроде наконец починили (3 года! блин ), но надо больше тестить.
Ваша ошибка, что вы во вью в шите передаёте ВМ из синглтона (инициализирована не внутри окружения/вью), а не из окружения.
Надо в родительском вью забрать ВМ из окружения
@EnvironmentObject var vm: ViewModel
и передать в шит
.sheet(isPresented: $showSettingsView) {
SettingsView()
.environmentObject(vm)
}