Skip to content

Instantly share code, notes, and snippets.

@karwa
Created January 29, 2022 22:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save karwa/5ca1e2287ed65fb0b166513c883d8c0d to your computer and use it in GitHub Desktop.
Save karwa/5ca1e2287ed65fb0b166513c883d8c0d to your computer and use it in GitHub Desktop.
POC form generation using private reflection APIs
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)
}
@karwa
Copy link
Author

karwa commented Jan 29, 2022

Mutable (via Binding)
Screenshot 2022-01-29 at 23 47 40

Immutable (plain value)
Screenshot 2022-01-29 at 23 50 03

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