Implement Environment (#135)

* Environment impl

* Fix line lengths

* Danger and review fixes

* rename _modifyEnvironment to modifyEnvironment
This commit is contained in:
Carson Katri 2020-07-01 08:28:09 -04:00 committed by GitHub
parent b1b5693b66
commit 1d71fe0c7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 126 additions and 17 deletions

View File

@ -12,15 +12,25 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
@propertyWrapper public struct Environment<Value> { protocol EnvironmentReader {
mutating func setContent(from values: EnvironmentValues)
}
@propertyWrapper public struct Environment<Value>: EnvironmentReader {
enum Content { enum Content {
case keyPath(KeyPath<EnvironmentValues, Value>) case keyPath(KeyPath<EnvironmentValues, Value>)
case value(Value) case value(Value)
} }
var content: Environment<Value>.Content var content: Environment<Value>.Content
let keyPath: KeyPath<EnvironmentValues, Value>
public init(_ keyPath: KeyPath<EnvironmentValues, Value>) { public init(_ keyPath: KeyPath<EnvironmentValues, Value>) {
content = .keyPath(keyPath) content = .keyPath(keyPath)
self.keyPath = keyPath
}
mutating func setContent(from values: EnvironmentValues) {
content = .value(values[keyPath: keyPath])
} }
public var wrappedValue: Value { public var wrappedValue: Value {

View File

@ -14,12 +14,14 @@
public protocol EnvironmentKey { public protocol EnvironmentKey {
associatedtype Value associatedtype Value
static var defaultValue: Self.Value { get } static var defaultValue: Value { get }
} }
public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier { protocol EnvironmentModifier {
public typealias Body = Never func modifyEnvironment(_ values: inout EnvironmentValues)
}
public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, EnvironmentModifier {
public let keyPath: WritableKeyPath<EnvironmentValues, Value> public let keyPath: WritableKeyPath<EnvironmentValues, Value>
public let value: Value public let value: Value
@ -27,6 +29,14 @@ public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier {
self.keyPath = keyPath self.keyPath = keyPath
self.value = value self.value = value
} }
public func body(content: Content) -> some View {
content
}
func modifyEnvironment(_ values: inout EnvironmentValues) {
values[keyPath: keyPath] = value
}
} }
extension View { extension View {

View File

@ -31,9 +31,12 @@ final class MountedCompositeView<R: Renderer>: MountedView<R>, Hashable {
private let parentTarget: R.TargetType private let parentTarget: R.TargetType
var state = [Any]() var state = [Any]()
var environmentValues: EnvironmentValues
init(_ view: AnyView, _ parentTarget: R.TargetType) { init(_ view: AnyView, _ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues) {
self.parentTarget = parentTarget self.parentTarget = parentTarget
self.environmentValues = environmentValues
super.init(view) super.init(view)
} }
@ -41,7 +44,8 @@ final class MountedCompositeView<R: Renderer>: MountedView<R>, Hashable {
override func mount(with reconciler: StackReconciler<R>) { override func mount(with reconciler: StackReconciler<R>) {
let childBody = reconciler.render(compositeView: self) let childBody = reconciler.render(compositeView: self)
let child: MountedView<R> = childBody.makeMountedView(parentTarget) let child: MountedView<R> = childBody.makeMountedView(parentTarget,
environmentValues)
mountedChildren = [child] mountedChildren = [child]
child.mount(with: reconciler) child.mount(with: reconciler)
} }
@ -58,7 +62,8 @@ final class MountedCompositeView<R: Renderer>: MountedView<R>, Hashable {
switch (mountedChildren.last, reconciler.render(compositeView: self)) { switch (mountedChildren.last, reconciler.render(compositeView: self)) {
// no mounted children, but children available now // no mounted children, but children available now
case let (nil, childBody): case let (nil, childBody):
let child: MountedView<R> = childBody.makeMountedView(parentTarget) let child: MountedView<R> = childBody.makeMountedView(parentTarget,
environmentValues)
mountedChildren = [child] mountedChildren = [child]
child.mount(with: reconciler) child.mount(with: reconciler)
@ -81,7 +86,8 @@ final class MountedCompositeView<R: Renderer>: MountedView<R>, Hashable {
// wrapper, then mount a new one with the new `childBody` // wrapper, then mount a new one with the new `childBody`
wrapper.unmount(with: reconciler) wrapper.unmount(with: reconciler)
let child: MountedView<R> = childBody.makeMountedView(parentTarget) let child: MountedView<R> = childBody.makeMountedView(parentTarget,
environmentValues)
mountedChildren = [child] mountedChildren = [child]
child.mount(with: reconciler) child.mount(with: reconciler)
} }

View File

@ -31,9 +31,13 @@ public final class MountedHostView<R: Renderer>: MountedView<R> {
/// Target of this host view supplied by a renderer after mounting has completed. /// Target of this host view supplied by a renderer after mounting has completed.
private var target: R.TargetType? private var target: R.TargetType?
private let environmentValues: EnvironmentValues
init(_ view: AnyView, init(_ view: AnyView,
_ parentTarget: R.TargetType) { _ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues) {
self.parentTarget = parentTarget self.parentTarget = parentTarget
self.environmentValues = environmentValues
super.init(view) super.init(view)
} }
@ -48,7 +52,9 @@ public final class MountedHostView<R: Renderer>: MountedView<R> {
guard !view.children.isEmpty else { return } guard !view.children.isEmpty else { return }
mountedChildren = view.children.map { $0.makeMountedView(target) } mountedChildren = view.children.map {
$0.makeMountedView(target, environmentValues)
}
mountedChildren.forEach { $0.mount(with: reconciler) } mountedChildren.forEach { $0.mount(with: reconciler) }
} }
@ -81,7 +87,9 @@ public final class MountedHostView<R: Renderer>: MountedView<R> {
// if no existing children then mount all new children // if no existing children then mount all new children
case (true, false): case (true, false):
mountedChildren = childrenViews.map { $0.makeMountedView(target) } mountedChildren = childrenViews.map {
$0.makeMountedView(target, environmentValues)
}
mountedChildren.forEach { $0.mount(with: reconciler) } mountedChildren.forEach { $0.mount(with: reconciler) }
// if both arrays have items then reconcile by types and keys // if both arrays have items then reconcile by types and keys
@ -99,7 +107,7 @@ public final class MountedHostView<R: Renderer>: MountedView<R> {
newChild = child newChild = child
} else { } else {
child.unmount(with: reconciler) child.unmount(with: reconciler)
newChild = firstChild.makeMountedView(target) newChild = firstChild.makeMountedView(target, environmentValues)
newChild.mount(with: reconciler) newChild.mount(with: reconciler)
} }
newChildren.append(newChild) newChildren.append(newChild)
@ -118,7 +126,7 @@ public final class MountedHostView<R: Renderer>: MountedView<R> {
// mount remaining views // mount remaining views
for firstChild in childrenViews { for firstChild in childrenViews {
let newChild: MountedView<R> = let newChild: MountedView<R> =
firstChild.makeMountedView(target) firstChild.makeMountedView(target, environmentValues)
newChild.mount(with: reconciler) newChild.mount(with: reconciler)
newChildren.append(newChild) newChildren.append(newChild)
} }

View File

@ -36,15 +36,16 @@ public class MountedView<R: Renderer> {
} }
extension View { extension View {
func makeMountedView<R: Renderer>(_ parentTarget: R.TargetType) func makeMountedView<R: Renderer>(_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues)
-> MountedView<R> { -> MountedView<R> {
let anyView = self as? AnyView ?? AnyView(self) let anyView = self as? AnyView ?? AnyView(self)
if anyView.type == EmptyView.self { if anyView.type == EmptyView.self {
return MountedNull(anyView) return MountedNull(anyView)
} else if anyView.bodyType == Never.self && !(anyView.type is ViewDeferredToRenderer.Type) { } else if anyView.bodyType == Never.self && !(anyView.type is ViewDeferredToRenderer.Type) {
return MountedHostView(anyView, parentTarget) return MountedHostView(anyView, parentTarget, environmentValues)
} else { } else {
return MountedCompositeView(anyView, parentTarget) return MountedCompositeView(anyView, parentTarget, environmentValues)
} }
} }
} }

View File

@ -35,7 +35,7 @@ public final class StackReconciler<R: Renderer> {
self.scheduler = scheduler self.scheduler = scheduler
rootTarget = target rootTarget = target
rootView = view.makeMountedView(target) rootView = view.makeMountedView(target, EnvironmentValues())
rootView.mount(with: self) rootView.mount(with: self)
} }
@ -88,6 +88,28 @@ public final class StackReconciler<R: Renderer> {
} }
try! stateProperty.set(value: state, on: &compositeView.view.view) try! stateProperty.set(value: state, on: &compositeView.view.view)
} }
let viewInfo = try! typeInfo(of: compositeView.view.type)
if viewInfo
.genericTypes
.filter({ $0 is EnvironmentModifier.Type }).count > 0 {
// Apply Environment changes:
if let modifier = try? viewInfo
.property(named: "modifier")
.get(from: compositeView.view.view) as? EnvironmentModifier {
modifier.modifyEnvironment(&compositeView.environmentValues)
}
}
// Inject @Environment values
// In the future we can also inject @EnvironmentObject values
for prop in viewInfo.properties.filter({ $0.type is EnvironmentReader.Type }) {
// swiftlint:disable force_cast
var wrapper = try! prop.get(from: compositeView.view.view) as! EnvironmentReader
wrapper.setContent(from: compositeView.environmentValues)
try! prop.set(value: wrapper, on: &compositeView.view.view)
// swiftlint:enable force_cast
}
// swiftlint:enable force_try // swiftlint:enable force_try
let result = compositeView.view.bodyClosure(compositeView.view.view) let result = compositeView.view.bodyClosure(compositeView.view.view)

View File

@ -0,0 +1,20 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 6/30/20.
//
import TokamakCore
public typealias Environment = TokamakCore.Environment

View File

@ -0,0 +1,30 @@
// Copyright 2019-2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 6/30/20.
//
import TokamakDOM
struct EnvironmentDemo: View {
@Environment(\.font) var font: Font?
var body: some View {
if let font = font {
return Text("\(font)")
} else {
return Text("`font` environment not set.")
}
}
}

View File

@ -55,6 +55,8 @@ let renderer = DOMRenderer(
SpacerDemo() SpacerDemo()
Spacer() Spacer()
Text("Forced to bottom.") Text("Forced to bottom.")
EnvironmentDemo()
.font(.system(size: 21))
} }
}, },
div div