Skip to content

Commit

Permalink
Refactor environment injection, add a test (#371)
Browse files Browse the repository at this point in the history
* Add a test for environment injection

We had some issues in this code area previously and I'm thinking of refactoring it in attempt to fix #367. Would be great to increase the test coverage here before further refactoring.

* Update copyright years in `MountedElement.swift`

* Update copyright years in the rest of the files
  • Loading branch information
MaxDesiatov committed Jan 25, 2021
1 parent e04b793 commit 192c43b
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 40 deletions.
54 changes: 27 additions & 27 deletions Sources/TokamakCore/MountedViews/MountedElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ private enum MountedElementKind {

public class MountedElement<R: Renderer> {
private var element: MountedElementKind
var type: Any.Type { element.type }

public internal(set) var app: _AnyApp {
get {
Expand Down Expand Up @@ -117,20 +118,16 @@ public class MountedElement<R: Renderer> {
updateEnvironment()
}

@discardableResult
func updateEnvironment() -> TypeInfo {
// swiftlint:disable:next force_try
let info = try! typeInfo(of: element.type)
func updateEnvironment() {
let type = element.type
switch element {
case .app:
environmentValues = info.injectEnvironment(from: environmentValues, into: &app.app)
environmentValues.inject(into: &app.app, type)
case .scene:
environmentValues = info.injectEnvironment(from: environmentValues, into: &scene.scene)
environmentValues.inject(into: &scene.scene, type)
case .view:
environmentValues = info.injectEnvironment(from: environmentValues, into: &view.view)
environmentValues.inject(into: &view.view, type)
}

return info
}

func mount(
Expand Down Expand Up @@ -163,59 +160,62 @@ public class MountedElement<R: Renderer> {
}
}

extension TypeInfo {
fileprivate func injectEnvironment(
from environmentValues: EnvironmentValues,
into element: inout Any
) -> EnvironmentValues {
var modifiedEnv = environmentValues
extension EnvironmentValues {
mutating func inject(into element: inout Any, _ type: Any.Type) {
// swiftlint:disable:next force_try
let info = try! typeInfo(of: type)

// swiftlint:disable force_try
// Extract the view from the AnyView for modification, apply Environment changes:
if genericTypes.contains(where: { $0 is EnvironmentModifier.Type }),
let modifier = try! property(named: "modifier").get(from: element) as? EnvironmentModifier
if info.genericTypes.contains(where: { $0 is EnvironmentModifier.Type }),
let modifier = try! info.property(named: "modifier")
.get(from: element) as? EnvironmentModifier
{
modifier.modifyEnvironment(&modifiedEnv)
modifier.modifyEnvironment(&self)
}

// Inject @Environment values
// swiftlint:disable force_cast
// `DynamicProperty`s can have `@Environment` properties contained in them,
// so we have to inject into them as well.
for dynamicProp in properties.filter({ $0.type is DynamicProperty.Type }) {
for dynamicProp in info.properties.filter({ $0.type is DynamicProperty.Type }) {
let propInfo = try! typeInfo(of: dynamicProp.type)
var propWrapper = try! dynamicProp.get(from: element) as! DynamicProperty
for prop in propInfo.properties.filter({ $0.type is EnvironmentReader.Type }) {
var wrapper = try! prop.get(from: propWrapper) as! EnvironmentReader
wrapper.setContent(from: modifiedEnv)
wrapper.setContent(from: self)
try! prop.set(value: wrapper, on: &propWrapper)
}
try! dynamicProp.set(value: propWrapper, on: &element)
}
for prop in properties.filter({ $0.type is EnvironmentReader.Type }) {
for prop in info.properties.filter({ $0.type is EnvironmentReader.Type }) {
var wrapper = try! prop.get(from: element) as! EnvironmentReader
wrapper.setContent(from: modifiedEnv)
wrapper.setContent(from: self)
try! prop.set(value: wrapper, on: &element)
}
// swiftlint:enable force_try
// swiftlint:enable force_cast

return modifiedEnv
}
}

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) -> [PropertyInfo] {
func dynamicProperties(
_ environment: inout EnvironmentValues,
source: inout Any
) -> [PropertyInfo] {
var dynamicProps = [PropertyInfo]()
for prop in properties where prop.type is DynamicProperty.Type {
dynamicProps.append(prop)
// swiftlint:disable force_try
let propInfo = try! typeInfo(of: prop.type)
_ = propInfo.injectEnvironment(from: environment, into: &source)
environment.inject(into: &source, prop.type)
var extracted = try! prop.get(from: source)
dynamicProps.append(
contentsOf: propInfo.dynamicProperties(
environment,
&environment,
source: &extracted
)
)
Expand Down
7 changes: 4 additions & 3 deletions Sources/TokamakCore/StackReconciler.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2018-2020 Tokamak contributors
// Copyright 2018-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -208,11 +208,12 @@ public final class StackReconciler<R: Renderer> {
body bodyKeypath: ReferenceWritableKeyPath<MountedCompositeElement<R>, Any>,
result: KeyPath<MountedCompositeElement<R>, (Any) -> T>
) -> T {
let info = compositeElement.updateEnvironment()
compositeElement.updateEnvironment()
let info = try! typeInfo(of: compositeElement.type)

var stateIdx = 0
let dynamicProps = info.dynamicProperties(
compositeElement.environmentValues,
&compositeElement.environmentValues,
source: &compositeElement[keyPath: bodyKeypath]
)

Expand Down
7 changes: 5 additions & 2 deletions Sources/TokamakDOM/DOMRenderer.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors
// Copyright 2020-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -24,8 +24,11 @@ extension EnvironmentValues {
/// Returns default settings for the DOM environment
static var defaultEnvironment: Self {
var environment = EnvironmentValues()

// `.toggleStyle` property is internal
environment[_ToggleStyleKey] = _AnyToggleStyle(DefaultToggleStyle())
environment[_ColorSchemeKey] = .init(matchMediaDarkScheme: matchMediaDarkScheme)

environment.colorScheme = .init(matchMediaDarkScheme: matchMediaDarkScheme)
environment._defaultAppStorage = LocalStorage.standard
_DefaultSceneStorageProvider.default = SessionStorage.standard

Expand Down
14 changes: 10 additions & 4 deletions Sources/TokamakGTK/Views/NavigationView.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors
// Copyright 2020-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -20,7 +20,9 @@ protocol GtkStackProtocol {}
// extension NavigationView: AnyWidget, ParentView, GtkStackProtocol {
// var expand: Bool { true }

// func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
// func new(
// _ application: UnsafeMutablePointer<GtkApplication>
// ) -> UnsafeMutablePointer<GtkWidget> {
// let box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)!
// let stack = gtk_stack_new()!
// let sidebar = gtk_stack_sidebar_new()!
Expand Down Expand Up @@ -77,7 +79,9 @@ extension NavigationLink: ViewDeferredToRenderer {
}

// extension NavigationLink: AnyWidget, ParentView {
// func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
// func new(
// _ application: UnsafeMutablePointer<GtkApplication>
// ) -> UnsafeMutablePointer<GtkWidget> {
// let btn = gtk_button_new()!
// bindAction(to: btn)
// return btn
Expand Down Expand Up @@ -105,7 +109,9 @@ extension NavigationLink: ViewDeferredToRenderer {
// }

// extension NavigationLink: AnyWidget, ParentView {
// func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> {
// func new(
// _ application: UnsafeMutablePointer<GtkApplication>
// ) -> UnsafeMutablePointer<GtkWidget> {
// print("Creating NavLink widget")
// let btn = gtk_button_new()!
// bindAction(to: btn)
Expand Down
46 changes: 46 additions & 0 deletions Tests/TokamakTests/EnvironmentTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2021 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.
import XCTest

@testable import TokamakCore

private struct TestView: View {
@Environment(\.colorScheme) var colorScheme

public var body: some View {
EmptyView()
}
}

final class EnvironmentTests: XCTestCase {
func testInjection() {
var test: Any = TestView()
var values = EnvironmentValues()
values.colorScheme = .light
values.inject(into: &test, TestView.self)
// swiftlint:disable:next force_cast
XCTAssertEqual((test as! TestView).colorScheme, .light)

values.colorScheme = .dark
values.inject(into: &test, TestView.self)
// swiftlint:disable:next force_cast
XCTAssertEqual((test as! TestView).colorScheme, .dark)

let modifier = TestView().colorScheme(.light)
var anyModifier: Any = modifier

values.inject(into: &anyModifier, type(of: modifier))
XCTAssertEqual(values.colorScheme, .light)
}
}
8 changes: 4 additions & 4 deletions Tests/TokamakTests/ReconcilerTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors
// Copyright 2020-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -20,12 +20,12 @@ import XCTest

@testable import TokamakCore

struct Counter: View {
private struct Counter: View {
@State var count: Int

let limit: Int

@ViewBuilder public var body: some View {
public var body: some View {
if count < limit {
VStack {
Button("Increment") { count += 1 }
Expand All @@ -37,7 +37,7 @@ struct Counter: View {
}
}

extension Text {
private extension Text {
var verbatim: String? {
guard case let .verbatim(text) = storage else { return nil }
return text
Expand Down

0 comments on commit 192c43b

Please sign in to comment.