Skip to content

Commit

Permalink
Introduce AdaptedEnvironmentScreen, for modifying the ViewEnvironment…
Browse files Browse the repository at this point in the history
… when creating screens.
  • Loading branch information
kyleve committed Mar 15, 2023
1 parent b2b0790 commit 753b3e0
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 0 deletions.
119 changes: 119 additions & 0 deletions WorkflowUI/Sources/Screen/AdaptedEnvironmentScreen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2023 Square Inc.
*
* 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.
*/

#if canImport(UIKit)

import Foundation
import ViewEnvironment

/// Wraps a `Screen` tree with a modified `ViewEnvironment`.
///
/// By specifying environmental values with this `Screen`, all child screens nested
/// will inherit those values automatically. Values can be changed
/// anywhere in a sub-tree by inserting another `AdaptedEnvironmentScreen`.
///
/// ```swift
/// MyScreen(...)
/// .adaptedEnvironment(keyPath: \.myValue, to: newValue)
/// ```
///
public struct AdaptedEnvironmentScreen<Content: Screen>: Screen {
/// The screen wrapped by this screen.
public var wrapped: Content

/// Takes in a mutable `ViewEnvironment` which can be mutated to add or override values.
public typealias Adapter = (inout ViewEnvironment) -> Void

var adapter: Adapter

/// Wraps a `Screen` with an environment that is modified using the given configuration block.
///
/// - Parameters:
/// - wrapping: The screen to be wrapped.
/// - adapting: A block that will set environmental values.
public init(
wrapping wrapped: Content,
adapting: @escaping Adapter
) {
self.wrapped = wrapped
self.adapter = adapting
}

/// Wraps a `Screen` with an environment that is modified for a single key and value.
///
/// - Parameters:
/// - wrapping: The screen to be wrapped.
/// - key: The environment key to modify.
/// - value: The new environment value to cascade.
public init<Key: ViewEnvironmentKey>(
wrapping screen: Content,
key: Key.Type,
value: Key.Value
) {
self.init(wrapping: screen, adapting: { $0[key] = value })
}

/// Wraps a `Screen` with an environment that is modified for a single value.
///
/// - Parameters:
/// - wrapping: The screen to be wrapped.
/// - keyPath: The keypath of the environment value to modify.
/// - value: The new environment value to cascade.
public init<Value>(
wrapping screen: Content,
keyPath: WritableKeyPath<ViewEnvironment, Value>,
value: Value
) {
self.init(wrapping: screen, adapting: { $0[keyPath: keyPath] = value })
}

// MARK: Screen

public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription {
var environment = environment

adapter(&environment)

return wrapped.viewControllerDescription(environment: environment)
}
}

extension Screen {
/// Wraps this screen in an `AdaptedEnvironmentScreen` with the given environment key and value.
public func adaptedEnvironment<Key: ViewEnvironmentKey>(
key: Key.Type,
value: Key.Value
) -> AdaptedEnvironmentScreen<Self> {
AdaptedEnvironmentScreen(wrapping: self, key: key, value: value)
}

/// Wraps this screen in an `AdaptedEnvironmentScreen` with the given keypath and value.
func adaptedEnvironment<Value>(
keyPath: WritableKeyPath<ViewEnvironment, Value>,
value: Value
) -> AdaptedEnvironmentScreen<Self> {
AdaptedEnvironmentScreen(wrapping: self, keyPath: keyPath, value: value)
}

/// Wraps this screen in an `AdaptedEnvironmentScreen` with the given configuration block.
func adaptedEnvironment(
adapting: @escaping (inout ViewEnvironment) -> Void
) -> AdaptedEnvironmentScreen<Self> {
AdaptedEnvironmentScreen(wrapping: self, adapting: adapting)
}
}

#endif
62 changes: 62 additions & 0 deletions WorkflowUI/Tests/AdaptedEnvironmentScreenTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2023 Square Inc.
*
* 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.
*/

#if canImport(UIKit)

import UIKit
import XCTest
@testable import WorkflowUI

class AdaptedEnvironmentScreenTests: XCTestCase {
func test_wrapping() {
var environment: ViewEnvironment = .empty

let screen = TestScreen { environment = $0 }
.adaptedEnvironment(key: TestingKey1.self, value: "adapted1.1")
.adaptedEnvironment(key: TestingKey1.self, value: "adapted1.2")
.adaptedEnvironment(key: TestingKey2.self, value: "adapted2.1")
.adaptedEnvironment(key: TestingKey1.self, value: "adapted1.3")
.adaptedEnvironment(key: TestingKey2.self, value: "adapted2.2")

_ = screen.viewControllerDescription(environment: .empty)

// The inner-most change; the one closest to the screen; should be the value we get.
XCTAssertEqual(environment[TestingKey1.self], "adapted1.1")
XCTAssertEqual(environment[TestingKey2.self], "adapted2.1")
}
}

fileprivate enum TestingKey1: ViewEnvironmentKey {
static let defaultValue: String? = nil
}

fileprivate enum TestingKey2: ViewEnvironmentKey {
static let defaultValue: String? = nil
}

fileprivate struct TestScreen: Screen {
var read: (ViewEnvironment) -> Void

func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription {
read(environment)

return ViewController.description(for: self, environment: environment)
}

private class ViewController: ScreenViewController<TestScreen> {}
}

#endif

0 comments on commit 753b3e0

Please sign in to comment.