Last active
December 21, 2023 05:37
-
-
Save danhalliday/a4e70a866334d4b254bd61f31420261a to your computer and use it in GitHub Desktop.
Sketch of a settings dependency.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
import Dependencies | |
import AsyncExtensions | |
public struct Setting<Value:Sendable>: Sendable { | |
let id: String | |
let name: String | |
let fallback: Value | |
let load: @Sendable (String) -> Value? | |
let save: @Sendable (String, Value) -> Void | |
let stream: @Sendable (String) -> AsyncStream<Value?> | |
} | |
// MARK: - API | |
extension Setting { | |
public var value: Value { | |
get { load(id) ?? fallback } nonmutating set { save(id, newValue) } | |
} | |
public var values: AsyncStream<Value> { | |
stream(id).map { value in value ?? fallback }.eraseToStream() | |
} | |
public var changes: AsyncStream<Void> { | |
stream(id).map { _ in }.eraseToStream() | |
} | |
public var binding: Binding<Value> { | |
Binding(get: { value }, set: { newValue in value = newValue }) | |
} | |
} | |
// MARK: - Constructors | |
extension Setting { | |
static func mock(_ fallback: Value) -> Setting { | |
let subject = AsyncCurrentValueSubject(Optional<Value>.none) | |
return Setting( | |
id: "", | |
name: "", | |
fallback: fallback, | |
load: { _ in subject.value }, | |
save: { _, newValue in subject.value = newValue }, | |
stream: { _ in subject.eraseToStream() } | |
) | |
} | |
static func constant(_ fallback: Value) -> Setting { | |
Setting( | |
id: "", | |
name: "", | |
fallback: fallback, | |
load: { _ in nil }, | |
save: { _, _ in }, | |
stream: { _ in [].async.eraseToStream() } | |
) | |
} | |
// TODO: Maybe something like Setting.userDefaultsBased(...) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os.log | |
import Dependencies | |
import DependenciesAdditions | |
extension Settings { | |
static var live: Settings { | |
@Dependency(\.userDefaults) var defaults | |
@Dependency(\.logger["Settings"]) var logger | |
return Settings( | |
editorFontSize: | |
Setting( | |
id: "editor-font-size", | |
name: "Editor Font Size", | |
fallback: 24, | |
load: { id in defaults.integer(forKey: id) }, | |
save: { id, newValue in defaults.set(newValue, forKey: id) }, | |
stream: { id in defaults.integerValues(forKey: id).eraseToStream() } | |
) | |
.logged(using: logger) | |
.tracked(), | |
isDarkModeEnabled: | |
Setting( | |
id: "dark-mode", | |
name: "Dark Mode", | |
fallback: false, | |
load: { id in defaults.bool(forKey: id) }, | |
save: { id, newValue in defaults.set(newValue, forKey: id) }, | |
stream: { id in defaults.boolValues(forKey: id).eraseToStream() } | |
) | |
.logged(using: logger) | |
.tracked() | |
) | |
} | |
} | |
// MARK: - Helpers | |
extension Setting { | |
func logged(using logger: Logger) -> Self { | |
Setting( | |
id: id, | |
name: name, | |
fallback: fallback, | |
load: load, | |
save: { id, newValue in | |
let name = self.name | |
let description = String(describing: newValue) | |
logger.info("Setting <\(name)> changed to <\(description)>") | |
save(id, value) | |
}, | |
stream: stream | |
) | |
} | |
func tracked(/* eg: using analytics: Analytics */) -> Self { | |
Setting( | |
id: id, | |
name: name, | |
fallback: fallback, | |
load: load, | |
save: { id, newValue in | |
// eg: analytics.track(property: id, value: newValue) | |
save(id, value) | |
}, | |
stream: stream | |
) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extension Settings { | |
static var mock: Settings { | |
Settings( | |
editorFontSize: .mock(24), | |
isDarkModeEnabled: .mock(false) | |
) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Dependencies | |
import AsyncExtensions | |
public struct Settings: Sendable { | |
public var editorFontSize: Setting<Int> | |
public var isDarkModeEnabled: Setting<Bool> | |
} | |
// MARK: - Dependency | |
extension Settings: DependencyKey { | |
public static let liveValue = Settings.live | |
public static let previewValue = Settings.mock | |
public static let testValue = Settings.mock | |
} | |
extension DependencyValues { | |
public var settings: Settings { | |
get { self[Settings.self] } | |
set { self[Settings.self] = newValue } | |
} | |
} | |
// MARK: - Helpers | |
extension Settings { | |
public var changes: AsyncStream<Void> { | |
merge( | |
editorFontSize.changes, | |
isDarkModeEnabled.changes | |
).eraseToStream() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import Dependencies | |
@MainActor public final class SettingsScreen: ObservableObject { | |
public init() {} | |
// MARK: - Dependencies | |
@Dependency(\.settings.changes) var changes | |
@Dependency(\.settings.editorFontSize) var editorFontSize | |
@Dependency(\.settings.isDarkModeEnabled) var isDarkModeEnabled | |
// MARK: - Tasks | |
func trackSettingsChanges() async { | |
for await _ in changes { | |
objectWillChange.send() | |
} | |
} | |
func trackEditorFontSizeValues() async { | |
for await size in editorFontSize.values { | |
print("Editor font size changed to: \(size)") | |
} | |
} | |
// MARK: - State | |
var darkModeStatusLabel: String { | |
"Dark Mode is \(isDarkModeEnabled.value ? "enabled" : "disabled")" | |
} | |
// MARK: - Actions | |
func toggleDarkModeButtonWasPressed() { | |
isDarkModeEnabled.value.toggle() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
public struct SettingsScreenView: View { | |
@ObservedObject var model: SettingsScreen | |
public init(model: SettingsScreen) { | |
self.model = model | |
} | |
// MARK: - Body | |
public var body: some View { | |
List { | |
Text(model.darkModeStatusLabel) | |
Toggle(isOn: model.isDarkModeEnabled.binding) { | |
Text("Use Dark Mode") | |
} | |
Button { model.toggleDarkModeButtonWasPressed() } label: { | |
Text("Toggle Dark Mode") | |
} | |
Stepper(value: model.editorFontSize.binding, in: 16...64, step: 1) { | |
Text("Editor Font Size: \(model.editorFontSize.value)") | |
} | |
} | |
.task { await model.trackSettingsChanges() } | |
.task { await model.trackEditorFontSizeValues() } | |
} | |
} | |
struct SettingsScreenPreviews: PreviewProvider { | |
@StateObject static var model = SettingsScreen() | |
static var previews: some View { | |
SettingsScreenView(model: model) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment