Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding dp prototype #68

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions DependencyPropertyPrototype/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
23 changes: 23 additions & 0 deletions DependencyPropertyPrototype/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "DependencyPropertyPrototype",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "DependencyPropertyPrototype",
targets: ["DependencyPropertyPrototype"]),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "DependencyPropertyPrototype"),
.testTarget(
name: "DependencyPropertyPrototypeTests",
dependencies: ["DependencyPropertyPrototype"]),
]
)
47 changes: 47 additions & 0 deletions DependencyPropertyPrototype/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Dependency Property Prototype

This project contains a prototype to show how we can model DependencyProperites after `Published` properties.

Furthermore, we can also specially generate them in a manner which enforces type safety when using Styles/Setters.

See the code in `Sources` dir for the API and explanation.

See the Tests for use cases

## Usage

### Change notifications

```
let myDO = MyDO()
myDO.myProperty = "hello"
myDO.$myProperty.sink {
XCTAssertEqual($0, "hello")
XCTAssertEqual($1, "world")
}

myDO.myProperty = "world"
```

### Style type enforcements

```
let style = Style(targetType: MyDO.self) {
Setter(.myProperty, "hi")
Setter(.myIntProperty, 2)
}

```

## Open Questions

The `Style` class has a type-erased base class `StyleBase` which is what the API would expose, however this type wouldn't be constructible.

In theory, someone could still apply an incorrect `Style` to a framework element. For example, something like this is allowed:

```
let obj = MyDO()
obj.Style = Style(targetType.MyOtherDO) { ... }
```

Is there a way to enforce this? Should `Style` have an API called `applyTo` which takes the object it wants to be applied to?
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// DOAbi is the
public class DOAbi {
func registerPropertyChanged(_ dp: DependencyPropertyHandle, _ handler: @escaping (Any?, Any?) -> Void) {
print("abi :registerPropertyChanged")
self.handler = handler
}

var handler: ((Any, Any) -> Void)?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class FrameworkElementABI: DOAbi {
private var styleProperty: StyleBase?
func setStyleProperty(_ value: StyleBase?) {
let oldValue = styleProperty
self.styleProperty = value
handler?(oldValue, value)
}
func getStyleProperty() -> StyleBase? {
return styleProperty
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class MyDOABI : FrameworkElementABI{
private var myProperty:String = ""
func setMyProperty(_ value: String) {
let oldValue = myProperty
self.myProperty = value
handler?(oldValue, value)
}
func getMyProperty() -> String {
return myProperty
}

private var myIntProperty:Int = 0
func setMyIntProperty(_ value: Int) {
let oldValue = myIntProperty
self.myIntProperty = value
handler?(oldValue, value)
}
func getMyIntProperty() -> Int {
return myIntProperty
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class MyDO2ABI : MyDOABI{
private var myBoolProperty:Bool = false
func setMyBoolProperty(_ value: Bool) {
let oldValue = myBoolProperty
self.myBoolProperty = value
handler?(oldValue, value)
}
func getMyBoolProperty() -> Bool {
return myBoolProperty
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class SetterAbi : DOAbi{
override init() {
self.property = DependencyPropertyHandle()
super.init()
}
private var property: DependencyPropertyHandle
func setProperty(_ value: DependencyPropertyHandle) {
let oldValue = property
self.property = value
handler?(oldValue, value)
}
func getProperty() -> DependencyPropertyHandle {
return property
}

private var value: Any?
func setValue(_ value: Any?) {
//let oldValue = self.value
self.value = value
//handler?(oldValue, value)
}
func getValue() -> Any? {
return value
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class StyleAbi : DOAbi{
override init() {
super.init()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// This prototype of DependencyObject is a "dummy" implementation that mimics how the swiftwinrt code gen
// works today. The public swift type wraps an internal type. The internal type is what talks to the actual
// ABI.

open class DependencyObject {

public init(fromABi: DOAbi) {
}

func registerPropertyChanged(_ dp: DependencyPropertyHandle, _ handler: (Any?, Any?) -> Void) {
print("registerPropertyChanged")
}

//@Styled var style: StyleBase?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// A "DependencyPropertyHandle" is the "true" DependencyProperty type that
// the WinUI system understands and uses
public class DependencyPropertyHandle {}

// DependencyProperties are the "public" properties for a given DependencyObject
// and are strongly typed to the actual type of the property and who owns them
// this allows us to use the swift compiler to enforce type safety
public struct DependencyProperties<Type: DependencyObject, Value> {
let handle: DependencyPropertyHandle
}

public protocol DependencyPropertyPublisher<Value> {
associatedtype Value
func sink(_ handler: @escaping (Value, Value) -> Void)
var property: DependencyPropertyHandle { get }
}

public protocol DependencyPropertyProxy<Value> {
associatedtype Value
var set: (Value) -> Void { get }
var get: () -> Value { get }
}

public protocol DependencyPropertyChangedProxy<Value>: DependencyPropertyPublisher, DependencyPropertyProxy{
}

// The DepdnencyProperty wrapper is given setters/getters since while it is possible to access the owning
// instance of the wrapper in the wrappedValue setter/getter, it is not possible to access the instance
// in the projectedValue. This means we can't properly subscribe to the property changed event on the
// object. It's for this reason that we have the DependencyPropertyProxy protocol instead of using KeyPaths as
// done here: https://www.swiftbysundell.com/articles/accessing-a-swift-property-wrappers-enclosing-instance/
@propertyWrapper
public struct DependencyProperty<Value> {
public var wrappedValue: Value {
get { _get() }
set { _set(newValue) }
}

private var _get: () -> Value
private var _set: (Value) -> Void

init<Proxy: DependencyPropertyChangedProxy<Value>>(_ proxy: Proxy) {
self._get = proxy.get
self._set = proxy.set
self.projectedValue = proxy
}

public let projectedValue: any DependencyPropertyPublisher<Value>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// implementation of the DependencyPropertyChangedProxy protocol. We require a separate implementation type
// because this type understands the ABI, whereas the generic DependencyPropertyChangedProxy just understands
// the value

struct DependencyPropertyChangedProxyImpl<Owner: DOAbi, Value>: DependencyPropertyChangedProxy {
let property: DependencyPropertyHandle
let owner: Owner

init(property: DependencyPropertyHandle, owner: Owner, set: @escaping (Owner, Value) -> Void, get: @escaping (Owner) -> Value) {
self.property = property
self.owner = owner
self.set = { value in
set(owner, value)
}
self.get = {
return get(owner)
}
}
let set: (Value) -> Void
let get: () -> Value

func sink(_ handler: @escaping (Value, Value) -> Void) {
owner.registerPropertyChanged(property) { (oldValue, newValue) in
handler(oldValue as! Value, newValue as! Value)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public class FrameworkElement : DependencyObject {

@Styled<Void> public var style: Style2<Self, Void>?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// A simple DependencyObject that has two properties. This would reflect the codegenerated to handle
// this type and properties associated with it

public class MyDO: FrameworkElement {

override init(fromABi: DOAbi) {
let abi = fromABi as! MyDOABI
self._abi = abi

// We have to initialize the _myProperty PropertyWrapper in the initializer since it needs a reference to the ABI.
// This can't be done in the
self._myProperty = .init(MyDOProperties.myPropertyPublisher(abi))
self._myIntProperty = .init(MyDOProperties.myIntPropertyPublisher(abi))
super.init(fromABi: abi)
}

public convenience init() {
let abi = MyDOABI()
self.init(fromABi: abi)
}

private let _abi: MyDOABI

@DependencyProperty var myProperty: String
@DependencyProperty var myIntProperty: Int
}

fileprivate extension MyDO {
struct MyDOProperties {
static let myPropertyProperty = DependencyPropertyHandle()
static let myPropertyPublisher = { (owner: MyDOABI) in DependencyPropertyChangedProxyImpl(
property: myPropertyProperty,
owner: owner,
set: { $0.setMyProperty($1) },
get: { $0.getMyProperty() }
)}

static let myIntPropertyProperty = DependencyPropertyHandle()
static let myIntPropertyPublisher = { (owner: MyDOABI) in DependencyPropertyChangedProxyImpl(
property: myPropertyProperty,
owner: owner,
set: { $0.setMyIntProperty($1) },
get: { $0.getMyIntProperty() }
)}
}
}

public extension DependencyProperties where Type: MyDO {
static var myProperty: DependencyProperties<MyDO, String> { .init(handle: MyDO.MyDOProperties.myPropertyProperty) }
static var myIntProperty: DependencyProperties<MyDO, Int> { .init(handle: MyDO.MyDOProperties.myIntPropertyProperty) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// A simple DependencyObject that has two properties. This would reflect the codegenerated to handle
// this type and properties associated with it

public class MyDO2: MyDO {

override init(fromABi: DOAbi) {
let abi = fromABi as! MyDO2ABI
self._abi = abi

// We have to initialize the _myProperty PropertyWrapper in the initializer since it needs a reference to the ABI.
// This can't be done in the
self._myBoolProperty = .init(MyDO2Properties.myBoolPropertyPublisher(abi))
super.init(fromABi: abi)
}

public convenience init() {
let abi = MyDO2ABI()
self.init(fromABi: abi)
}

private let _abi: MyDO2ABI

@DependencyProperty var myBoolProperty: Bool
}

fileprivate extension MyDO2 {
struct MyDO2Properties {
static let myBoolPropertyProperty = DependencyPropertyHandle()
static let myBoolPropertyPublisher = { (owner: MyDO2ABI) in DependencyPropertyChangedProxyImpl(
property: myBoolPropertyProperty,
owner: owner,
set: { $0.setMyBoolProperty($1) },
get: { $0.getMyBoolProperty() }
)}
}
}

public extension DependencyProperties where Type: MyDO2 {
static var myBoolProperty: DependencyProperties<MyDO2, Bool> { .init(handle: MyDO2.MyDO2Properties.myBoolPropertyProperty) }
}
Loading