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
// 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 {
case keyPath(KeyPath<EnvironmentValues, Value>)
case value(Value)
}
var content: Environment<Value>.Content
let keyPath: KeyPath<EnvironmentValues, Value>
public init(_ keyPath: KeyPath<EnvironmentValues, Value>) {
content = .keyPath(keyPath)
self.keyPath = keyPath
}
mutating func setContent(from values: EnvironmentValues) {
content = .value(values[keyPath: keyPath])
}
public var wrappedValue: Value {

View File

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

View File

@ -31,9 +31,12 @@ final class MountedCompositeView<R: Renderer>: MountedView<R>, Hashable {
private let parentTarget: R.TargetType
var state = [Any]()
var environmentValues: EnvironmentValues
init(_ view: AnyView, _ parentTarget: R.TargetType) {
init(_ view: AnyView, _ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues) {
self.parentTarget = parentTarget
self.environmentValues = environmentValues
super.init(view)
}
@ -41,7 +44,8 @@ final class MountedCompositeView<R: Renderer>: MountedView<R>, Hashable {
override func mount(with reconciler: StackReconciler<R>) {
let childBody = reconciler.render(compositeView: self)
let child: MountedView<R> = childBody.makeMountedView(parentTarget)
let child: MountedView<R> = childBody.makeMountedView(parentTarget,
environmentValues)
mountedChildren = [child]
child.mount(with: reconciler)
}
@ -58,7 +62,8 @@ final class MountedCompositeView<R: Renderer>: MountedView<R>, Hashable {
switch (mountedChildren.last, reconciler.render(compositeView: self)) {
// no mounted children, but children available now
case let (nil, childBody):
let child: MountedView<R> = childBody.makeMountedView(parentTarget)
let child: MountedView<R> = childBody.makeMountedView(parentTarget,
environmentValues)
mountedChildren = [child]
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.unmount(with: reconciler)
let child: MountedView<R> = childBody.makeMountedView(parentTarget)
let child: MountedView<R> = childBody.makeMountedView(parentTarget,
environmentValues)
mountedChildren = [child]
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.
private var target: R.TargetType?
private let environmentValues: EnvironmentValues
init(_ view: AnyView,
_ parentTarget: R.TargetType) {
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues) {
self.parentTarget = parentTarget
self.environmentValues = environmentValues
super.init(view)
}
@ -48,7 +52,9 @@ public final class MountedHostView<R: Renderer>: MountedView<R> {
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) }
}
@ -81,7 +87,9 @@ public final class MountedHostView<R: Renderer>: MountedView<R> {
// if no existing children then mount all new children
case (true, false):
mountedChildren = childrenViews.map { $0.makeMountedView(target) }
mountedChildren = childrenViews.map {
$0.makeMountedView(target, environmentValues)
}
mountedChildren.forEach { $0.mount(with: reconciler) }
// 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
} else {
child.unmount(with: reconciler)
newChild = firstChild.makeMountedView(target)
newChild = firstChild.makeMountedView(target, environmentValues)
newChild.mount(with: reconciler)
}
newChildren.append(newChild)
@ -118,7 +126,7 @@ public final class MountedHostView<R: Renderer>: MountedView<R> {
// mount remaining views
for firstChild in childrenViews {
let newChild: MountedView<R> =
firstChild.makeMountedView(target)
firstChild.makeMountedView(target, environmentValues)
newChild.mount(with: reconciler)
newChildren.append(newChild)
}

View File

@ -36,15 +36,16 @@ public class MountedView<R: Renderer> {
}
extension View {
func makeMountedView<R: Renderer>(_ parentTarget: R.TargetType)
func makeMountedView<R: Renderer>(_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues)
-> MountedView<R> {
let anyView = self as? AnyView ?? AnyView(self)
if anyView.type == EmptyView.self {
return MountedNull(anyView)
} else if anyView.bodyType == Never.self && !(anyView.type is ViewDeferredToRenderer.Type) {
return MountedHostView(anyView, parentTarget)
return MountedHostView(anyView, parentTarget, environmentValues)
} 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
rootTarget = target
rootView = view.makeMountedView(target)
rootView = view.makeMountedView(target, EnvironmentValues())
rootView.mount(with: self)
}
@ -88,6 +88,28 @@ public final class StackReconciler<R: Renderer> {
}
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
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()
Spacer()
Text("Forced to bottom.")
EnvironmentDemo()
.font(.system(size: 21))
}
},
div