Created
January 29, 2022 22:49
-
-
Save karwa/5ca1e2287ed65fb0b166513c883d8c0d to your computer and use it in GitHub Desktop.
POC form generation using private reflection APIs
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 | |
struct ContentView: View { | |
@State var immutableForm = false | |
@State var data = Person( | |
name: "Johnny Appleseed", address: "The spaceship", dateOfBirth: Date(), | |
aSwitch: true, anImmutableString: "Hello, world!", anImmutableSwitch: false | |
) | |
var body: some View { | |
VStack { | |
Button("Toggle mutable") { immutableForm.toggle() } | |
if immutableForm { | |
data.form | |
} else { | |
$data.form | |
} | |
}.padding(14) | |
} | |
} | |
struct Person: AutoForm { | |
var name: String | |
var address: String | |
var dateOfBirth: Date | |
var aSwitch: Bool | |
let anImmutableString: String | |
let anImmutableSwitch: Bool | |
} | |
/// Creates a very simple form using reflection. | |
/// | |
protocol AutoForm { | |
associatedtype Form: View | |
var form: Form { get } | |
} | |
// Default implementation for plain values. | |
extension AutoForm { | |
var form: some View { | |
// _canBeClass: | |
// https://github.com/apple/swift/blob/29e0a31941e609bb7d19ff26dba177cdb25c9d3b/stdlib/public/core/Builtin.swift#L48 | |
_AutoForm(object: .constant(self), immutable: _canBeClass(Self.self) != 0) | |
} | |
} | |
// If we have a binding to an AutoForm type, we can provide an editable Form. | |
extension Binding: AutoForm where Value: AutoForm { | |
var form: some View { | |
_AutoForm(object: self, immutable: false) | |
} | |
} | |
/// A super-basic form that I knocked up in under an hour, almost all of which | |
/// was spent struggling with existentials. | |
/// | |
struct _AutoForm<Object>: View { | |
@Binding var object: Object | |
let immutable: Bool | |
let reflectedData: [(name: String, path: PartialKeyPath<Object>)] | |
init(object: Binding<Object>, immutable: Bool) { | |
self._object = object | |
self.immutable = immutable | |
var data = [(name: String, path: PartialKeyPath<Object>)]() | |
let _ = _forEachFieldWithKeyPath(of: Object.self) { cStringName, keyPath in | |
data.append((name: String(cString: cStringName), path: keyPath)) | |
return true | |
} | |
self.reflectedData = data | |
} | |
var body: some View { | |
VStack { | |
ForEach(reflectedData, id: \.name) { (name, keypath) in | |
HStack(spacing: 10) { | |
Text(name) | |
.bold() | |
.frame(minWidth: 100, maxWidth: 200, alignment: .trailing) | |
if let valueType = type(of: keypath).valueType as? AutoFormElement.Type { | |
valueType.makeFormPresentation(root: $object, keypath: keypath, immutable: self.immutable) | |
.frame(alignment: .leading) | |
} else { | |
Text("?? No form presentation ??") | |
} | |
Spacer() | |
} | |
} | |
} | |
} | |
} | |
extension Binding { | |
func readWriteKeyPath<T>(_ kp: WritableKeyPath<Value, T>) -> Binding<T> { | |
Binding<T>( | |
get: { wrappedValue[keyPath: kp] }, | |
set: { wrappedValue[keyPath: kp] = $0 } | |
) | |
} | |
func readOnlyKeyPath<T>(_ kp: KeyPath<Value, T>) -> Binding<T> { | |
Binding<T>( | |
get: { wrappedValue[keyPath: kp] }, | |
set: { _ in } | |
) | |
} | |
} | |
/// A view with a custom AutoForm presentation. | |
/// | |
/// Ideally, this would be split in to mutable/immutable view functions, take a `Binding<Self>`, | |
/// and not return an `AnyView`. But we quickly run in to PAT problems and it becomes a nightmare. | |
/// Swift 5.6 should improve things. | |
/// | |
protocol AutoFormElement { | |
static func makeFormPresentation<T>( | |
root: Binding<T>, keypath: PartialKeyPath<T>, immutable: Bool | |
) -> AnyView | |
} | |
extension String: AutoFormElement { | |
static func makeFormPresentation<T>(root: Binding<T>, keypath: PartialKeyPath<T>, immutable: Bool) -> AnyView { | |
if let kp = keypath as? WritableKeyPath<T, Self>, !immutable { | |
return AnyView(TextField("", text: root.readWriteKeyPath(kp))) | |
} else { | |
let kp = keypath as! KeyPath<T, Self> | |
return AnyView(Text(root.readOnlyKeyPath(kp).wrappedValue)) | |
} | |
} | |
} | |
extension Bool: AutoFormElement { | |
static func makeFormPresentation<T>(root: Binding<T>, keypath: PartialKeyPath<T>, immutable: Bool) -> AnyView { | |
if let kp = keypath as? WritableKeyPath<T, Self>, !immutable { | |
return AnyView(Toggle("", isOn: root.readWriteKeyPath(kp))) | |
} else { | |
let kp = keypath as! KeyPath<T, Self> | |
return AnyView(Toggle("", isOn: root.readOnlyKeyPath(kp)).disabled(true)) | |
} | |
} | |
} | |
extension Date: AutoFormElement { | |
static func makeFormPresentation<T>(root: Binding<T>, keypath: PartialKeyPath<T>, immutable: Bool) -> AnyView { | |
if let kp = keypath as? WritableKeyPath<T, Self>, !immutable { | |
return AnyView(DatePicker("", selection: root.readWriteKeyPath(kp))) | |
} else { | |
let kp = keypath as! KeyPath<T, Self> | |
return AnyView(Text(root.readOnlyKeyPath(kp).wrappedValue.description)) | |
} | |
} | |
} | |
// Hacks. And I mean _hacks_. Like seriously. | |
@_silgen_name("$ss24_forEachFieldWithKeyPath2of7options4bodySbxm_s01_bC7OptionsVSbSPys4Int8VG_s07PartialeF0CyxGtXEtlF") | |
@discardableResult | |
func _forEachFieldWithKeyPath<Root>( | |
of type: Root.Type, | |
options: _EachFieldOptions = [], | |
body: (UnsafePointer<CChar>, PartialKeyPath<Root>) -> Bool | |
) -> Bool | |
// This is supposed to be a struct, but passing a struct crashes because the real stdlib type is resilient. | |
// Passing it as a class doesn't crash. 🤷♂️ | |
// | |
// I did say hacks, did I not? Pretty sure I did. | |
public class _EachFieldOptions: OptionSet { | |
public var rawValue: UInt32 | |
required public init(rawValue: UInt32) { | |
self.rawValue = rawValue | |
} | |
/// Require the top-level type to be a class. | |
/// | |
/// If this is not set, the top-level type is required to be a struct or | |
/// tuple. | |
public static var classType = _EachFieldOptions(rawValue: 1 << 0) | |
/// Ignore fields that can't be introspected. | |
/// | |
/// If not set, the presence of things that can't be introspected causes | |
/// the function to immediately return `false`. | |
public static var ignoreUnknown = _EachFieldOptions(rawValue: 1 << 1) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Mutable (via Binding)
Immutable (plain value)