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> {
|
||||
private var element: MountedElementKind
|
||||
var type: Any.Type { element.type }
|
||||
|
||||
public internal(set) var app: _AnyApp {
|
||||
get {
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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.
|
||||
|
@ -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]
|
||||
)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
@ -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()!
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue