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:
parent
e04b7934fb
commit
192c43b140
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue