Created
March 5, 2024 23:38
-
-
Save codelynx/bebf5fd473a933fb98d1f9f741573dc4 to your computer and use it in GitHub Desktop.
Inspectables helps you implementing inspector view and its model in swift
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
// | |
// Inspectable.swift | |
// Inspectables | |
// | |
// Created by Kaz Yoshikawa on 3/5/24. | |
// | |
// | |
// Overview: | |
// Inspectables a set of code help you implementing inspectable property values and its inspector value editing visual components, by using | |
// @InspectableValue, or @InspectableEnum property wrappers, then you may forcus writing property value editing components. | |
// | |
// Discussion: | |
// This code helps implementing inspecter like component. For example model can be provided as follows. | |
// | |
// class SomeModel { | |
// @InspectableValue(name: "city") var city: String = "" | |
// @InspectableValue(name: "count") var count: Int = 0 | |
// @InspectableValue(name: "color") var color: UIColor = .black | |
// } | |
// | |
// Then you may implement table view cell like this way. Be aware that you may still have to provide its own value editing user interface. | |
// But basically InspectablePropertyValue represents binding mechanism between model value and user interface. You may set and get value | |
// from InspectablePropertyValue<T>. | |
// | |
// class StringViewCell { | |
// var inspectableValue: InspectablePropertyValue<String>! | |
// init(inspectableValue: InspectableValueType) { | |
// guard let inspectableValue = inspectableValue as? InspectablePropertyValue<String> else { fatalError() } | |
// self.inspectableValue = inspectableValue | |
// } | |
// func setValue(value: String) { | |
// self.inspectableValue.value = value | |
// } | |
// } | |
// | |
// Then whewn you need to build a list of properties for table view like visyal component, then you can write this way, using '$' signatured | |
// projectedValue to bind between value in model and visual representation of inspector view. | |
// | |
// let stringCell = StringViewCell(inspectableValue: self.viewModel.$city) | |
// let integerCell = IntegerViewCell(inspectableValue: self.viewModel.$count) | |
// let colorCell = ColorViewCell(inspectableValue: self.viewModel.$color) | |
// let enumCell = EnumViewCell(inspectableValue: self.viewModel.$direction) | |
// | |
// When you need to implement enum values for inspector views, use @InspectableEnum, instead @InspectableValue. Then you do not have to provide inspector | |
// view for each enum declarations. You may still have to provide Popup menu or other visual represenation to show and pick enum value from inspector view, | |
// but as long as emum is CaseIterable, you will not have to provide inspector view for each enum types. | |
// | |
// @InspectableEnum(name: "direction") var direction: Direction = .north | |
// | |
// Then inspector view pseudo code may look like this, but InspectableEnumValueType can be givin by late initializer. | |
// | |
// class EnumViewCell { | |
// var inspectableValue: InspectableEnumValueType! | |
// init(inspectableValue: InspectableEnumValueType) { | |
// self.inspectableValue = inspectableValue | |
// } | |
// func setValue(named name: String) { | |
// inspectableValue.valueName = name | |
// } | |
// } | |
// | |
// Be aware, value is set by name which is represented by String(describing:). And cannot be directory set by value. If you still like to so. then | |
// You may have to write this way. | |
// | |
// inspectableValue.valueName = String(describing: Direction.south) | |
// | |
// Or, all name of values can be made from InspectableEnumValueType.names, and your inspector view may let user picked | |
// one of the value in string and set it with setValue(named:) method. | |
// | |
// let names = inspectableValue.names | |
// let name: String = names.randomElement()! // pretend picked one | |
// self.inspectableValue.setValue(named: name) | |
// | |
// Note that when you implemnting Inspector view as table view like component, you may have to write swich | |
#if os(macOS) | |
import AppKit | |
typealias XColor = NSColor | |
#elseif os(iOS) | |
import UIKit | |
typealias XColor = UIColor | |
#endif | |
protocol InspectableValueType { | |
} | |
@propertyWrapper | |
class InspectableValue<T> { | |
var name: String | |
var value: T | |
init(wrappedValue: T, name: String) { | |
self.name = name | |
self.value = wrappedValue | |
} | |
var wrappedValue: T { | |
get { return self.value } | |
set { self.value = newValue } | |
} | |
var projectedValue: InspectableValueType { | |
return InspectablePropertyValue<T>(getter: { self.value }, setter: { self.value = $0 }) | |
} | |
} | |
@propertyWrapper | |
class InspectableEnum<T: CaseIterable & Equatable> { | |
var name: String | |
var value: T | |
init(wrappedValue: T, name: String) { | |
self.name = name | |
self.value = wrappedValue | |
} | |
var wrappedValue: T { | |
get { return self.value } | |
set { self.value = newValue } | |
} | |
var projectedValue: InspectableEnumValueType { | |
return InspectableEnumValue<T>(getter: { self.value }, setter: { self.value = $0 }) | |
} | |
} | |
protocol InspectableEnumValueType: InspectableValueType { | |
var names: [String] { get } | |
var valueNamed: String { get set } | |
func setValue(named name: String) | |
} | |
class InspectablePropertyValue<T>: InspectableValueType { | |
let getter: (()->T) | |
let setter: ((T)->()) | |
init(getter: @escaping (()->T), setter: @escaping ((T)->())) { | |
self.getter = getter | |
self.setter = setter | |
} | |
var value: T { | |
get { self.getter() } | |
set { self.setter(newValue) } | |
} | |
} | |
class InspectableEnumValue<T: CaseIterable & Equatable>: InspectablePropertyValue<T>, InspectableEnumValueType { | |
var names: [String] { T.allCases.map { String(describing: $0) } } | |
var valueNamed: String { | |
get { | |
let value: T = self.getter() | |
return String(describing: value) | |
} | |
set { | |
let value = T.allCases.filter { newValue == String(describing: $0) }.first! | |
self.setter(value) | |
} | |
} | |
func setValue(named name: String) { | |
self.valueNamed = name | |
} | |
} | |
/* | |
enum Direction: CaseIterable { | |
case north | |
case east | |
case south | |
case west | |
} | |
class StringViewCell { | |
var inspectableValue: InspectablePropertyValue<String>! | |
init(inspectableValue: InspectableValueType) { | |
guard let inspectableValue = inspectableValue as? InspectablePropertyValue<String> else { fatalError() } | |
self.inspectableValue = inspectableValue | |
} | |
func setValue(value: String) { | |
self.inspectableValue.value = value | |
} | |
} | |
class IntegerViewCell { | |
var inspectableValue: InspectablePropertyValue<Int>! | |
init(inspectableValue: InspectableValueType) { | |
guard let inspectableValue = inspectableValue as? InspectablePropertyValue<Int> else { fatalError() } | |
self.inspectableValue = inspectableValue | |
} | |
func setValue(value: Int) { | |
self.inspectableValue.value = value | |
} | |
} | |
class ColorViewCell { | |
var inspectableValue: InspectablePropertyValue<XColor>! | |
init(inspectableValue: InspectableValueType) { | |
guard let inspectableValue = inspectableValue as? InspectablePropertyValue<XColor> else { fatalError() } | |
self.inspectableValue = inspectableValue | |
} | |
func setValue(value: XColor) { | |
self.inspectableValue.value = value | |
} | |
} | |
class EnumViewCell { | |
var inspectableValue: InspectableEnumValueType! | |
init(inspectableValue: InspectableEnumValueType) { | |
self.inspectableValue = inspectableValue | |
} | |
func setValue(named name: String) { | |
self.inspectableValue.valueNamed = name | |
} | |
} | |
class SomeModel { | |
@InspectableValue(name: "city") var city: String = "" | |
@InspectableValue(name: "count") var count: Int = 0 | |
@InspectableValue(name: "color") var color: XColor = .black | |
@InspectableEnum(name: "direction") var direction: Direction = .north | |
init(city: String, count: Int, color: XColor, direction: Direction) { | |
self.city = city | |
self.count = count | |
self.color = color | |
self.direction = direction | |
} | |
} | |
class Tester { | |
var viewModel = SomeModel(city: "Paris", count: 1, color: .black, direction: .north) | |
func test() { | |
let stringCell = StringViewCell(inspectableValue: self.viewModel.$city) | |
let integerCell = IntegerViewCell(inspectableValue: self.viewModel.$count) | |
let colorCell = ColorViewCell(inspectableValue: self.viewModel.$color) | |
let enumCell = EnumViewCell(inspectableValue: self.viewModel.$direction) | |
print("==before updating==") | |
print(viewModel.city) | |
print(viewModel.count) | |
print(viewModel.color) | |
print(viewModel.direction) | |
stringCell.setValue(value: "New York") | |
integerCell.setValue(value: 25) | |
colorCell.setValue(value: .red) | |
enumCell.setValue(named: String(describing: Direction.south)) | |
print("==after updating==") | |
print(viewModel.city) | |
print(viewModel.count) | |
print(viewModel.color) | |
print(viewModel.direction) | |
} | |
} | |
*/ | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment