admin管理员组文章数量:1244397
I've learned how to read/write application data from/to a file in my iOS application. I've also learned how to put data in the @Environment for use my various view objects. I just can't seem to figure out how to link those together. I have some app data (e.g. some configuration values):
@Observable class Config: Codable {
var someConfigVal: Bool = false
}
And have defined a "store" to read and write this info.
@Observable class ConfigStore {
var configuration: Config = Config()
private static func fileURL() throws -> URL {
try FileManager.default.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
)
.appendingPathComponent("config.data")
}
func load() async throws {
let task = Task<Config, Error> {
let fileURL = try Self.fileURL()
guard let data = try? Data(contentsOf: fileURL) else {
return Config()
}
let storedConfig = try JSONDecoder().decode(Config.self, from: data)
print("loaded", storedConfig.someConfigVal)
return storedConfig
}
let appConfig = try await task.value
self.configuration = appConfig
print("load", self.configuration.someConfigVal)
}
func save(appConfig: Config) async throws {
print("Some val on save", appConfig.someConfigVal)
let task = Task {
let data = try JSONEncoder().encode(appConfig)
let outfile = try Self.fileURL()
try data.write(to: outfile)
}
_ = try await task.value
}
}
extension EnvironmentValues {
var configurationStore: ConfigStore {
get { self[ConfigStoreKey.self] }
set { self[ConfigStoreKey.self] = newValue }
}
}
private struct ConfigStoreKey: EnvironmentKey {
static let defaultValue: ConfigStore = ConfigStore()
}
I'm currently creating this store as a @State
object in the main application object along with the async code to do the file storage management.
@main
struct StoreTestApp: App {
@State private var configStore = ConfigStore()
var body: some Scene {
WindowGroup {
ContentView(configStore: configStore) {
Task {
print("In save task", configStore.configuration.someConfigVal)
do {
try await configStore.save(appConfig: configStore.configuration)
} catch {
fatalError(error.localizedDescription)
}
}
}
.task {
do {
try await configStore.load()
} catch {
fatalError(error.localizedDescription)
}
}
// .environment(configStore)
}
}
}
For the UI, I've created a simple tabbed view: 1 to show the value and 1 to edit it.
struct ContentView: View {
@Environment(\.scenePhase) private var scenePhase
// @Environment(\.configurationStore) private var configStore
var configStore: ConfigStore
let saveAction: () -> Void
var body: some View {
TabView {
ShowView()
.tabItem {
Label("Show", systemImage: "magnifyingglass")
}
ParentEditView()
.tabItem {
Label("Edit", systemImage: "pencil")
}
}
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .inactive {
print("Scene is inactive", configStore.configuration.someConfigVal)
saveAction()
}
}
.environment(configStore)
}
}
struct ShowView: View {
@Environment(\.configurationStore) private var configStore
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text(String(configStore.configuration.someConfigVal))
}
.padding()
}
}
struct ParentEditView: View {
@Environment(\.configurationStore) private var configStore
var body: some View {
EditView(configStore: configStore)
}
}
struct EditView: View {
@Bindable var configStore: ConfigStore
var body: some View {
Toggle("Some val", isOn: $configStore.configuration.someConfigVal)
.onChange(of: configStore.configuration.someConfigVal, { print(configStore.configuration.someConfigVal) })
}
}
When I run the app in the simulator, change the value to true and click the "Home" button, I get the following in the output window:
loaded false
load false
true
Scene is inactive false
In save task false
Some val on save false
Any ideas where I am going wrong or what I should do differently? I'm kind of invested in the @Observable
functionality at this point (lots of classes built with it), but if changing that's the only to "solve" this, I'll do it but please try to work with me on that. Thanks!
I've learned how to read/write application data from/to a file in my iOS application. I've also learned how to put data in the @Environment for use my various view objects. I just can't seem to figure out how to link those together. I have some app data (e.g. some configuration values):
@Observable class Config: Codable {
var someConfigVal: Bool = false
}
And have defined a "store" to read and write this info.
@Observable class ConfigStore {
var configuration: Config = Config()
private static func fileURL() throws -> URL {
try FileManager.default.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
)
.appendingPathComponent("config.data")
}
func load() async throws {
let task = Task<Config, Error> {
let fileURL = try Self.fileURL()
guard let data = try? Data(contentsOf: fileURL) else {
return Config()
}
let storedConfig = try JSONDecoder().decode(Config.self, from: data)
print("loaded", storedConfig.someConfigVal)
return storedConfig
}
let appConfig = try await task.value
self.configuration = appConfig
print("load", self.configuration.someConfigVal)
}
func save(appConfig: Config) async throws {
print("Some val on save", appConfig.someConfigVal)
let task = Task {
let data = try JSONEncoder().encode(appConfig)
let outfile = try Self.fileURL()
try data.write(to: outfile)
}
_ = try await task.value
}
}
extension EnvironmentValues {
var configurationStore: ConfigStore {
get { self[ConfigStoreKey.self] }
set { self[ConfigStoreKey.self] = newValue }
}
}
private struct ConfigStoreKey: EnvironmentKey {
static let defaultValue: ConfigStore = ConfigStore()
}
I'm currently creating this store as a @State
object in the main application object along with the async code to do the file storage management.
@main
struct StoreTestApp: App {
@State private var configStore = ConfigStore()
var body: some Scene {
WindowGroup {
ContentView(configStore: configStore) {
Task {
print("In save task", configStore.configuration.someConfigVal)
do {
try await configStore.save(appConfig: configStore.configuration)
} catch {
fatalError(error.localizedDescription)
}
}
}
.task {
do {
try await configStore.load()
} catch {
fatalError(error.localizedDescription)
}
}
// .environment(configStore)
}
}
}
For the UI, I've created a simple tabbed view: 1 to show the value and 1 to edit it.
struct ContentView: View {
@Environment(\.scenePhase) private var scenePhase
// @Environment(\.configurationStore) private var configStore
var configStore: ConfigStore
let saveAction: () -> Void
var body: some View {
TabView {
ShowView()
.tabItem {
Label("Show", systemImage: "magnifyingglass")
}
ParentEditView()
.tabItem {
Label("Edit", systemImage: "pencil")
}
}
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .inactive {
print("Scene is inactive", configStore.configuration.someConfigVal)
saveAction()
}
}
.environment(configStore)
}
}
struct ShowView: View {
@Environment(\.configurationStore) private var configStore
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text(String(configStore.configuration.someConfigVal))
}
.padding()
}
}
struct ParentEditView: View {
@Environment(\.configurationStore) private var configStore
var body: some View {
EditView(configStore: configStore)
}
}
struct EditView: View {
@Bindable var configStore: ConfigStore
var body: some View {
Toggle("Some val", isOn: $configStore.configuration.someConfigVal)
.onChange(of: configStore.configuration.someConfigVal, { print(configStore.configuration.someConfigVal) })
}
}
When I run the app in the simulator, change the value to true and click the "Home" button, I get the following in the output window:
loaded false
load false
true
Scene is inactive false
In save task false
Some val on save false
Any ideas where I am going wrong or what I should do differently? I'm kind of invested in the @Observable
functionality at this point (lots of classes built with it), but if changing that's the only to "solve" this, I'll do it but please try to work with me on that. Thanks!
- Maybe @AppStorage can help: hackingwithswift/quick-start/swiftui/… – koen Commented Feb 16 at 6:23
- Change class Config to struct. developer.apple/documentation/swift/… – malhal Commented Feb 16 at 6:46
- FYI Using State with class instead of struct brings risk of memory leaks, check for leaks, your objects probably aren’t being deinit and are being init too often. – malhal Commented Feb 16 at 6:48
2 Answers
Reset to default 1To be able to read your config.data
file and use @Observable class Config: Codable
to decode the json data in your ConfigStore func load()
,
you need to have:
@Observable class Config: Codable {
var someConfigVal: Bool = false
enum CodingKeys: String, CodingKey {
case _someConfigVal = "someConfigVal" //<-- here
}
}
This is because the @Observable
macro changes the property name to _someConfigVal
for use by SwiftUI.
EDIT-1:
instead of loading @Observable class
using the environment object, try
using the normal
passing of @Observable class
to Views using
@Environment(ConfigStore.self) private var configStore
.
With the changes as shown in this example code, all works well for me.
@Observable class Config: Codable {
var someConfigVal: Bool = false
enum CodingKeys: String, CodingKey {
case _someConfigVal = "someConfigVal" //<-- here
}
}
@Observable class ConfigStore {
var configuration: Config = Config()
private static func fileURL() throws -> URL {
try FileManager.default.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
)
.appendingPathComponent("config.data")
}
// why you want to use Task I don't know.
func load() async throws {
let task = Task<Config, Error> {
let fileURL = try Self.fileURL()
guard let data = try? Data(contentsOf: fileURL) else {
return Config()
}
let storedConfig = try JSONDecoder().decode(Config.self, from: data)
return storedConfig
}
let appConfig = try await task.value
self.configuration = appConfig
print("----> load storedConfig: ", self.configuration.someConfigVal)
}
// --- here no Task, no async throws
func save() {
do {
print("----> saving: ", configuration.someConfigVal)
let data = try JSONEncoder().encode(configuration) //<-- here
let outfile = try Self.fileURL()
try data.write(to: outfile)
} catch {
print(error)
}
}
}
struct ContentView: View {
@Environment(\.scenePhase) private var scenePhase
@Environment(ConfigStore.self) private var configStore // <--- here
let saveAction: () -> Void
var body: some View {
TabView() {
ShowView()
.tabItem {
Label("Show", systemImage: "magnifyingglass")
}
ParentEditView()
.tabItem {
Label("Edit", systemImage: "pencil")
}
}
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .inactive || newPhase == .background { // <--- for testing
print("--> Scene is inactive or background: ", configStore.configuration.someConfigVal)
saveAction()
}
}
}
}
struct ShowView: View {
@Environment(ConfigStore.self) private var configStore // <--- here
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text(String(configStore.configuration.someConfigVal))
}
.padding()
}
}
struct ParentEditView: View {
@Environment(ConfigStore.self) private var configStore // <--- here
var body: some View {
EditView()
}
}
struct EditView: View {
@Environment(ConfigStore.self) private var configStore // <--- here
var body: some View {
@Bindable var configStore = configStore // <--- here
Toggle("Some val", isOn: $configStore.configuration.someConfigVal)
.onChange(of: configStore.configuration.someConfigVal, {
print("-----> EditView: \(configStore.configuration.someConfigVal)")
})
}
}
@main
struct StoreTestApp: App {
@State private var configStore = ConfigStore()
var body: some Scene {
WindowGroup {
ContentView() {
print("=======> App saving: \(configStore.configuration.someConfigVal)")
configStore.save()
}
.task {
do {
try await configStore.load()
} catch {
print(error)
}
}
.environment(configStore) // <-- here
}
}
}
Note the content of my test congig.data
is just this json data {"someConfigVal":false}
You are mixing up the two different Environment
-related APIs.
There is the @Observable
-based APIs, where you use .environment(_:)
to write to the environment, and @Environment(SomeObservable.self)
to read the environment.
There is the key-based APIs, where you use .environment(\.someKey, value: someValue)
to write to the environment, and @Environment(\.someKey)
to read the environment.
You are using the key-based API to read the environment, and the @Observable
-based API to write to the environment!
Instead of:
@Environment(\.configurationStore) private var configStore
Write:
@Environment(ConfigStore.self) private var configStore
You should not use the key-based APIs for @Observable
objects, because it is very hard to provide a defaultValue
in a concurrency-safe way. Also regarding concurrency, your using Task { ... }
everywhere for no apparent reason is going to give you a lot of trouble on Swift 6. If your intention is to do the loading and saving off of the main actor, you can, but this is not how you should do it. In any case, that's a separate question.
For now, here is some working code with some additional stylistic changes.
@Observable class Config: Codable {
var someConfigVal: Bool = false
}
@Observable class ConfigStore {
var configuration: Config = Config()
private static func fileURL() throws -> URL {
try FileManager.default.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
)
.appendingPathComponent("config.data")
}
func load() throws {
let fileURL = try Self.fileURL()
let appConfig = if let data = try? Data(contentsOf: fileURL) {
try JSONDecoder().decode(Config.self, from: data)
} else {
Config()
}
print("loaded", appConfig.someConfigVal)
self.configuration = appConfig
print("load", self.configuration.someConfigVal)
}
func save(appConfig: Config) throws {
print("Some val on save", appConfig.someConfigVal)
let data = try JSONEncoder().encode(appConfig)
let outfile = try Self.fileURL()
try data.write(to: outfile)
}
}
@main
struct YourApp: App {
@State private var configStore = ConfigStore()
var body: some Scene {
WindowGroup {
ContentView {
print("In save task", configStore.configuration.someConfigVal)
do {
try configStore.save(appConfig: configStore.configuration)
} catch {
print(error)
fatalError()
}
}
.task {
do {
try configStore.load()
} catch {
print(error)
fatalError()
}
}
.environment(configStore)
}
}
}
struct ContentView: View {
@Environment(\.scenePhase) private var scenePhase
@Environment(ConfigStore.self) private var configStore
let saveAction: () -> Void
var body: some View {
TabView {
ShowView()
.tabItem {
Label("Show", systemImage: "magnifyingglass")
}
EditView()
.tabItem {
Label("Edit", systemImage: "pencil")
}
}
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .inactive {
print("Scene is inactive", configStore.configuration.someConfigVal)
saveAction()
}
}
}
}
struct ShowView: View {
@Environment(ConfigStore.self) private var configStore
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text(String(configStore.configuration.someConfigVal))
}
.padding()
}
}
struct EditView: View {
@Environment(ConfigStore.self) private var configStore
var body: some View {
@Bindable var configStore = self.configStore
Toggle("Some val", isOn: $configStore.configuration.someConfigVal)
.onChange(of: configStore.configuration.someConfigVal, { print(configStore.configuration.someConfigVal) })
}
}
本文标签: swiftuiHow do I add a loaded object to the environmentStack Overflow
版权声明:本文标题:swiftui - How do I add a loaded object to the environment? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1740223473a2244254.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论