Skip to content

Instantly share code, notes, and snippets.

@dehlen
Created May 14, 2024 12:34
Show Gist options
  • Save dehlen/972172d750555072b6096f19504ee60c to your computer and use it in GitHub Desktop.
Save dehlen/972172d750555072b6096f19504ee60c to your computer and use it in GitHub Desktop.
Showcasing a concurrency related warning. Discussion: https://chaos.social/@dvk/112438279480546893
import Foundation
import Observation
import SwiftUI
@MainActor
@Observable final class UserPreferences {
private class Storage {
private static let store = UserDefaults.standard
@AppStorage("isSoundEnabled", store: store) var isSoundEnabled: Bool = true
init() {}
}
static let shared = UserPreferences()
private let storage = Storage()
var isSoundEnabled: Bool {
didSet {
storage.isSoundEnabled = isSoundEnabled
}
}
private init() {
isSoundEnabled = storage.isSoundEnabled
}
}
struct UserPreferencesKey: EnvironmentKey {
static let defaultValue: UserPreferences = .shared
}
extension EnvironmentValues {
var userPreferences: UserPreferences {
get { self[UserPreferencesKey.self] }
set { self[UserPreferencesKey.self] = newValue }
}
}
@mattmassicotte
Copy link

Ok, here is a potential solution. What I did was transform the instance variable isSoundEnabled into a pure wrapper. This class now has no state. And honestly, I think this is an improvement, because state was previously being duplicated into two places.

import Foundation
import Observation
import SwiftUI

@MainActor
@Observable final class UserPreferences {
	private class Storage {
		private static let store = UserDefaults.standard
		@AppStorage("isSoundEnabled", store: store) var isSoundEnabled: Bool = true
	}

	static nonisolated let shared = UserPreferences()
	private let storage = Storage()

	var isSoundEnabled: Bool {
		get {
			storage.isSoundEnabled
		} set {
			storage.isSoundEnabled = newValue
		}
	}

	private nonisolated init() {

	}
}

struct UserPreferencesKey: EnvironmentKey {
	static let defaultValue: UserPreferences = .shared
}

extension EnvironmentValues {
	var userPreferences: UserPreferences {
		get { self[UserPreferencesKey.self] }
		set { self[UserPreferencesKey.self] = newValue }
	}
}

But, I still think that the root issue is you have too many levels of indirection between your prefs and the UI. I've struggled with AppStorage too, I don't think it is a particularly well-designed API. Here's the link again that might help. I didn't have a particularly easy time implementing this myself, but I think its overall much closer to an idealized version of what is needed here.

https://fatbobman.com/en/posts/appstorage/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment