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

Remove _AnyApp, add Renderer.AppType associated type #225

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 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
39 changes: 17 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,18 @@ struct Counter: View {
}
}
}
```

You can then render your view in any DOM node captured with
[JavaScriptKit](https://github.com/kateinoigakukun/JavaScriptKit/), just
pass it as an argument to the `DOMRenderer` initializer together with your view:

```swift
import JavaScriptKit
import TokamakDOM

let document = JSObjectRef.global.document.object!

let divElement = document.createElement!("div").object!
let renderer = DOMRenderer(Counter(count: 5, limit: 15), divElement)
struct CounterApp: App {
var body: some Scene {
WindowGroup("Counter Demo") {
Counter(count: 5, limit: 15)
}
}
}

let body = document.body.object!
_ = body.appendChild!(divElement)
// @main attribute is not supported in SwiftPM apps.
// See https://bugs.swift.org/browse/SR-12683 for more details.
CounterApp.main()
```

### Arbitrary HTML
Expand Down Expand Up @@ -108,9 +103,9 @@ app.
## Requirements for app developers

- macOS 10.15 and Xcode 11.4/11.5/11.6 for macOS. Xcode betas are currently not supported. You can have
those installed, but please make sure you use
[`xcode-select`](https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-HOW_DO_I_SELECT_THE_DEFAULT_VERSION_OF_XCODE_TO_USE_FOR_MY_COMMAND_LINE_TOOLS_)
to point it to a release version of Xcode.
those installed, but please make sure you use
[`xcode-select`](https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-HOW_DO_I_SELECT_THE_DEFAULT_VERSION_OF_XCODE_TO_USE_FOR_MY_COMMAND_LINE_TOOLS_)
to point it to a release version of Xcode.
- [Swift 5.2 or later](https://swift.org/download/) for Linux.

## Requirements for app users
Expand All @@ -128,7 +123,7 @@ Not all of these were tested though, compatibility reports are very welcome!

Tokamak relies on [`carton`](https://carton.dev) as a primary build tool. As a part of these steps
you'll install `carton` via [Homebrew](https://brew.sh/) on macOS (unfortunately you'll have to build
it manually on Linux). Assuming you already have Homebrew installed, you can create a new Tokamak
it manually on Linux). Assuming you already have Homebrew installed, you can create a new Tokamak
app by following these steps:

1. Install `carton`:
Expand Down Expand Up @@ -156,15 +151,15 @@ carton init --template tokamak
```

4. Build the project and start the development server, `carton dev` can be kept running
during development:
during development:

```
carton dev
```

5. Open [http://127.0.0.1:8080/](http://127.0.0.1:8080/) in your browser to see the app
running. You can edit the app source code in your favorite editor and save it, `carton`
will immediately rebuild the app and reload all browser tabs that have the app open.
running. You can edit the app source code in your favorite editor and save it, `carton`
will immediately rebuild the app and reload all browser tabs that have the app open.

You can also clone this repository and run `carton dev` in its root directory. This
will build the demo app that shows almost all of the currently implemented APIs.
Expand Down
4 changes: 0 additions & 4 deletions Sources/TokamakCore/App/Scenes/Scene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ protocol TitledScene {
var title: Text? { get }
}

protocol ViewContainingScene {
var anyContent: AnyView { get }
}

protocol ParentScene {
var children: [_AnyScene] { get }
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/TokamakCore/App/Scenes/WindowGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// Created by Carson Katri on 7/16/20.
//

public struct WindowGroup<Content>: Scene, TitledScene, ViewContainingScene where Content: View {
public struct WindowGroup<Content>: Scene, TitledScene where Content: View {
public let id: String
public let title: Text?
public let content: Content
Expand Down
54 changes: 0 additions & 54 deletions Sources/TokamakCore/App/_AnyApp.swift

This file was deleted.

55 changes: 52 additions & 3 deletions Sources/TokamakCore/App/_AnyScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,62 @@
// Created by Carson Katri on 7/19/20.
//

import Runtime

public struct _AnyScene: Scene {
let scene: Any
/** The result type of `bodyClosure` allowing to disambiguate between scenes that
produce other scenes or scenes that only produce containing views.
*/
enum BodyResult {
case scene(_AnyScene)
case view(AnyView)
}

/** The name of the unapplied generic type of the underlying `view`. `Button<Text>` and
`Button<Image>` types are different, but when reconciling the tree of mounted views
they are treated the same, thus the `Button` part of the type (the type constructor)
is stored in this property.
*/
let typeConstructorName: String

/// The actual `Scene` value wrapped within this `_AnyScene`.
var scene: Any

/// The type of the underlying `scene`
let type: Any.Type

/** Type-erased `body` of the underlying `scene`. Needs to take a fresh version of `scene` as an
argument, otherwise it captures an old value of the `body` property.
*/
let bodyClosure: (Any) -> BodyResult

/** The type of the `body` of the underlying `scene`. Used to cast the result of the applied
`bodyClosure` property.
*/
let bodyType: Any.Type

init<S: Scene>(_ scene: S) {
self.scene = scene
type = S.self
if let anyScene = scene as? _AnyScene {
self = anyScene
} else {
self.scene = scene
type = S.self
bodyType = S.Body.self
if scene is SceneDeferredToRenderer {
// swiftlint:disable:next force_cast
bodyClosure = { .view(($0 as! SceneDeferredToRenderer).deferredBody) }
} else {
// swiftlint:disable:next force_cast
bodyClosure = { .scene(_AnyScene(($0 as! S).body)) }
}
// FIXME: no idea if using `mangledName` is reliable, but seems to be the only way to get
// a name of a type constructor in runtime. Should definitely check if these are different
// across modules, otherwise can cause problems with scenes with same names in different
// modules.

// swiftlint:disable:next force_try
typeConstructorName = try! typeInfo(of: type).mangledName
}
}

public var body: Never {
Expand Down
16 changes: 10 additions & 6 deletions Sources/TokamakCore/DynamicProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ extension TypeInfo {
/// Extract all `DynamicProperty` from a type, recursively.
/// This is necessary as a `DynamicProperty` can be nested.
/// `EnvironmentValues` can also be injected at this point.
func dynamicProperties(_ environment: EnvironmentValues,
source: inout Any,
shouldUpdate: Bool) -> [PropertyInfo] {
func dynamicProperties<T>(
_ environment: EnvironmentValues,
source: inout T,
shouldUpdate: Bool
) -> [PropertyInfo] {
var dynamicProps = [PropertyInfo]()
for prop in properties where prop.type is DynamicProperty.Type {
dynamicProps.append(prop)
Expand All @@ -40,9 +42,11 @@ extension TypeInfo {
propInfo.injectEnvironment(from: environment, into: &source)
var extracted = try! prop.get(from: source)
dynamicProps.append(
contentsOf: propInfo.dynamicProperties(environment,
source: &extracted,
shouldUpdate: shouldUpdate)
contentsOf: propInfo.dynamicProperties(
environment,
source: &extracted,
shouldUpdate: shouldUpdate
)
)
// swiftlint:disable:next force_cast
var extractedDynamicProp = extracted as! DynamicProperty
Expand Down
45 changes: 18 additions & 27 deletions Sources/TokamakCore/MountedViews/MountedApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,49 +34,40 @@ final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
mountedChildren.forEach { $0.unmount(with: reconciler) }
}

private func mountChild<S: Scene>(_ childBody: S) -> MountedElement<R> {
let mountedScene: MountedScene<R> = childBody.makeMountedView(parentTarget,
environmentValues)
private func mountChild<S: Scene>(_ scene: S) -> MountedElement<R> {
let mountedScene: MountedScene<R> = scene.makeMountedScene(parentTarget, environmentValues)
if let title = mountedScene.title {
// swiftlint:disable force_cast
(app.type as! _TitledApp.Type)._setTitle(title)
R.AppType._setTitle(title)
}
return mountedScene.body
return mountedScene
}

override func update(with reconciler: StackReconciler<R>) {
let element = reconciler.render(mountedApp: self)
reconciler.reconcile(
self,
with: element,
getElementType: { ($0 as? _AnyScene)?.type ?? type(of: $0) },
updateChild: { $0.scene = _AnyScene(element) },
getElementType: { type(of: $0) },
MaxDesiatov marked this conversation as resolved.
Show resolved Hide resolved
updateChild: {
$0.environmentValues = environmentValues
$0.scene = _AnyScene(element)
},
mountChild: { mountChild($0) }
)
}
}

extension App {
func makeMountedApp<R>(_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues)
-> MountedApp<R> where R: Renderer {
// Find Environment changes
var injectableApp = self
let any = (injectableApp as? _AnyApp) ?? _AnyApp(injectableApp)
// swiftlint:disable force_try
func makeMountedApp<R>(
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues
) -> MountedApp<R> where R: Renderer, R.AppType == Self {
// swiftlint:disable:next force_try
let info = try! typeInfo(of: Self.self)

let appInfo = try! typeInfo(of: any.type)
var extractedApp = any.app
var modified = self
info.injectEnvironment(from: environmentValues, into: &modified)

appInfo.injectEnvironment(from: environmentValues, into: &extractedApp)

// Set the extractedApp back on the AnyApp after modification
let anyAppInfo = try! typeInfo(of: _AnyApp.self)
try! anyAppInfo.property(named: "app").set(value: extractedApp, on: &injectableApp)
// swiftlint:enable force_try

// Make MountedView
let anyApp = injectableApp as? _AnyApp ?? _AnyApp(injectableApp)
return MountedApp(anyApp, parentTarget, environmentValues)
return MountedApp(modified, parentTarget, environmentValues)
}
}
13 changes: 4 additions & 9 deletions Sources/TokamakCore/MountedViews/MountedCompositeElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,23 @@ class MountedCompositeElement<R: Renderer>: MountedElement<R>, Hashable {
hasher.combine(ObjectIdentifier(self))
}

var mountedChildren = [MountedElement<R>]()
let parentTarget: R.TargetType

var state = [Any]()
var subscriptions = [AnyCancellable]()
var environmentValues: EnvironmentValues

init(_ app: _AnyApp, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) {
init(_ app: R.AppType, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) {
self.parentTarget = parentTarget
self.environmentValues = environmentValues
super.init(app)
super.init(app, environmentValues)
}

init(_ scene: _AnyScene, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) {
self.parentTarget = parentTarget
self.environmentValues = environmentValues
super.init(scene)
super.init(scene, environmentValues)
}

init(_ view: AnyView, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) {
self.parentTarget = parentTarget
self.environmentValues = environmentValues
super.init(view)
super.init(view, environmentValues)
}
}
7 changes: 5 additions & 2 deletions Sources/TokamakCore/MountedViews/MountedCompositeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
reconciler.reconcile(
self,
with: element,
getElementType: { ($0 as? AnyView)?.type ?? type(of: $0) },
updateChild: { $0.view = AnyView(element) },
getElementType: { $0.type },
updateChild: {
$0.environmentValues = environmentValues
$0.view = AnyView(element)
},
mountChild: { $0.makeMountedView(parentTarget, environmentValues) }
)
}
Expand Down
Loading