GBA001/DeltaFeatures/Option.swift

210 lines
7.8 KiB
Swift

//
// Option.swift
// Delta
//
// Created by Riley Testut on 4/7/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import SwiftUI
import Combine
@propertyWrapper
public class Option<Value: OptionValue, DetailView: View>: _AnyOption
{
// Nil name == hidden option.
public let name: LocalizedStringKey?
public let description: LocalizedStringKey?
public let values: [Value]?
public private(set) var detailView: () -> DetailView? = { nil }
// Assigned to property name.
public internal(set) var key: String = ""
// Used for `SettingsUserInfoKey.name` value in .settingsDidChange notification.
public var settingsKey: SettingsName {
guard let feature = self.feature else { return SettingsName(rawValue: self.key) }
let defaultsKey = feature.key + "_" + self.key
return SettingsName(rawValue: defaultsKey)
}
internal weak var feature: (any AnyFeature)?
private let defaultValue: Value
private var valueBinding: Binding<Value> {
Binding(get: {
self.wrappedValue
}, set: { newValue in
self.wrappedValue = newValue
})
}
/// @propertyWrapper
public var projectedValue: some Option {
return self
}
public var wrappedValue: Value {
get {
do {
let wrappedValue = try UserDefaults.standard.optionValue(forKey: self.settingsKey.rawValue, type: Value.self)
return wrappedValue ?? self.defaultValue
}
catch {
print("[ALTLog] Failed to read option value for key \(self.settingsKey.rawValue).", error)
return self.defaultValue
}
}
set {
Task { @MainActor in
// Delay to avoid "Publishing changes from within view updates is not allowed" runtime warning.
(self.feature?.objectWillChange as? ObservableObjectPublisher)?.send()
}
do {
try UserDefaults.standard.setOptionValue(newValue, forKey: self.settingsKey.rawValue)
NotificationCenter.default.post(name: .settingsDidChange, object: nil, userInfo: [SettingsUserInfoKey.name: self.settingsKey, SettingsUserInfoKey.value: newValue])
}
catch {
print("[ALTLog] Failed to set option value for key \(self.settingsKey.rawValue).", error)
}
}
}
private init(defaultValue: Value, name: LocalizedStringKey?, description: LocalizedStringKey?, values: (some Collection<Value>)?)
{
self.defaultValue = defaultValue
self.name = name
self.description = description
self.values = values.map { Array($0) }
self.detailView = { nil }
}
private convenience init(defaultValue: Value, name: LocalizedStringKey?, description: LocalizedStringKey?)
{
self.init(defaultValue: defaultValue, name: name, description: description, values: [Value]?.none)
}
}
// "Hidden" Option (no name, pre-set values, or custom SwiftUI view)
public extension Option where DetailView == EmptyView
{
// Non-Optional
convenience init(wrappedValue: Value)
{
self.init(defaultValue: wrappedValue, name: nil, description: nil)
}
// Optional, default = nil
convenience init() where Value: OptionalProtocol
{
self.init(defaultValue: Value.none, name: nil, description: nil)
}
// Optional, default = non-nil
convenience init(wrappedValue: Value) where Value: OptionalProtocol
{
self.init(defaultValue: wrappedValue, name: nil, description: nil)
}
}
// "Toggle" Option (User-visible, Bool option with default toggle UI)
public extension Option where Value == Bool, DetailView == OptionToggleView
{
// Non-Optional
convenience init(wrappedValue: Value, name: LocalizedStringKey, description: LocalizedStringKey? = nil)
{
self.init(defaultValue: wrappedValue, name: name, description: description)
self.detailView = { [weak self] () -> DetailView? in
guard let self else { return nil }
return OptionToggleView(name: name, selectedValue: self.valueBinding)
}
}
}
// "Picker" Option (User-visible, pre-set options with default picker UI)
public extension Option where Value: LocalizedOptionValue, DetailView == OptionPickerView<Value>
{
// Non-Optional
convenience init(wrappedValue: Value, name: LocalizedStringKey, description: LocalizedStringKey? = nil, values: some Collection<Value>)
{
self.init(defaultValue: wrappedValue, name: name, description: description, values: values)
self.detailView = { [weak self] () -> DetailView? in
guard let self else { return nil }
return OptionPickerView(name: name, options: Array(values), selectedValue: self.valueBinding)
}
}
// Optional, default = nil
convenience init(name: LocalizedStringKey, description: LocalizedStringKey? = nil, values: some Collection<Value>) where Value: OptionalProtocol, Value.Wrapped: LocalizedOptionValue
{
self.init(defaultValue: Value.none, name: name, description: description, values: values)
self.detailView = { [weak self] () -> DetailView? in
guard let self else { return nil }
return OptionPickerView(name: name, options: values.appendingNil(), selectedValue: self.valueBinding)
}
}
// Optional, default = non-nil
convenience init(wrappedValue: Value, name: LocalizedStringKey, description: LocalizedStringKey? = nil, values: some Collection<Value>) where Value: OptionalProtocol, Value.Wrapped: LocalizedOptionValue
{
self.init(defaultValue: wrappedValue, name: name, description: description, values: values)
self.detailView = { [weak self] () -> DetailView? in
guard let self else { return nil }
return OptionPickerView(name: name, options: values.appendingNil(), selectedValue: self.valueBinding)
}
}
}
// "Custom" Option (User-visible, provides SwiftUI view to configure option)
public extension Option where Value: LocalizedOptionValue
{
// Non-Optional
convenience init(wrappedValue: Value, name: LocalizedStringKey, description: LocalizedStringKey? = nil, @ViewBuilder detailView: @escaping (Binding<Value>) -> DetailView)
{
self.init(defaultValue: wrappedValue, name: name, description: description)
self.detailView = { [weak self] in
guard let self else { return nil }
let view = detailView(self.valueBinding)
return view
}
}
// Optional, default = nil
convenience init(name: LocalizedStringKey, description: LocalizedStringKey? = nil, @ViewBuilder detailView: @escaping (Binding<Value>) -> DetailView) where Value: OptionalProtocol, Value.Wrapped: LocalizedOptionValue
{
self.init(defaultValue: Value.none, name: name, description: description)
self.detailView = { [weak self] in
guard let self else { return nil }
let view = detailView(self.valueBinding)
return view
}
}
// Optional, default = non-nil
convenience init(wrappedValue: Value, name: LocalizedStringKey, description: LocalizedStringKey? = nil, @ViewBuilder detailView: @escaping (Binding<Value>) -> DetailView) where Value: OptionalProtocol, Value.Wrapped: LocalizedOptionValue
{
self.init(defaultValue: wrappedValue, name: name, description: description)
self.detailView = { [weak self] in
guard let self else { return nil }
let view = detailView(self.valueBinding)
return view
}
}
}