Created
February 6, 2025 23:38
-
-
Save rygrob/f79093bc6dcae6af57ff64e280cd83c8 to your computer and use it in GitHub Desktop.
Type-erased Parameter System
This file contains hidden or 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
// | |
// Parameters.swift | |
// --- | |
// | |
// Created by Ryan Robinson on 2/5/25. | |
// | |
import CoreGraphics | |
import Foundation | |
protocol ParameterValue { | |
associatedtype Value | |
var value: Value { get } | |
var defaultValue: Value { get } | |
mutating func setValue(_ value: Value) | |
mutating func restoreDefault() | |
} | |
class AnyParameter: ObservableObject, Identifiable { | |
// | |
let name: String | |
@Published private(set) var payload: Any | |
private var setValueAction: ((Any) -> Void)? = nil | |
private var restoreDefaultAction: (() -> Void)? = nil | |
// Dynamic member lookup | |
var key: String { | |
name.camelCase() | |
} | |
init<T: ParameterValue>(name: String, _ value: T) { | |
self.name = name | |
self.payload = value | |
self.setValueAction = { [weak self] value in | |
guard let self else { return } | |
if var typed = payload as? T, let x = value as? T.Value { | |
typed.setValue(x) | |
payload = typed // Force refresh. | |
} | |
} | |
self.restoreDefaultAction = { [weak self] in | |
guard let self else { return } | |
if var typed = payload as? T { | |
typed.restoreDefault() | |
payload = typed // Force refresh. | |
} | |
} | |
} | |
func setValue(_ value: Any) { | |
setValueAction?(value) | |
} | |
func restoreDefault() { | |
restoreDefaultAction?() | |
} | |
} | |
// MARK: - Helpers | |
extension AnyParameter { | |
static func float(name: String, value: CGFloat, range: ClosedRange<CGFloat>, defaultValue: CGFloat) -> AnyParameter { | |
return .init(name: name, FloatValue(value: value, range: range, defaultValue: defaultValue)) | |
} | |
func asFloat() -> CGFloat? { | |
return (payload as? FloatValue)?.value | |
} | |
static func string(name: String, value: String, defaultValue: String) -> AnyParameter { | |
return .init(name: name, StringValue(value: value, defaultValue: defaultValue)) | |
} | |
func asString() -> String? { | |
return (payload as? StringValue)?.value | |
} | |
static func color(name: String, value: CGColor, defaultValue: CGColor) -> AnyParameter { | |
return .init(name: name, ColorValue(value: value, defaultValue: defaultValue)) | |
} | |
func asColor() -> CGColor? { | |
return (payload as? ColorValue)?.value | |
} | |
static func indexed(name: String, value: Int, valueStrings: [String], defaultValue: Int) -> AnyParameter { | |
return .init(name: name, IndexedValue(value: value, valueStrings: valueStrings, defaultValue: defaultValue)) | |
} | |
func asIndex() -> Int? { | |
return (payload as? IndexedValue)?.value | |
} | |
} | |
// MARK: - Values | |
struct FloatValue: ParameterValue { | |
var value: CGFloat | |
let range: ClosedRange<CGFloat> | |
let defaultValue: CGFloat | |
init(value: CGFloat, range: ClosedRange<CGFloat>, defaultValue: CGFloat) { | |
self.value = value | |
self.range = range | |
self.defaultValue = defaultValue | |
} | |
mutating func setValue(_ value: CGFloat) { | |
self.value = value | |
} | |
mutating func restoreDefault() { | |
value = defaultValue | |
} | |
} | |
struct StringValue: ParameterValue { | |
var value: String | |
let defaultValue: String | |
init(value: String, defaultValue: String) { | |
self.value = value | |
self.defaultValue = defaultValue | |
} | |
mutating func setValue(_ value: String) { | |
self.value = value | |
} | |
mutating func restoreDefault() { | |
value = defaultValue | |
} | |
} | |
struct ColorValue: ParameterValue { | |
var value: CGColor | |
let defaultValue: CGColor | |
init(value: CGColor, defaultValue: CGColor) { | |
self.value = value | |
self.defaultValue = defaultValue | |
} | |
mutating func setValue(_ value: CGColor) { | |
self.value = value | |
} | |
mutating func restoreDefault() { | |
value = defaultValue | |
} | |
} | |
struct IndexedValue: ParameterValue { | |
var value: Int | |
let valueStrings: [String] | |
let defaultValue: Int | |
init(value: Int, valueStrings: [String], defaultValue: Int) { | |
self.value = value | |
self.valueStrings = valueStrings | |
self.defaultValue = defaultValue | |
} | |
mutating func setValue(_ value: Int) { | |
self.value = value | |
} | |
mutating func restoreDefault() { | |
value = defaultValue | |
} | |
} | |
// MARK: - Groups | |
@dynamicMemberLookup | |
class ParameterGroup: Identifiable { | |
// | |
let name: String | |
let parameters: [AnyParameter] | |
var key: String { | |
name.camelCase() | |
} | |
init(name: String, parameters: [AnyParameter]) { | |
self.name = name | |
self.parameters = parameters | |
} | |
// Dynamic member lookup | |
subscript(dynamicMember member: String) -> AnyParameter? { | |
return parameters.first { $0.key == member } | |
} | |
} | |
@dynamicMemberLookup | |
class AllParameters { | |
// | |
let groups: [ParameterGroup] | |
init(groups: [ParameterGroup]) { | |
self.groups = groups | |
} | |
// Dynamic member lookup | |
subscript(dynamicMember member: String) -> ParameterGroup? { | |
return groups.first { $0.key == member } | |
} | |
} |
This file contains hidden or 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
// | |
// ParametersView.swift | |
// --- | |
// | |
// Created by Ryan Robinson on 1/10/25. | |
// | |
import SwiftUI | |
struct ParametersView<E: Editor>: View { | |
// | |
let editor: E | |
var body: some View { | |
VStack { | |
ForEach(editor.params.groups) { group in | |
VStack { | |
ForEach(group.parameters) { parameter in | |
ParameterView(parameter) | |
.padding([.leading, .trailing]) | |
.padding([.top, .bottom], 4) | |
} | |
} | |
} | |
} | |
} | |
} | |
// MARK: - ParameterView | |
struct ParameterView: View { | |
@ObservedObject var parameter: AnyParameter | |
@Environment(\.self) var environment | |
init(_ parameter: AnyParameter) { | |
self.parameter = parameter | |
} | |
var body: some View { | |
ZStack { | |
switch parameter.payload { | |
case let payload as FloatValue: | |
let binding: Binding<CGFloat> = .init( | |
get: { parameter.asFloat() ?? 0 }, | |
set: { parameter.setValue($0) } | |
) | |
HStack(spacing: 0) { | |
let w: CGFloat = 42 | |
HStack { | |
Text(parameter.name) | |
Spacer() | |
} | |
.frame(width: 1.5 * w) | |
Slider(value: binding, in: payload.range) | |
} | |
case _ as StringValue: | |
let binding: Binding<String> = .init( | |
get: { parameter.asString() ?? "" }, | |
set: { parameter.setValue($0) } | |
) | |
HStack { | |
Text(parameter.name) | |
TextField(parameter.name, text: binding) | |
} | |
case _ as ColorValue: | |
let binding: Binding<Color> = .init( | |
get: { Color(cgColor: parameter.asColor() ?? .red) }, | |
set: { color in | |
let resolved = color.resolve(in: environment) | |
parameter.setValue(resolved.cgColor) | |
} | |
) | |
HStack { | |
ColorPicker(selection: binding) { | |
HStack { | |
Text(parameter.name) | |
Spacer() | |
} | |
} | |
} | |
case let payload as IndexedValue: | |
let binding: Binding<Int> = .init( | |
get: { parameter.asIndex() ?? 0 }, | |
set: { parameter.setValue($0) } | |
) | |
Picker(parameter.name, selection: binding) { | |
ForEach(Array(payload.valueStrings.indices), id: \.self) { index in | |
Text(payload.valueStrings[index]) | |
.tag(index) | |
} | |
} | |
default: | |
EmptyView() | |
} | |
} | |
.contentShape(.rect) | |
.contextMenu { | |
Button { | |
parameter.restoreDefault() | |
} label: { | |
Text("Reset to Default") | |
} | |
} | |
} | |
} | |
#Preview { | |
VStack { | |
ParameterView(.string(name: "Text", value: "Hello", defaultValue: "Hello")) | |
ParameterView(.color(name: "Color", value: .red, defaultValue: .red)) | |
ParameterView(.float(name: "Amount", value: 0, range: 0...1, defaultValue: 0)) | |
ParameterView(.indexed(name: "Quality", value: 0, valueStrings: ["Low", "Med.", "High"], defaultValue: 0)) | |
} | |
.padding() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment