Refactor environment injection, add a test (#371)

* 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
This commit is contained in:
Max Desiatov 2021-01-25 11:39:09 +00:00 committed by GitHub
parent e04b7934fb
commit 192c43b140
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 96 additions and 40 deletions

View File

@ -34,6 +34,7 @@ private enum MountedElementKind {
public class MountedElement<R: Renderer> { public class MountedElement<R: Renderer> {
private var element: MountedElementKind private var element: MountedElementKind
var type: Any.Type { element.type }
public internal(set) var app: _AnyApp { public internal(set) var app: _AnyApp {
get { get {
@ -117,20 +118,16 @@ public class MountedElement<R: Renderer> {
updateEnvironment() updateEnvironment()
} }
@discardableResult func updateEnvironment() {
func updateEnvironment() -> TypeInfo { let type = element.type
// swiftlint:disable:next force_try
let info = try! typeInfo(of: element.type)
switch element { switch element {
case .app: case .app:
environmentValues = info.injectEnvironment(from: environmentValues, into: &app.app) environmentValues.inject(into: &app.app, type)
case .scene: case .scene:
environmentValues = info.injectEnvironment(from: environmentValues, into: &scene.scene) environmentValues.inject(into: &scene.scene, type)
case .view: case .view:
environmentValues = info.injectEnvironment(from: environmentValues, into: &view.view) environmentValues.inject(into: &view.view, type)
} }
return info
} }
func mount( func mount(
@ -163,59 +160,62 @@ public class MountedElement<R: Renderer> {
} }
} }
extension TypeInfo { extension EnvironmentValues {
fileprivate func injectEnvironment( mutating func inject(into element: inout Any, _ type: Any.Type) {
from environmentValues: EnvironmentValues, // swiftlint:disable:next force_try
into element: inout Any let info = try! typeInfo(of: type)
) -> EnvironmentValues {
var modifiedEnv = environmentValues
// swiftlint:disable force_try // swiftlint:disable force_try
// Extract the view from the AnyView for modification, apply Environment changes: // Extract the view from the AnyView for modification, apply Environment changes:
if genericTypes.contains(where: { $0 is EnvironmentModifier.Type }), if info.genericTypes.contains(where: { $0 is EnvironmentModifier.Type }),
let modifier = try! property(named: "modifier").get(from: element) as? EnvironmentModifier let modifier = try! info.property(named: "modifier")
.get(from: element) as? EnvironmentModifier
{ {
modifier.modifyEnvironment(&modifiedEnv) modifier.modifyEnvironment(&self)
} }
// Inject @Environment values // Inject @Environment values
// swiftlint:disable force_cast // swiftlint:disable force_cast
// `DynamicProperty`s can have `@Environment` properties contained in them, // `DynamicProperty`s can have `@Environment` properties contained in them,
// so we have to inject into them as well. // 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) let propInfo = try! typeInfo(of: dynamicProp.type)
var propWrapper = try! dynamicProp.get(from: element) as! DynamicProperty var propWrapper = try! dynamicProp.get(from: element) as! DynamicProperty
for prop in propInfo.properties.filter({ $0.type is EnvironmentReader.Type }) { for prop in propInfo.properties.filter({ $0.type is EnvironmentReader.Type }) {
var wrapper = try! prop.get(from: propWrapper) as! EnvironmentReader 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! prop.set(value: wrapper, on: &propWrapper)
} }
try! dynamicProp.set(value: propWrapper, on: &element) 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 var wrapper = try! prop.get(from: element) as! EnvironmentReader
wrapper.setContent(from: modifiedEnv) wrapper.setContent(from: self)
try! prop.set(value: wrapper, on: &element) try! prop.set(value: wrapper, on: &element)
} }
// swiftlint:enable force_try // swiftlint:enable force_try
// swiftlint:enable force_cast // swiftlint:enable force_cast
return modifiedEnv
} }
}
extension TypeInfo {
/// Extract all `DynamicProperty` from a type, recursively. /// Extract all `DynamicProperty` from a type, recursively.
/// This is necessary as a `DynamicProperty` can be nested. /// This is necessary as a `DynamicProperty` can be nested.
/// `EnvironmentValues` can also be injected at this point. /// `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]() var dynamicProps = [PropertyInfo]()
for prop in properties where prop.type is DynamicProperty.Type { for prop in properties where prop.type is DynamicProperty.Type {
dynamicProps.append(prop) dynamicProps.append(prop)
// swiftlint:disable force_try // swiftlint:disable force_try
let propInfo = try! typeInfo(of: prop.type) 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) var extracted = try! prop.get(from: source)
dynamicProps.append( dynamicProps.append(
contentsOf: propInfo.dynamicProperties( contentsOf: propInfo.dynamicProperties(
environment, &environment,
source: &extracted source: &extracted
) )
) )

View File

@ -1,4 +1,4 @@
// Copyright 2018-2020 Tokamak contributors // Copyright 2018-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -208,11 +208,12 @@ public final class StackReconciler<R: Renderer> {
body bodyKeypath: ReferenceWritableKeyPath<MountedCompositeElement<R>, Any>, body bodyKeypath: ReferenceWritableKeyPath<MountedCompositeElement<R>, Any>,
result: KeyPath<MountedCompositeElement<R>, (Any) -> T> result: KeyPath<MountedCompositeElement<R>, (Any) -> T>
) -> T { ) -> T {
let info = compositeElement.updateEnvironment() compositeElement.updateEnvironment()
let info = try! typeInfo(of: compositeElement.type)
var stateIdx = 0 var stateIdx = 0
let dynamicProps = info.dynamicProperties( let dynamicProps = info.dynamicProperties(
compositeElement.environmentValues, &compositeElement.environmentValues,
source: &compositeElement[keyPath: bodyKeypath] source: &compositeElement[keyPath: bodyKeypath]
) )

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -24,8 +24,11 @@ extension EnvironmentValues {
/// Returns default settings for the DOM environment /// Returns default settings for the DOM environment
static var defaultEnvironment: Self { static var defaultEnvironment: Self {
var environment = EnvironmentValues() var environment = EnvironmentValues()
// `.toggleStyle` property is internal
environment[_ToggleStyleKey] = _AnyToggleStyle(DefaultToggleStyle()) environment[_ToggleStyleKey] = _AnyToggleStyle(DefaultToggleStyle())
environment[_ColorSchemeKey] = .init(matchMediaDarkScheme: matchMediaDarkScheme)
environment.colorScheme = .init(matchMediaDarkScheme: matchMediaDarkScheme)
environment._defaultAppStorage = LocalStorage.standard environment._defaultAppStorage = LocalStorage.standard
_DefaultSceneStorageProvider.default = SessionStorage.standard _DefaultSceneStorageProvider.default = SessionStorage.standard

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -20,7 +20,9 @@ protocol GtkStackProtocol {}
// extension NavigationView: AnyWidget, ParentView, GtkStackProtocol { // extension NavigationView: AnyWidget, ParentView, GtkStackProtocol {
// var expand: Bool { true } // 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 box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)!
// let stack = gtk_stack_new()! // let stack = gtk_stack_new()!
// let sidebar = gtk_stack_sidebar_new()! // let sidebar = gtk_stack_sidebar_new()!
@ -77,7 +79,9 @@ extension NavigationLink: ViewDeferredToRenderer {
} }
// extension NavigationLink: AnyWidget, ParentView { // extension NavigationLink: AnyWidget, ParentView {
// func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> { // func new(
// _ application: UnsafeMutablePointer<GtkApplication>
// ) -> UnsafeMutablePointer<GtkWidget> {
// let btn = gtk_button_new()! // let btn = gtk_button_new()!
// bindAction(to: btn) // bindAction(to: btn)
// return btn // return btn
@ -105,7 +109,9 @@ extension NavigationLink: ViewDeferredToRenderer {
// } // }
// extension NavigationLink: AnyWidget, ParentView { // extension NavigationLink: AnyWidget, ParentView {
// func new(_ application: UnsafeMutablePointer<GtkApplication>) -> UnsafeMutablePointer<GtkWidget> { // func new(
// _ application: UnsafeMutablePointer<GtkApplication>
// ) -> UnsafeMutablePointer<GtkWidget> {
// print("Creating NavLink widget") // print("Creating NavLink widget")
// let btn = gtk_button_new()! // let btn = gtk_button_new()!
// bindAction(to: btn) // bindAction(to: btn)

View File

@ -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)
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2020 Tokamak contributors // Copyright 2020-2021 Tokamak contributors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -20,12 +20,12 @@ import XCTest
@testable import TokamakCore @testable import TokamakCore
struct Counter: View { private struct Counter: View {
@State var count: Int @State var count: Int
let limit: Int let limit: Int
@ViewBuilder public var body: some View { public var body: some View {
if count < limit { if count < limit {
VStack { VStack {
Button("Increment") { count += 1 } Button("Increment") { count += 1 }
@ -37,7 +37,7 @@ struct Counter: View {
} }
} }
extension Text { private extension Text {
var verbatim: String? { var verbatim: String? {
guard case let .verbatim(text) = storage else { return nil } guard case let .verbatim(text) = storage else { return nil }
return text return text