├── XcodeProject
├── App
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── MyEnvironmentKey.swift
│ ├── View.swift
│ ├── Info.plist
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── AppDelegate.swift
│ └── ViewController.swift
├── Package.swift
├── .swiftpm
│ └── xcode
│ │ └── package.xcworkspace
│ │ └── contents.xcworkspacedata
├── UIEnvironment.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ ├── xcshareddata
│ │ └── xcschemes
│ │ │ └── App.xcscheme
│ └── project.pbxproj
└── Tests
│ └── UIEnvironmentValuesTests
│ ├── Helpers
│ ├── Locale+ExpressibleByStringLiteral.swift
│ ├── Helpers.swift
│ ├── UIViewController+Extensions.swift
│ └── XCTestCase+Extensions.swift
│ ├── Predefined Environment Keys Tests
│ └── UIUserInterfaceStyleEnvironmentKeyTests.swift
│ ├── UIEnvironmentValuesTests.swift
│ └── UIEnvironmentUpdatingTests.swift
├── .gitignore
├── Sources
└── UIEnvironment
│ ├── Utilities
│ ├── UIView+Traverse.swift
│ ├── UIApplication+Windows.swift
│ ├── UIResponder+EnvironmentValuesWrapper.swift
│ ├── UIResponder+Environment.swift
│ ├── UIViewController+Traverse.swift
│ └── UIResponder+TraverseHierarchy.swift
│ ├── UIEnvironmentUpdating.swift
│ ├── Predefined Environment Keys
│ ├── TimeZoneEnvironmentKey.swift
│ ├── CalendarEnvironmentKey.swift
│ ├── LocaleEnvironmentKey.swift
│ ├── SizeCategoryEnvironmentKey.swift
│ └── UIUserInterfaceStyleEnvironmentKey.swift
│ ├── UIEnvironmentValues+PredefinedListeners.swift
│ ├── Documentation.docc
│ └── UIEnvironment.md
│ ├── UIEnvironmentKey.swift
│ ├── UIEnvironment.swift
│ ├── UIEnvironmentable.swift
│ └── UIEnvironmentValues.swift
├── Package.resolved
├── .github
└── workflows
│ ├── ci.yml
│ └── documentation.yml
├── Package.swift
├── LICENSE.md
├── .swiftpm
└── xcode
│ └── xcshareddata
│ └── xcschemes
│ └── UIEnvironmentPackage.xcscheme
└── README.md
/XcodeProject/App/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/XcodeProject/Package.swift:
--------------------------------------------------------------------------------
1 | import PackageDescription
2 |
3 | let package = Package(
4 | name: "",
5 | products: [],
6 | dependencies: [],
7 | targets: []
8 | )
9 |
--------------------------------------------------------------------------------
/XcodeProject/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/XcodeProject/App/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/XcodeProject/UIEnvironment.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | /.swiftpm/config/registries.json
8 | /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | /.netrc
10 | /swift-docc
11 | .docc-build
12 |
--------------------------------------------------------------------------------
/XcodeProject/Tests/UIEnvironmentValuesTests/Helpers/Locale+ExpressibleByStringLiteral.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Locale: ExpressibleByStringLiteral {
4 | public init(stringLiteral value: StringLiteralType) {
5 | self.init(identifier: value)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/XcodeProject/Tests/UIEnvironmentValuesTests/Helpers/Helpers.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | @discardableResult
5 | func putInViewHierarchy(_ vc: UIViewController) -> UIWindow {
6 | let window = UIWindow()
7 | window.rootViewController = vc
8 | window.makeKeyAndVisible()
9 | return window
10 | }
11 |
--------------------------------------------------------------------------------
/XcodeProject/Tests/UIEnvironmentValuesTests/Helpers/UIViewController+Extensions.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIViewController {
4 | func addChildViewController(_ child: UIViewController) {
5 | addChild(child)
6 | view.addSubview(child.view)
7 | child.didMove(toParent: self)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/Utilities/UIView+Traverse.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIView {
4 | internal func traverse(_ stopVisitor: (_ view: UIView) -> Bool) {
5 | guard !stopVisitor(self) else { return }
6 |
7 | self.subviews.forEach { view in
8 | view.traverse(stopVisitor)
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/XcodeProject/UIEnvironment.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/XcodeProject/App/MyEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | private struct MyEnvironmentKey: UIEnvironmentKey {
4 | static let defaultValue: String = "Default value"
5 | }
6 |
7 | extension UIEnvironmentValues {
8 | var myCustomValue: String {
9 | get { self[MyEnvironmentKey.self] }
10 | set { self[MyEnvironmentKey.self] = newValue }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/Utilities/UIApplication+Windows.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIApplication {
4 | internal func forEachWindow(_ body: (UIWindow) -> Void) {
5 | UIApplication
6 | .shared
7 | .connectedScenes
8 | .compactMap { $0 as? UIWindowScene }
9 | .flatMap { $0.windows }
10 | .forEach(body)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/Utilities/UIResponder+EnvironmentValuesWrapper.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIResponder {
4 | internal class EnvironmentValuesWrapper: NSObject {
5 | var values: UIEnvironmentValues
6 |
7 | init(values: UIEnvironmentValues) {
8 | self.values = values
9 | }
10 | }
11 |
12 | internal static let key = UnsafeMutablePointer.allocate(capacity: 1)
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/UIEnvironmentUpdating.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// An interface for a view/view controller that uses ``UIEnvironment`` property
4 | /// and wants to update on the environment changes.
5 | public protocol UIEnvironmentUpdating {
6 |
7 | /// UIEnvironment calls this function when the ``UIEnvironment`` changes.
8 | /// It can be used to react to the changes and get the updated ``UIEnvironment`` values.
9 | func updateEnvironment()
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/Predefined Environment Keys/TimeZoneEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | private enum TimeZoneEnvironmentKey: UIEnvironmentKey {
4 | static let defaultValue: TimeZone = TimeZone.current
5 | }
6 |
7 | extension UIEnvironmentValues {
8 |
9 | /// The current time zone that views should use when handling dates.
10 | public var timeZone: TimeZone {
11 | get { self[TimeZoneEnvironmentKey.self] }
12 | set { self[TimeZoneEnvironmentKey.self] = newValue }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/Predefined Environment Keys/CalendarEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | private enum CalendarEnvironmentKey: UIEnvironmentKey {
5 | static let defaultValue: Calendar = Calendar.current
6 | }
7 |
8 | extension UIEnvironmentValues {
9 |
10 | /// The current calendar that views should use when handling dates.
11 | public var calendar: Calendar {
12 | get { self[CalendarEnvironmentKey.self] }
13 | set { self[CalendarEnvironmentKey.self] = newValue }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "libffi",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/623637646/libffi.git",
7 | "state" : {
8 | "revision" : "bc1dac0c1b522539f21fc28fe1f7e07bb7c2fbd5",
9 | "version" : "3.4.5"
10 | }
11 | },
12 | {
13 | "identity" : "swifthook",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/623637646/SwiftHook",
16 | "state" : {
17 | "revision" : "474f80132a3a4e74b3f8a5b87b707d74de387769",
18 | "version" : "3.5.2"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/Utilities/UIResponder+Environment.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIResponder {
4 | internal func valueInSelf(forKeyPath keyPath: KeyPath) -> Value? {
5 | guard let wrapper = objc_getAssociatedObject(
6 | self,
7 | UIResponder.key
8 | ) as? UIResponder.EnvironmentValuesWrapper
9 | else {
10 | return nil
11 | }
12 |
13 | var _values = wrapper.values
14 | if _values.hasValue(for: keyPath) {
15 | return wrapper.values[keyPath: keyPath]
16 | // return value
17 | }
18 |
19 | return nil
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - '*'
10 | workflow_dispatch:
11 |
12 | jobs:
13 | tests:
14 | runs-on: macos-13
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: install xcbeautify
18 | run: brew install xcbeautify
19 | - name: Select Xcode Version
20 | uses: maxim-lobanov/setup-xcode@v1
21 | with:
22 | xcode-version: "15.2"
23 | - name: Run tests
24 | run: xcodebuild test -project ./XcodeProject/UIEnvironment.xcodeproj -destination platform="iOS Simulator,name=iPhone 15 Pro Max" -scheme "App" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO | xcbeautify
--------------------------------------------------------------------------------
/XcodeProject/UIEnvironment.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "libffi",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/623637646/libffi.git",
7 | "state" : {
8 | "revision" : "bc1dac0c1b522539f21fc28fe1f7e07bb7c2fbd5",
9 | "version" : "3.4.5"
10 | }
11 | },
12 | {
13 | "identity" : "swifthook",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/623637646/SwiftHook",
16 | "state" : {
17 | "revision" : "474f80132a3a4e74b3f8a5b87b707d74de387769",
18 | "version" : "3.5.2"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "UIEnvironment",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | .library(
13 | name: "UIEnvironment",
14 | targets: ["UIEnvironment"]),
15 | ],
16 | dependencies: [
17 | .package(url: "https://github.com/623637646/SwiftHook", from: "3.5.2")
18 | ],
19 | targets: [
20 | .target(
21 | name: "UIEnvironment",
22 | dependencies: [
23 | .product(name: "SwiftHook", package: "SwiftHook")
24 | ]),
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/Utilities/UIViewController+Traverse.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIViewController {
4 | internal func traverse(_ stopVisitor: (_ viewController: UIViewController) -> Bool) {
5 | guard !stopVisitor(self) else { return }
6 |
7 | self.children.forEach { viewController in
8 | viewController.traverse(stopVisitor)
9 | }
10 |
11 | self.childModalViewController?.traverse(stopVisitor)
12 | }
13 | }
14 |
15 | extension UIViewController {
16 | fileprivate var childModalViewController: UIViewController? {
17 | if self.presentedViewController?.presentingViewController == self {
18 | return self.presentedViewController
19 | } else {
20 | return nil
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/UIEnvironmentValues+PredefinedListeners.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | extension UIEnvironmentValues {
5 | /// A closure that is called once for each instance of UIEnvironment passing it's `keyPath`.
6 | /// As such, any setup or initialization that has to happens only once, has to be handled internally within this closure.
7 | ///
8 | /// Use this property to disable or override default environment listeners.
9 | ///
10 | public static var setupPredefinedEnvironmentListeners: (_ keyPath: AnyKeyPath) -> (() -> Void)? = { _ in
11 | {
12 | UIScreen.setupTraitCollectionListener
13 | UIWindow.setupOverrideUserInterfaceListener
14 | UIEnvironmentValues.setupCurrentLocaleListener
15 | UIEnvironmentValues.setupSizeCategoryListener
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/Predefined Environment Keys/LocaleEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | private enum LocaleEnvironmentKey: UIEnvironmentKey {
4 | static let defaultValue: Locale = Locale.current
5 | }
6 |
7 | extension UIEnvironmentValues {
8 |
9 | /// The current locale that views should use.
10 | public var locale: Locale {
11 | get { self[LocaleEnvironmentKey.self] }
12 | set { self[LocaleEnvironmentKey.self] = newValue }
13 | }
14 |
15 | public static let setupCurrentLocaleListener: Void = {
16 | NotificationCenter.default.addObserver(
17 | forName: NSLocale.currentLocaleDidChangeNotification,
18 | object: nil,
19 | queue: .main
20 | ) { _ in
21 | UIApplication.shared.forEachWindow {
22 | $0.environment(\.locale, Locale.current)
23 | }
24 | }
25 | }()
26 | }
27 |
--------------------------------------------------------------------------------
/XcodeProject/App/View.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 | import UIEnvironment
4 |
5 | final class View: UILabel {
6 | @UIEnvironment(\.myCustomValue) var customValue: String
7 | @UIEnvironment(\.userInterfaceStyle) var userInterfaceStyle
8 |
9 | override init(frame: CGRect) {
10 | super.init(frame: frame)
11 | self.text = "MyCustomValue: \(customValue)"
12 | updateEnvironment()
13 | }
14 |
15 | required init?(coder: NSCoder) {
16 | fatalError("init(coder:) has not been implemented")
17 | }
18 | }
19 |
20 | extension View: UIEnvironmentUpdating {
21 | func updateEnvironment() {
22 | switch userInterfaceStyle {
23 | case .light, .unspecified:
24 | backgroundColor = .lightGray
25 | textColor = .black
26 | case .dark:
27 | backgroundColor = .black
28 | textColor = .white
29 | @unknown default:
30 | fatalError()
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Łukasz Śliwiński
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Sources/UIEnvironment/Predefined Environment Keys/SizeCategoryEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | private enum SizeCategoryEnvironmentKey: UIEnvironmentKey {
5 | static let defaultValue: UIContentSizeCategory = UIApplication.shared.preferredContentSizeCategory
6 | }
7 |
8 | extension UIEnvironmentValues {
9 |
10 | /// The current Size Category.
11 | ///
12 | /// This value changes as the user's chosen Dynamic Type size changes. The
13 | /// default value is device-dependent.
14 | public var sizeCategory: UIContentSizeCategory {
15 | get { self[SizeCategoryEnvironmentKey.self] }
16 | set { self[SizeCategoryEnvironmentKey.self] = newValue }
17 | }
18 |
19 | public static let setupSizeCategoryListener: Void = {
20 | NotificationCenter.default.addObserver(
21 | forName: UIContentSizeCategory.didChangeNotification,
22 | object: nil,
23 | queue: .main
24 | ) { _ in
25 | UIApplication.shared.forEachWindow {
26 | $0.environment(\.sizeCategory, UIApplication.shared.preferredContentSizeCategory)
27 | }
28 | }
29 | }()
30 | }
31 |
--------------------------------------------------------------------------------
/XcodeProject/Tests/UIEnvironmentValuesTests/Helpers/XCTestCase+Extensions.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | extension XCTestCase {
4 | func trackForMemoryLeaks(_ instance: AnyObject, file: StaticString = #filePath, line: UInt = #line) {
5 | addTeardownBlock { [weak instance] in
6 | XCTAssertNil(
7 | instance,
8 | "#\(line): Instance should have been deallocated. Potential memory leak.",
9 | file: file,
10 | line: line
11 | )
12 | }
13 | }
14 | }
15 |
16 | extension XCTestCase {
17 | func presentViewController(
18 | _ viewController: UIViewController,
19 | on presentingViewController: UIViewController
20 | ) {
21 | let exp = expectation(description: #function)
22 |
23 | presentingViewController.present(viewController, animated: false) {
24 | exp.fulfill()
25 | }
26 |
27 | wait(for: [exp], timeout: 1.0)
28 | }
29 |
30 | func dismissViewController(_ viewController: UIViewController) {
31 | let exp = expectation(description: #function)
32 |
33 | viewController.dismiss(animated: false) {
34 | exp.fulfill()
35 | }
36 |
37 | wait(for: [exp], timeout: 1.0)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/XcodeProject/App/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | UIEnvironmentApp
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/Documentation.docc/UIEnvironment.md:
--------------------------------------------------------------------------------
1 | # ``UIEnvironment``
2 |
3 | A framework that mimics the SwiftUI view's environment to replicate the value distribution thought your UIKit view hierarchy.
4 |
5 | ## Additional Resources
6 |
7 | * [GitHub Repo](https://github.com/nonameplum/UIEnvironment)
8 | * [SwiftUI @Environment](https://developer.apple.com/documentation/swiftui/environment)
9 | * [Example App](https://github.com/nonameplum/UIEnvironment/tree/main/XcodeProject)
10 |
11 | ## Overview
12 |
13 | Use the `UIEnvironment` property wrapper to read a value stored in a view’s environment. Indicate the value to read using an `UIEnvironmentValues` key path in the property declaration. For example, you can create a property that reads the user interface style of the current view using the key path of the `userInterfaceStyle` property:
14 |
15 | ```swift
16 | final class ViewController: UIViewController {
17 | @UIEnvironment(\.userInterfaceStyle) private var userInterfaceStyle
18 | ...
19 | }
20 | ```
21 |
22 | You can condition a view's content on the associated value, which you read from the declared property by directly referring from it:
23 |
24 | ```swift
25 | override func viewDidLoad() {
26 | super.viewDidLoad()
27 | view.backgroundColor = userInterfaceStyle == .dark ? .black : .white
28 | }
29 | ```
30 |
31 | If the value changes, UIEnvironment framework updates any view that implements ``UIEnvironmentUpdating``.
32 | For example, that might happen in the above example if the user changes the Appearance settings.
33 |
34 | ```swift
35 | final class ViewController: UIViewController {
36 | @UIEnvironment(\.userInterfaceStyle) private var userInterfaceStyle
37 | ...
38 | }
39 |
40 | extension ViewController: UIEnvironmentUpdating {
41 | func updateEnvironment() {
42 | view.backgroundColor = userInterfaceStyle == .dark ? .black : .white
43 | }
44 | }
45 | ```
--------------------------------------------------------------------------------
/XcodeProject/App/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/.github/workflows/documentation.yml:
--------------------------------------------------------------------------------
1 | # Build and deploy DocC to GitHub pages. Based off of @karwa's work here:
2 | # https://github.com/karwa/swift-url/blob/main/.github/workflows/docs.yml
3 | name: Documentation
4 |
5 | on:
6 | release:
7 | types:
8 | - published
9 | push:
10 | branches:
11 | - main
12 | workflow_dispatch:
13 |
14 | jobs:
15 | build:
16 | runs-on: macos-13
17 | steps:
18 | - name : Setup Xcode
19 | uses: maxim-lobanov/setup-xcode@v1
20 | with:
21 | xcode-version: 15.2
22 |
23 | - name: Checkout Package
24 | uses: actions/checkout@v2
25 | with:
26 | fetch-depth: 0
27 |
28 | - name: Checkout gh-pages Branch
29 | uses: actions/checkout@v2
30 | with:
31 | ref: gh-pages
32 | path: docs-out
33 |
34 | - name: Build documentation
35 | run: >
36 | rm -rf docs-out/.git;
37 | rm -rf docs-out/main;
38 |
39 | for tag in $(echo "main"; git tag);
40 | do
41 | echo "⏳ Generating documentation for "$tag" release.";
42 |
43 | if [ -d "docs-out/$tag" ]
44 | then
45 | echo "✅ Documentation for "$tag" already exists.";
46 | else
47 | git checkout "$tag";
48 |
49 | rm -rf .build;
50 |
51 | mkdir -p "docs-out/$tag";
52 |
53 | xcodebuild docbuild -scheme UIEnvironment \
54 | -destination generic/platform=iOS \
55 | OTHER_DOCC_FLAGS="--transform-for-static-hosting --output-path docs-out/$tag --hosting-base-path /UIEnvironment/$tag" \
56 | && echo "✅ Documentation generated for "$tag" release." \
57 | || echo "⚠️ Documentation skipped for "$tag".";
58 | fi;
59 | done
60 |
61 | - name: Fix permissions
62 | run: 'sudo chown -R $USER docs-out'
63 | - name: Publish documentation to GitHub Pages
64 | uses: JamesIves/github-pages-deploy-action@4.1.7
65 | with:
66 | branch: gh-pages
67 | folder: docs-out
68 | single-commit: true
--------------------------------------------------------------------------------
/Sources/UIEnvironment/UIEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A key for accessing values in the environment.
4 | ///
5 | /// You can create custom environment values by extending the
6 | /// ``UIEnvironmentValues`` structure with new properties.
7 | /// First declare a new environment key type and specify a value for the
8 | /// required ``defaultValue`` property:
9 | ///
10 | /// ```swift
11 | /// private struct MyEnvironmentKey: UIEnvironmentKey {
12 | /// static let defaultValue: String = "Default value"
13 | /// }
14 | /// ```
15 | ///
16 | /// The Swift compiler automatically infers the associated ``Value`` type as the
17 | /// type you specify for the default value. Then use the key to define a new
18 | /// environment value property:
19 | ///
20 | /// ```swift
21 | /// extension UIEnvironmentValues {
22 | /// var myCustomValue: String {
23 | /// get { self[MyEnvironmentKey.self] }
24 | /// set { self[MyEnvironmentKey.self] = newValue }
25 | /// }
26 | /// }
27 | /// ```
28 | ///
29 | /// Clients of your environment value never use the key directly.
30 | /// Instead, they use the key path of your custom environment value property.
31 | /// To set the environment value for a view/view controllers and all its children, add the
32 | /// ``UIEnvironmentable/environment(_:_:)`` convenience method:
33 | ///
34 | /// ```swift
35 | /// MyView()
36 | /// .environment(\.myCustomValue, "Another string")
37 | /// ```
38 | ///
39 | /// To read the value from inside `MyView` or one of its descendants, use the
40 | /// ``UIEnvironment`` property wrapper:
41 | ///
42 | /// ```swift
43 | /// class MyView: UILabel {
44 | /// @UIEnvironment(\.myCustomValue) var customValue: String
45 | ///
46 | /// override init(frame: CGRect) {
47 | /// super.init(frame: frame)
48 | /// self.text = customValue
49 | /// }
50 | ///
51 | /// required init?(coder: NSCoder) {
52 | /// fatalError("init(coder:) has not been implemented")
53 | /// }
54 | /// }
55 | /// ```
56 | ///
57 | public protocol UIEnvironmentKey {
58 |
59 | /// The associated type representing the type of the environment key's value.
60 | associatedtype Value
61 |
62 | /// The default value for the environment key.
63 | static var defaultValue: Self.Value { get }
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/Utilities/UIResponder+TraverseHierarchy.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIResponder {
4 | internal func traverseTopDown(_ visitor: (_ responder: UIResponder) -> Void) {
5 | if let view = self as? UIView {
6 | // If we start from the UIView, traverse until the first subview
7 | // that is the `UIViewController.view`.
8 | // The rest will be traversed from the UIViewController point of view.
9 | var stop = false
10 | // Do not look for a view controller if the view is the traverse starting point.
11 | var isFirst = true
12 | view.traverse { view in
13 | if let nextVC = view.next as? UIViewController, !isFirst {
14 | nextVC.traverseTopDown(visitor)
15 | stop = true
16 | } else {
17 | visitor(view)
18 | }
19 | isFirst = false
20 | return stop
21 | }
22 | } else if let viewController = self as? UIViewController {
23 | viewController.traverse { viewController in
24 | visitor(viewController)
25 | viewController.view.traverse { view in
26 | visitor(view)
27 | if view.next is UIViewController, view.next != viewController {
28 | // Stop view's traverse if the view controller is reached by the subview,
29 | // as all view controllers will be visited by the `viewController.traverse`.
30 | return true
31 | } else {
32 | return false
33 | }
34 | }
35 | return false
36 | }
37 | }
38 | }
39 |
40 | internal func traverseBottomUp(stop: (_ responder: UIResponder) -> Bool) {
41 | var nextResponder: UIResponder? = self
42 | while let responder = nextResponder {
43 | if stop(responder) {
44 | break
45 | }
46 |
47 | if responder.next == nil,
48 | let parent = (responder as? UIViewController)?.parent {
49 | nextResponder = parent
50 | } else {
51 | nextResponder = responder.next
52 | }
53 | }
54 | }
55 | }
56 |
57 | extension UIViewController {
58 | fileprivate var isModal: Bool {
59 | return presentingViewController?.presentedViewController == self
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/UIEnvironmentPackage.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/XcodeProject/App/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/XcodeProject/UIEnvironment.xcodeproj/xcshareddata/xcschemes/App.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/UIEnvironment.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | /// A property wrapper that reads a value from a UIKit view's environment.
5 | ///
6 | /// Use the `UIEnvironment` property wrapper to read a value
7 | /// stored in a view's environment. Indicate the value to read using an
8 | /// ``UIEnvironmentValues`` key path in the property declaration. For example, you
9 | /// can create a property that reads the color scheme of the current
10 | /// view using the key path of the ``UIEnvironmentValues/userInterfaceStyle``
11 | /// property:
12 | ///
13 | /// ```swift
14 | /// @UIEnvironment(\.userInterfaceStyle) var userInterfaceStyle: UIUserInterfaceStyle
15 | /// ```
16 | ///
17 | /// You can condition a view's content on the associated value, which
18 | /// you read from the declared property by directly referring from it:
19 | ///
20 | /// ```swift
21 | /// if userInterfaceStyle == .dark {
22 | /// DarkContent()
23 | /// } else {
24 | /// LightContent()
25 | /// }
26 | /// ```
27 | ///
28 | /// If the value changes, UIEnvironment framework updates any view
29 | /// that implements ``UIEnvironmentUpdating``.
30 | /// For example, that might happen in the above example if the user
31 | /// changes the Appearance settings.
32 | ///
33 | /// You can use this property wrapper to read --- but not set --- an environment
34 | /// value. UIEnvironment framework updates some environment values automatically based on system
35 | /// settings and provides reasonable defaults for others. You can override some
36 | /// of these, as well as set custom environment values that you define,
37 | /// using the ``UIEnvironmentable/environment(_:_:)`` convenience method.
38 | ///
39 | /// For the complete list of environment values provided by UIEnvironment framework, see the
40 | /// properties of the ``UIEnvironmentValues`` structure. For information about
41 | /// creating custom environment values, see the ``UIEnvironmentKey`` protocol.
42 | @propertyWrapper public struct UIEnvironment {
43 | public static subscript(
44 | _enclosingInstance instance: EnclosingSelf,
45 | wrapped wrappedKeyPath: ReferenceWritableKeyPath,
46 | storage storageKeyPath: ReferenceWritableKeyPath
47 | ) -> Value {
48 | get {
49 | let keyPath = instance[keyPath: storageKeyPath].keyPath
50 | return instance.value(forKeyPath: keyPath)
51 | }
52 | set {}
53 | }
54 |
55 | @available(*, unavailable, message: "@UIEnvironment can only be applied to classes")
56 | /// The wrapped property can not be directly used.
57 | ///
58 | /// You don't access`wrappedValue` directly.
59 | /// Instead, you read the property variable created with
60 | /// the ``UIEnvironment`` property wrapper:
61 | ///
62 | /// ```swift
63 | /// @Environment(\.userInterfaceStyle) private var userInterfaceStyle
64 | ///
65 | /// if userInterfaceStyle == .dark {
66 | /// DarkContent()
67 | /// } else {
68 | /// LightContent()
69 | /// }
70 | /// ```
71 | ///
72 | public var wrappedValue: Value {
73 | get { fatalError() }
74 | set { fatalError() }
75 | }
76 |
77 | private let keyPath: KeyPath
78 |
79 | /// Creates an environment property to read the specified key path.
80 | /// - Parameter keyPath: A key path to a specific resulting value.
81 | public init(_ keyPath: KeyPath) {
82 | if let setupPredefinedEnvironmentListeners = UIEnvironmentValues.setupPredefinedEnvironmentListeners(keyPath) {
83 | setupPredefinedEnvironmentListeners()
84 | }
85 | self.keyPath = keyPath
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/XcodeProject/Tests/UIEnvironmentValuesTests/Predefined Environment Keys Tests/UIUserInterfaceStyleEnvironmentKeyTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIEnvironment
3 | import UIKit
4 | import XCTest
5 |
6 | final class UIUserInterfaceStyleEnvironmentKeyTests: XCTestCase {
7 | func test_interfaceStyleEnvironmentKey_overrideUserInterfaceStyle_changesEnvironmentValue() {
8 | let vc = makeVC()
9 | let window = putInViewHierarchy(vc)
10 |
11 | window.overrideUserInterfaceStyle = .light
12 |
13 | XCTAssertEqual(vc.userInterfaceStyle, .light)
14 |
15 | window.overrideUserInterfaceStyle = .dark
16 |
17 | XCTAssertEqual(vc.userInterfaceStyle, .dark)
18 | }
19 |
20 | func test_preferredUserInterfaceStyle_changesStyleForTheEnclosingPresentation() {
21 | let vc = makeVC()
22 | let modalVC = makeVC()
23 | let window = putInViewHierarchy(vc)
24 | presentViewController(modalVC, on: vc)
25 |
26 | let view = EnvView()
27 | modalVC.view.addSubview(view)
28 | let subView = EnvView()
29 | view.addSubview(subView)
30 |
31 | window.overrideUserInterfaceStyle = .light
32 |
33 | XCTAssertEqual(vc.userInterfaceStyle, .light, "precondition")
34 | XCTAssertEqual(modalVC.userInterfaceStyle, .light, "precondition")
35 |
36 | subView.preferredUserInterfaceStyle(.dark)
37 |
38 | XCTAssertEqual(vc.userInterfaceStyle, .light, "Expected the value change only to the nearest enclosing presentation")
39 | XCTAssertEqual(vc.envView.userInterfaceStyle, .light, "Expected the value change only to the nearest enclosing presentation")
40 |
41 | XCTAssertEqual(view.userInterfaceStyle, .dark)
42 | XCTAssertEqual(subView.userInterfaceStyle, .dark, "Expected that the value should flow down")
43 | XCTAssertEqual(modalVC.userInterfaceStyle, .dark, "Expected that the value propagates up through the view hierarchy")
44 | XCTAssertEqual(modalVC.envView.userInterfaceStyle, .dark, "Expected that the value propagates up through the view hierarchy")
45 | }
46 |
47 | func test_preferredUserInterfaceStyle_changesStyleUntilFirstMetOverriddenStyle() {
48 | let vc = makeVC()
49 | let view = EnvView()
50 | vc.view.addSubview(view)
51 | let subView = EnvView()
52 | view.addSubview(subView)
53 |
54 | vc.envView.preferredUserInterfaceStyle(.dark)
55 |
56 | XCTAssertEqual(vc.userInterfaceStyle, .dark, "precondition")
57 | XCTAssertEqual(vc.envView.userInterfaceStyle, .dark, "precondition")
58 | XCTAssertEqual(view.userInterfaceStyle, .dark, "precondition")
59 | XCTAssertEqual(subView.userInterfaceStyle, .dark, "precondition")
60 |
61 | view.preferredUserInterfaceStyle(.light)
62 |
63 | XCTAssertEqual(vc.userInterfaceStyle, .dark, "Expected to change the value until another overriden style")
64 | XCTAssertEqual(vc.envView.userInterfaceStyle, .dark, "Expected to change the value until another overriden style")
65 | XCTAssertEqual(view.userInterfaceStyle, .light, "precondition")
66 | XCTAssertEqual(subView.userInterfaceStyle, .light, "precondition")
67 | }
68 |
69 | // MARK: Helpers
70 | private func makeVC() -> EnvViewController {
71 | return EnvViewController()
72 | }
73 |
74 | private final class EnvView: UIView {
75 | @UIEnvironment(\.userInterfaceStyle) var userInterfaceStyle
76 | }
77 |
78 | private final class EnvViewController: UIViewController {
79 | @UIEnvironment(\.userInterfaceStyle) var userInterfaceStyle
80 | var envView: EnvView { view as! EnvView }
81 |
82 | override func loadView() {
83 | view = EnvView()
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/XcodeProject/App/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | @_exported import UIEnvironment
2 | import UIKit
3 |
4 | @UIApplicationMain
5 | class AppDelegate: UIResponder, UIApplicationDelegate {
6 | var window: UIWindow?
7 | private var rootViewController: UIViewController {
8 | guard let vc = window?.rootViewController else {
9 | fatalError()
10 | }
11 |
12 | return vc
13 | }
14 |
15 | func application(
16 | _ application: UIApplication,
17 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
18 | ) -> Bool {
19 | window = .init(frame: UIScreen.main.bounds)
20 | window?.rootViewController = makeRootViewController()
21 | window?.makeKeyAndVisible()
22 | return true
23 | }
24 |
25 | // MARK: Helpers
26 | private func makeRootViewController() -> UIViewController {
27 | return makeTabBarController()
28 | }
29 |
30 | private func makeTabBarController() -> UITabBarController {
31 | let tabVC = UITabBarController()
32 | let vc1 = makeNavigationController()
33 | vc1.tabBarItem = .init(title: "First", image: UIImage(systemName: "1.circle.fill"), tag: 0)
34 | let vc2 = makeNavigationController()
35 | vc2.tabBarItem = .init(title: "Second", image: UIImage(systemName: "2.circle.fill"), tag: 1)
36 | tabVC.viewControllers = [vc1, vc2]
37 | tabVC.loadViewIfNeeded()
38 | tabVC.tabBar.isTranslucent = true
39 | tabVC.tabBar.barStyle = .black
40 |
41 | return tabVC
42 | }
43 |
44 | private func makeNavigationController() -> UINavigationController {
45 | let vc1 = makeViewController()
46 | let vc2 = makeViewController()
47 | let nav = UINavigationController()
48 | nav.viewControllers = [vc1, vc2]
49 | nav.viewControllers.enumerated().forEach { index, vc in
50 | vc.title = Self.stringNumber(from: index + 1)
51 | }
52 | return nav
53 | }
54 |
55 | private func makeViewController() -> ViewController {
56 | return ViewController(actions: [pushAction(), presentAction(), localeAction()])
57 | }
58 |
59 | private func pushAction() -> ViewController.Action {
60 | .init(title: "Push", onTap: { [unowned self] viewController in
61 | guard let navVC = viewController.navigationController else { return }
62 | let vc = self.makeViewController()
63 | vc.title = Self.stringNumber(from: navVC.viewControllers.count + 1)
64 | navVC.pushViewController(vc, animated: true)
65 | })
66 | }
67 |
68 | private func presentAction() -> ViewController.Action {
69 | .init(title: "Present", onTap: { [unowned self] viewController in
70 | let vc = self.makeTabBarController()
71 | viewController.present(vc, animated: true)
72 | })
73 | }
74 |
75 | private func dimissAction() -> ViewController.Action {
76 | .init(title: "Dimiss", onTap: { viewController in
77 | viewController.dismiss(animated: true)
78 | })
79 | }
80 |
81 | private func localeAction() -> ViewController.Action {
82 | var index = 0
83 |
84 | return .init(title: "Change locale", onTap: { [weak self] vc in
85 | let lang = ["pl", "uk", "en"][index % 3]
86 | index += 1
87 | self?.rootViewController.environment(\.locale, .init(identifier: lang))
88 | })
89 | }
90 |
91 | private static func stringNumber(from value: Int) -> String? {
92 | return Self.numberFormatter.string(from: NSNumber(value: value))?.localizedCapitalized
93 | }
94 |
95 | private static let numberFormatter: NumberFormatter = {
96 | let formatter = NumberFormatter()
97 | formatter.numberStyle = .spellOut
98 | return formatter
99 | }()
100 | }
101 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/UIEnvironmentable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | /// An interface for a view/view controller that uses ``UIEnvironment`` property.
5 | public protocol UIEnvironmentable: UIResponder {}
6 |
7 | extension UIView: UIEnvironmentable {}
8 | extension UIViewController: UIEnvironmentable {}
9 |
10 | extension UIEnvironmentable {
11 | /// Sets the environment value of the specified key path to the given value.
12 | ///
13 | /// Use this modifier to set one of the writable properties of the
14 | /// ``UIEnvironmentValues`` structure, including custom values that you
15 | /// create. For example, you can set the value associated with the
16 | /// ``UIEnvironmentValues/userInterfaceStyle`` key:
17 | ///
18 | /// ```swift
19 | /// MyView()
20 | /// .environment(\.userInterfaceStyle, .dark)
21 | /// ```
22 | ///
23 | /// You then read the value inside `MyView` or one of its descendants
24 | /// using the ``UIEnvironment`` property wrapper:
25 | ///
26 | /// ```swift
27 | /// class MyView: UIView {
28 | /// @Environment(\.userInterfaceStyle) var userInterfaceStyle: UIUserInterfaceStyle
29 | /// }
30 | /// ```
31 | ///
32 | /// The ``UIEnvironmentable/environment(_:_:)`` method affects the given view or view controller,
33 | /// as well as that view/view controller's descendant views and view controllers. It has no effect
34 | /// outside the view hierarchy on which you call it.
35 | ///
36 | /// - Parameters:
37 | /// - keyPath: A key path that indicates the property of the
38 | /// ``UIEnvironmentValues`` structure to update.
39 | /// - value: The new value to set for the item specified by `keyPath`.
40 | public func environment(_ keyPath: WritableKeyPath, _ value: V) {
41 | var values = environmentValues
42 | values[keyPath: keyPath] = value
43 | objc_setAssociatedObject(
44 | self,
45 | UIResponder.key,
46 | UIResponder.EnvironmentValuesWrapper(values: values),
47 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC
48 | )
49 |
50 | notifyChilds()
51 | }
52 | }
53 |
54 | // MARK: - Internal helpers
55 | extension UIEnvironmentable {
56 | /// Gets the ``UIEnvironmentValues`` value for given `keyPath` by finding the first object
57 | /// in the view hierarchy that has it set. If there is no value defined in the view
58 | /// hierarchy, [UIResponder.defaultValues](x-source-tag://UIResponder.defaultValues) instance will be used.
59 | ///
60 | /// - Parameter keyPath: A key path that indicates the property of the
61 | /// ``UIEnvironmentValues`` structure to get the value.
62 | /// - Returns: The value for the specified `keyPath`.
63 | internal func value(
64 | forKeyPath keyPath: KeyPath
65 | ) -> Value {
66 | var value = UIResponder.defaultValues[keyPath: keyPath]
67 |
68 | traverseBottomUp(stop: { responder in
69 | if let receviedValue = responder.valueInSelf(forKeyPath: keyPath) {
70 | value = receviedValue
71 | return true
72 | } else {
73 | return false
74 | }
75 | })
76 |
77 | return value
78 | }
79 | }
80 |
81 | // MARK: - UIResponder Helpers
82 | extension UIResponder {
83 | fileprivate func notifyChilds() {
84 | traverseTopDown { responder in
85 | guard let updatable = responder as? UIEnvironmentUpdating else {
86 | return
87 | }
88 |
89 | updatable.updateEnvironment()
90 | }
91 | }
92 |
93 | fileprivate var environmentValues: UIEnvironmentValues {
94 | get {
95 | let wrapper = objc_getAssociatedObject(self, Self.key) as? EnvironmentValuesWrapper
96 | return wrapper?.values ?? UIEnvironmentValues()
97 | }
98 | set {
99 | guard let wrapper = objc_getAssociatedObject(self, Self.key) as? EnvironmentValuesWrapper
100 | else {
101 | return
102 | }
103 |
104 | wrapper.values = newValue
105 | }
106 | }
107 |
108 | // MARK: Helpers
109 |
110 | /// Default ``UIEnvironmentValues``
111 | /// - Tag: UIResponder.defaultValues
112 | fileprivate static let defaultValues = UIEnvironmentValues()
113 | }
114 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UIEnvironment
2 |
3 | A framework that mimics the [SwiftUI view's environment](https://developer.apple.com/documentation/swiftui/environment) to replicate the value distribution thought your UIKit view hierarchy.
4 |
5 | ---
6 |
7 | * [Overview](#Overview)
8 | * [Documentation](#Documentation)
9 | * [Installation](#Installation)
10 | * [License](#License)
11 |
12 | ## Overview
13 |
14 | Use the `UIEnvironment` property wrapper to read a value stored in a view’s environment. Indicate the value to read using an `UIEnvironmentValues` key path in the property declaration.
15 | For example, you can create a property that reads the user interface style of the current view using the key path of the `userInterfaceStyle` property:
16 |
17 | ```swift
18 | final class ViewController: UIViewController {
19 | @UIEnvironment(\.userInterfaceStyle) private var userInterfaceStyle
20 | ...
21 | }
22 | ```
23 |
24 | You can condition a view's content on the associated value, which
25 | you read from the declared property by directly referring from it:
26 |
27 | ```swift
28 | override func viewDidLoad() {
29 | super.viewDidLoad()
30 | view.backgroundColor = userInterfaceStyle == .dark ? .black : .white
31 | }
32 | ```
33 |
34 | If the value changes, UIEnvironment framework updates any view
35 | that implements ``UIEnvironmentUpdating``.
36 | For example, that might happen in the above example if the user
37 | changes the Appearance settings.
38 |
39 | ```swift
40 | final class ViewController: UIViewController {
41 | @UIEnvironment(\.userInterfaceStyle) private var userInterfaceStyle
42 | ...
43 | }
44 |
45 | extension ViewController: UIEnvironmentUpdating {
46 | func updateEnvironment() {
47 | view.backgroundColor = userInterfaceStyle == .dark ? .black : .white
48 | }
49 | }
50 | ```
51 |
52 | Please refer to the [example application](https://github.com/nonameplum/UIEnvironment/tree/main/XcodeProject) for more details.
53 |
54 | You can use this property wrapper to read _but not set_ an environment
55 | value. UIEnvironment framework updates some environment values automatically based on system
56 | settings and provides reasonable defaults for others. You can override some
57 | of these, as well as set custom environment values that you define,
58 | using the `UIEnvironmentable.environment(_:_:)` convenience method.
59 | For the complete list of environment values provided by UIEnvironment framework, see the
60 | properties of the `UIEnvironmentValues` structure. For information about
61 | creating custom environment values, see the `UIEnvironmentKey` protocol.
62 |
63 | ## Documentation
64 |
65 | The documentation for the latest release is available here:
66 |
67 | * [main](https://nonameplum.github.io/UIEnvironment/main/documentation/uienvironment/)
68 | * [1.0.0](https://nonameplum.github.io/UIEnvironment/1.0.0/documentation/uienvironment/)
69 | * [1.1.0](https://nonameplum.github.io/UIEnvironment/1.0.0/documentation/uienvironment/)
70 |
71 | ## Installation
72 |
73 | You can add UIEnvironment to an Xcode project by adding it as a package dependency.
74 |
75 | 1. From the **File** menu, select **Add Packages...**
76 | 2. Enter "https://github.com/nonameplum/uienvironment" into the package repository URL text field
77 | 3. Depending on how your project is structured:
78 | - If you have a single application target that needs access to the library, then add **UIEnvironment** directly to your application.
79 | - If you want to use this library from multiple Xcode targets, or mixing Xcode targets and SPM targets, you must create a shared framework that depends on **UIEnvironment** and then depend on that framework in all of your targets.
80 |
81 |
82 | You can add `UIEnvironment` to an Xcode project by adding it as a package dependency.
83 |
84 | ### Adding UIEnvironment as a Dependency
85 |
86 | To use the UIEnvironment framework in a SwiftPM project, add the following line to the dependencies in your Package.swift file:
87 |
88 | ```swift
89 | .package(url: "https://github.com/nonameplum/uienvironment"),
90 | ```
91 |
92 | Include `"UIEnvironment"` as a dependency for your executable target:
93 |
94 | ```swift
95 | .target(name: "", dependencies: [
96 | .product(name: "UIEnvironment", package: "uienvironment"),
97 | ]),
98 | ```
99 |
100 | Finally, add `import UIEnvironment` to your source code.
101 |
102 | ## License
103 |
104 | This library is released under the MIT license. See [LICENSE](LICENSE) for details.
105 |
--------------------------------------------------------------------------------
/XcodeProject/App/ViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | final class ViewController: UIViewController {
4 | struct Action {
5 | let title: String
6 | let onTap: (UIViewController) -> Void
7 | }
8 |
9 | @UIEnvironment(\.locale) private var locale
10 | @UIEnvironment(\.userInterfaceStyle) private var userInterfaceStyle
11 | @UIEnvironment(\.timeZone) private var timeZone
12 | @UIEnvironment(\.sizeCategory) private var sizeCategory
13 | private var actions: [Action]
14 |
15 | init(actions: [Action]) {
16 | self.actions = actions
17 | super.init(nibName: nil, bundle: nil)
18 | }
19 |
20 | @available(*, unavailable)
21 | required init?(coder: NSCoder) {
22 | fatalError("init(coder:) has not been implemented")
23 | }
24 |
25 | override func viewDidLoad() {
26 | super.viewDidLoad()
27 | view.backgroundColor = .systemBackground
28 | view.addSubview(stackView)
29 | NSLayoutConstraint.activate([
30 | stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
31 | stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
32 | stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8.0),
33 | stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
34 | ])
35 | stackView.addArrangedSubview(UIView())
36 | }
37 |
38 | // MARK: Helpers
39 | private lazy var stackView: UIStackView = {
40 | let stackView = UIStackView()
41 | stackView.translatesAutoresizingMaskIntoConstraints = false
42 | stackView.axis = .vertical
43 | stackView.alignment = .center
44 | stackView.spacing = 8.0
45 |
46 | stackView.addArrangedSubview(label)
47 | stackView.addArrangedSubview(subview)
48 |
49 | var subviewStyle: UIUserInterfaceStyle = .light
50 | actions.append(
51 | Action(
52 | title: "Toggle user interface style",
53 | onTap: { [weak subview] _ in
54 | subviewStyle = subviewStyle == .dark ? .light : .dark
55 | subview?.environment(\.userInterfaceStyle, subviewStyle)
56 | }
57 | )
58 | )
59 | makeButtons(actions: actions).forEach { button in
60 | stackView.addArrangedSubview(button)
61 | }
62 |
63 | return stackView
64 | }()
65 |
66 | private lazy var label: UILabel = {
67 | let label = UILabel()
68 | label.translatesAutoresizingMaskIntoConstraints = false
69 | label.text = labelText()
70 | label.font = UIFont.preferredFont(forTextStyle: .body)
71 | label.adjustsFontForContentSizeCategory = true
72 | label.numberOfLines = 0
73 | label.lineBreakMode = .byWordWrapping
74 | label.textAlignment = .center
75 | label.setContentHuggingPriority(.init(rawValue: 999.0), for: .vertical)
76 | return label
77 | }()
78 |
79 | private lazy var subview: View = View()
80 |
81 | private func makeButtons(actions: [Action]) -> [UIButton] {
82 | return actions.map { action in
83 | let button = UIButton(configuration: .borderedTinted())
84 | button.translatesAutoresizingMaskIntoConstraints = false
85 | button.setTitle(action.title, for: .normal)
86 | button.addAction(.init(handler: { [unowned self] _ in
87 | action.onTap(self)
88 | }), for: .touchUpInside)
89 | return button
90 | }
91 | }
92 |
93 | private func labelText() -> String {
94 | """
95 | 🚀
96 | UIEnvironment
97 | Example
98 | Locale: \(locale.identifier)
99 | Language: \(locale.languageCode ?? "N/A")
100 | TimeZone: \(timeZone.identifier)
101 | InterfaceStyle: \(userInterfaceStyle)
102 | SizeCategory: \(sizeCategory.rawValue)
103 | """
104 | }
105 | }
106 |
107 | extension ViewController: UIEnvironmentUpdating {
108 | func updateEnvironment() {
109 | label.text = labelText()
110 | }
111 | }
112 |
113 | extension UIUserInterfaceStyle: CustomStringConvertible {
114 | public var description: String {
115 | switch self {
116 | case .dark:
117 | return "dark"
118 | case .light:
119 | return "light"
120 | case .unspecified:
121 | return "unspecified"
122 | @unknown default:
123 | return "unkown"
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/UIEnvironmentValues.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A collection of environment values propagated through a view/view controller hierarchy.
4 | ///
5 | /// UIEnvironmentValues exposes a collection of values to your app's views in an
6 | /// `UIEnvironmentValues` structure. To read a value from the structure,
7 | /// declare a property using the ``UIEnvironment`` property wrapper and
8 | /// specify the value's key path. For example, you can read the current locale:
9 | ///
10 | /// @UIEnvironment(\.locale) var locale: Locale
11 | ///
12 | /// Use the property you declare to dynamically control a view's layout.
13 | /// UIEnvironmentValues automatically sets or updates many environment values, like
14 | /// ``UIEnvironmentValues/locale``, based on device characteristics, system state,
15 | /// or user settings.
16 | ///
17 | /// You can set or override some values using the ``UIEnvironmentable/environment(_:_:)``
18 | /// view modifier:
19 | ///
20 | /// MyView()
21 | /// .environment(\.locale, .init(identifier: "pl"))
22 | ///
23 | /// The value that you set affects the environment for the view that you modify
24 | /// --- including its descendants in the view hierarchy --- but only up to the
25 | /// point where you apply a different environment modifier.
26 | ///
27 | /// Create custom environment values by defining a type that
28 | /// conforms to the ``UIEnvironmentKey`` protocol, and then extending the
29 | /// environment values structure with a new property. Use your key to get and
30 | /// set the value, and provide a dedicated modifier for clients to use when
31 | /// setting the value:
32 | ///
33 | /// private struct MyEnvironmentKey: UIEnvironmentKey {
34 | /// static let defaultValue: String = "Default value"
35 | /// }
36 | ///
37 | /// extension UIEnvironmentValues {
38 | /// var myCustomValue: String {
39 | /// get { self[MyEnvironmentKey.self] }
40 | /// set { self[MyEnvironmentKey.self] = newValue }
41 | /// }
42 | /// }
43 | ///
44 | /// Clients of your value then access the value in the usual way, reading it
45 | /// with the ``UIEnvironment`` property wrapper, and setting it
46 | /// using the ``UIEnvironmentable/environment(_:_:)`` convenience method.
47 | public struct UIEnvironmentValues {
48 | private var values: [ObjectIdentifier: Any] = [:]
49 |
50 | /// Creates an environment values instance.
51 | ///
52 | /// You don't typically create an instance of ``UIEnvironmentValues``
53 | /// directly. Doing so would provide access only to default values that
54 | /// don't update based on system settings or device characteristics.
55 | /// Instead, you rely on an environment values' instance
56 | /// that UIEnvironment framework manages for you when you use the ``UIEnvironment``
57 | /// property wrapper and the ``UIEnvironmentable/environment(_:_:)`` convenience method.
58 | public init() {}
59 |
60 | /// Accesses the environment value associated with a custom key.
61 | ///
62 | /// Create custom environment values by defining a key
63 | /// that conforms to the ``UIEnvironmentKey`` protocol, and then using that
64 | /// key with the subscript operator of the ``UIEnvironmentValues`` structure
65 | /// to get and set a value for that key:
66 | ///
67 | /// private struct MyEnvironmentKey: UIEnvironmentKey {
68 | /// static let defaultValue: String = "Default value"
69 | /// }
70 | ///
71 | /// extension UIEnvironmentValues {
72 | /// var myCustomValue: String {
73 | /// get { self[MyEnvironmentKey.self] }
74 | /// set { self[MyEnvironmentKey.self] = newValue }
75 | /// }
76 | /// }
77 | ///
78 | /// You use custom environment values the same way you use system-provided
79 | /// values, setting a value with the ``UIEnvironmentable/environment(_:_:)`` convenience
80 | /// method, and reading values with the ``UIEnvironment`` property wrapper.
81 | public subscript(key: K.Type) -> K.Value where K: UIEnvironmentKey {
82 | get {
83 | if let value = self.values[ObjectIdentifier(key)] as? K.Value {
84 | return value
85 | } else {
86 | self.noValueFound?()
87 | return key.defaultValue
88 | }
89 | }
90 | set {
91 | self.values[ObjectIdentifier(key)] = newValue
92 | }
93 | }
94 |
95 | // MARK: Helpers
96 | private var noValueFound: (() -> Void)?
97 |
98 | internal mutating func hasValue(for keyPath: KeyPath) -> Bool {
99 | var hasValue = true
100 | self.noValueFound = { hasValue = false }
101 | let _ = self[keyPath: keyPath]
102 | self.noValueFound = nil
103 | return hasValue
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Sources/UIEnvironment/Predefined Environment Keys/UIUserInterfaceStyleEnvironmentKey.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | private enum UIUserInterfaceStyleEnvironmentKey: UIEnvironmentKey {
4 | static let defaultValue: UIUserInterfaceStyle = UITraitCollection.current.userInterfaceStyle
5 | }
6 |
7 | extension UIEnvironmentValues {
8 |
9 | /// The user interface style of this environment.
10 | ///
11 | /// Read this environment value from within a view to find out if UIKit
12 | /// is currently displaying the view using the `light` or
13 | /// `dark` appearance. The value that you receive depends on
14 | /// whether the user has enabled Dark Mode, possibly superseded by
15 | /// the configuration of the current presentation's view hierarchy.
16 | ///
17 | /// ```swift
18 | /// @Environment(\.userInterfaceStyle) private var userInterfaceStyle
19 | ///
20 | /// if userInterfaceStyle == .dark {
21 | /// DarkContent()
22 | /// } else {
23 | /// LightContent()
24 | /// }
25 | /// ```
26 | ///
27 | /// You can set the `userInterfaceStyle` environment value directly,
28 | /// using ``UIEnvironmentable/environment(_:_:)`` convenience method,
29 | /// but that usually isn't what you want. Doing so changes the user
30 | /// interface style of the given view and its child views but *not* the views
31 | /// above it in the view hierarchy. Instead, set a color scheme using the
32 | /// ``UIEnvironmentable/preferredUserInterfaceStyle(_:)`` modifier, which
33 | /// also propagates the value up through the view hierarchy
34 | /// to the enclosing presentation, like a sheet or a window.
35 | public var userInterfaceStyle: UIUserInterfaceStyle {
36 | get { self[UIUserInterfaceStyleEnvironmentKey.self] }
37 | set { self[UIUserInterfaceStyleEnvironmentKey.self] = newValue }
38 | }
39 | }
40 |
41 | extension UIEnvironmentable {
42 |
43 | /// Sets the preferred user interface style for this presentation.
44 | ///
45 | /// Use one of the values in `UIUserInterfaceStyle` with this modifier to set a
46 | /// preferred user interface style for the nearest enclosing presentation, like a
47 | /// view controller or a window. The value that you set overrides the
48 | /// user's Dark Mode selection for that presentation.
49 | ///
50 | /// If you apply the modifier to any of the views in the view controller
51 | /// the value that you set propagates up through the view hierarchy to the enclosing
52 | /// presentation, or until another color scheme modifier higher in the
53 | /// hierarchy overrides it. The value you set also flows down to all child
54 | /// views of the enclosing presentation.
55 | ///
56 | /// If you need to detect the user interface style that currently applies to a view,
57 | /// read the ``UIEnvironmentValues/userInterfaceStyle`` environment value:
58 | ///
59 | /// ```swift
60 | /// @Environment(\.userInterfaceStyle) private var userInterfaceStyle
61 | ///
62 | /// if userInterfaceStyle == .dark {
63 | /// DarkContent()
64 | /// } else {
65 | /// LightContent()
66 | /// }
67 | /// ```
68 | ///
69 | /// - Parameter userInterfaceStyle: The preferred user interface style for this view.
70 | public func preferredUserInterfaceStyle(_ userInterfaceStyle: UIUserInterfaceStyle) {
71 | var prevResponder: UIResponder?
72 |
73 | traverseBottomUp { responder in
74 | defer { prevResponder = responder }
75 |
76 | if responder.valueInSelf(forKeyPath: \.userInterfaceStyle) != nil {
77 | (prevResponder as? UIEnvironmentable)?.environment(\.userInterfaceStyle, userInterfaceStyle)
78 | return true
79 | }
80 |
81 | if responder is UIViewController || responder is UIWindow,
82 | let object = responder as? UIEnvironmentable {
83 | object.environment(\.userInterfaceStyle, userInterfaceStyle)
84 | return true
85 | }
86 | return false
87 | }
88 |
89 | environment(\.userInterfaceStyle, userInterfaceStyle)
90 | }
91 | }
92 |
93 | import SwiftHook
94 |
95 | extension UIScreen {
96 | public static let setupTraitCollectionListener: Void = {
97 | _ = try? hookBefore(
98 | targetClass: UIScreen.self,
99 | selector: #selector(UIScreen.traitCollectionDidChange(_:)),
100 | closure: {
101 | UIApplication.shared.forEachWindow {
102 | $0.environment(\.userInterfaceStyle, UITraitCollection.current.userInterfaceStyle)
103 | }
104 | }
105 | )
106 | }()
107 |
108 | @objc private func swizzled_traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
109 | swizzled_traitCollectionDidChange(previousTraitCollection)
110 | UIApplication.shared.forEachWindow {
111 | $0.environment(\.userInterfaceStyle, UITraitCollection.current.userInterfaceStyle)
112 | }
113 | }
114 | }
115 |
116 | extension UIWindow {
117 | public static let setupOverrideUserInterfaceListener: Void = {
118 | _ = try? hookBefore(
119 | targetClass: UIWindow.self,
120 | selector: #selector(setter: UIWindow.overrideUserInterfaceStyle),
121 | closure: { object, sel, style in
122 | object.environment(\.userInterfaceStyle, style)
123 | } as @convention(block) (UIWindow, Selector, UIUserInterfaceStyle) -> Void
124 | )
125 | }()
126 | }
127 |
--------------------------------------------------------------------------------
/XcodeProject/Tests/UIEnvironmentValuesTests/UIEnvironmentValuesTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIEnvironment
3 | import UIKit
4 | import XCTest
5 |
6 | final class UIEnvironmentValuesTests: XCTestCase {
7 | func test_view_defaultValue() {
8 | let view = makeView()
9 |
10 | XCTAssertEqual(view.locale, Locale.current)
11 | }
12 |
13 | func test_viewController_defaultValue() {
14 | let vc = makeVC()
15 |
16 | XCTAssertEqual(vc.locale, Locale.current)
17 | }
18 |
19 | func test_view_environment() {
20 | let view = makeView()
21 |
22 | view.environment(\.locale, "pl")
23 |
24 | XCTAssertEqual(view.locale, "pl")
25 | }
26 |
27 | func test_viewController_environment() {
28 | let vc = makeVC()
29 |
30 | vc.environment(\.locale, "se")
31 |
32 | XCTAssertEqual(vc.locale, "se")
33 | }
34 |
35 | func test_flat_hierarchy() {
36 | let root = makeView()
37 | let child1 = makeView()
38 | let child2 = makeView()
39 |
40 | root.addSubview(child1)
41 | root.addSubview(child2)
42 |
43 | root.environment(\.locale, "pl")
44 |
45 | XCTAssertEqual(root.locale, "pl")
46 | XCTAssertEqual(child1.locale, "pl")
47 | XCTAssertEqual(child2.locale, "pl")
48 | }
49 |
50 | func test_deep_view_hierarchy() {
51 | let root = makeView()
52 | let child1 = makeView()
53 | let child2 = makeView()
54 |
55 | root.addSubview(child1)
56 | child1.addSubview(child2)
57 |
58 | root.environment(\.locale, "pl")
59 |
60 | XCTAssertEqual(root.locale, "pl")
61 | XCTAssertEqual(child1.locale, "pl")
62 | XCTAssertEqual(child2.locale, "pl")
63 | }
64 |
65 | func test_viewControllers_andView_environment() {
66 | let vc = makeVC()
67 | let view = makeView()
68 |
69 | vc.view.addSubview(view)
70 |
71 | vc.environment(\.locale, "se")
72 |
73 | XCTAssertEqual(vc.locale, "se")
74 | XCTAssertEqual(view.locale, "se")
75 | }
76 |
77 | func test_childViewControllers() {
78 | let vc = makeVC()
79 | let child1VC = makeVC()
80 | let child2VC = makeVC()
81 |
82 | vc.addChildViewController(child1VC)
83 | child1VC.addChildViewController(child2VC)
84 |
85 | vc.environment(\.locale, "se")
86 |
87 | XCTAssertEqual(vc.locale, "se")
88 | XCTAssertEqual(child1VC.locale, "se")
89 | XCTAssertEqual(child2VC.locale, "se")
90 | }
91 |
92 | func test_present_viewController() {
93 | let vc1 = makeVC()
94 | let vc2 = makeVC()
95 | putInViewHierarchy(vc1)
96 |
97 | vc1.environment(\.locale, "pl")
98 |
99 | presentViewController(vc2, on: vc1)
100 | dismissViewController(vc1)
101 | }
102 |
103 | func test_navigationController() {
104 | let vc1 = makeVC()
105 | let vc2 = makeVC()
106 |
107 | let nav = UINavigationController()
108 | nav.viewControllers = [vc1, vc2]
109 | putInViewHierarchy(nav)
110 |
111 | nav.environment(\.locale, "pl")
112 |
113 | XCTAssertEqual(vc1.locale, "pl")
114 | XCTAssertEqual(vc2.locale, "pl")
115 |
116 | nav.viewControllers = [vc1]
117 |
118 | XCTAssertEqual(vc1.locale, "pl")
119 |
120 | nav.viewControllers = []
121 | }
122 |
123 | func test_do_not_override_child_environment() {
124 | let view = makeView()
125 | let childView = makeView()
126 | view.addSubview(childView)
127 |
128 | childView.environment(\.locale, "pl")
129 |
130 | XCTAssertEqual(view.locale, Locale.current)
131 | XCTAssertEqual(childView.locale, "pl")
132 |
133 | view.environment(\.locale, "uk")
134 |
135 | XCTAssertEqual(view.locale, "uk")
136 | XCTAssertEqual(childView.locale, "pl")
137 | }
138 |
139 | func test_environmentUpdating() {
140 | let view = makeView()
141 | let childView = makeView()
142 | view.addSubview(childView)
143 |
144 | var changeCount = 0
145 | childView.onEnvironmentChange = { changeCount += 1 }
146 |
147 | view.environment(\.locale, "pl")
148 |
149 | XCTAssertEqual(childView.locale, "pl")
150 | XCTAssertEqual(changeCount, 1)
151 |
152 | view.environment(\.locale, "uk")
153 |
154 | XCTAssertEqual(childView.locale, "uk")
155 | XCTAssertEqual(changeCount, 2)
156 |
157 | childView.environment(\.locale, "fr")
158 |
159 | XCTAssertEqual(childView.locale, "fr")
160 | XCTAssertEqual(changeCount, 3)
161 | }
162 |
163 | func test_multiple_environemntValues_changes() {
164 | let view = makeView()
165 | let childView = makeView()
166 | view.addSubview(childView)
167 |
168 | XCTAssertEqual(view.locale.languageCode, Locale.current.languageCode)
169 | XCTAssertEqual(childView.locale.languageCode, Locale.current.languageCode)
170 |
171 | view.environment(\.locale, "pl")
172 |
173 | XCTAssertEqual(view.locale, "pl", "precondition")
174 | XCTAssertEqual(childView.locale, "pl", "precondition")
175 | XCTAssertEqual(view.calendar, .current, "precondition")
176 | XCTAssertEqual(childView.calendar, .current, "precondition")
177 |
178 | childView.environment(\.locale, "fr")
179 | view.environment(\.calendar, .init(identifier: .indian))
180 |
181 | XCTAssertEqual(view.locale, "pl")
182 | XCTAssertEqual(childView.locale, "fr")
183 | XCTAssertEqual(view.calendar, .init(identifier: .indian))
184 | XCTAssertEqual(childView.calendar, .init(identifier: .indian))
185 | }
186 |
187 | // MARK: Helpers
188 | private func makeView(file: StaticString = #file, line: UInt = #line) -> EnvView {
189 | let view = EnvView()
190 | trackForMemoryLeaks(view, file: file, line: line)
191 | return view
192 | }
193 |
194 | private func makeVC(file: StaticString = #file, line: UInt = #line) -> EnvViewController {
195 | let vc = EnvViewController()
196 | trackForMemoryLeaks(vc, file: file, line: line)
197 | return vc
198 | }
199 |
200 | private final class EnvView: UIView, UIEnvironmentUpdating {
201 | @UIEnvironment(\.locale) var locale: Locale
202 | @UIEnvironment(\.calendar) var calendar: Calendar
203 | var onEnvironmentChange: (() -> Void)?
204 |
205 | init() {
206 | super.init(frame: .zero)
207 | }
208 |
209 | @available(*, unavailable)
210 | required init?(coder: NSCoder) {
211 | fatalError("init(coder:) has not been implemented")
212 | }
213 |
214 | func updateEnvironment() {
215 | onEnvironmentChange?()
216 | }
217 | }
218 |
219 | private final class EnvViewController: UIViewController {
220 | @UIEnvironment(\.locale) var locale: Locale
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/XcodeProject/Tests/UIEnvironmentValuesTests/UIEnvironmentUpdatingTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIEnvironment
3 | import UIKit
4 | import XCTest
5 |
6 | final class UIEnvironmentUpdatingTests: XCTestCase {
7 | func test_environmentValueChange_traverseViewHierarchy_shouldCallUpdates() {
8 | var counter: [String: Int] = [:]
9 | func incrementCount(for key: String) {
10 | counter[key, default: 0] += 1
11 | }
12 |
13 | func makeViewHierarchy(prefix: String) -> UITabBarController {
14 | let tabVC = UITabBarController()
15 | tabVC.viewControllers = [
16 | makeNavigationController(
17 | EnvViewController(
18 | onUpdateEnvironment: {
19 | incrementCount(for: "\(prefix)vc1")
20 | },
21 | envView: nil
22 | ),
23 | makeViewController()
24 | ),
25 | makeNavigationController(
26 | makeViewController(),
27 | EnvViewController(
28 | onUpdateEnvironment: {
29 | incrementCount(for: "\(prefix)vc2")
30 | },
31 | envView: EnvView {
32 | UIView()
33 | EnvView {
34 | EnvView().then {
35 | $0.onUpdateEnvironment = {
36 | incrementCount(for: "\(prefix)vc2-view-subview1-subview2")
37 | }
38 | }
39 | }.then {
40 | $0.onUpdateEnvironment = {
41 | incrementCount(for: "\(prefix)vc2-view-subview1")
42 | }
43 | }
44 | }.then {
45 | $0.onUpdateEnvironment = {
46 | incrementCount(for: "\(prefix)vc2-view")
47 | }
48 | }
49 | )
50 | )
51 | ]
52 | return tabVC
53 | }
54 | let tabVC = makeViewHierarchy(prefix: "")
55 | let window = putInViewHierarchy(tabVC)
56 | let modalTabVC = makeViewHierarchy(prefix: "modal-")
57 | presentViewController(modalTabVC, on: tabVC)
58 |
59 | window.overrideUserInterfaceStyle = .dark
60 |
61 | XCTAssertEqual(counter["vc1"], 1)
62 | XCTAssertEqual(counter["vc2"], 1)
63 | XCTAssertEqual(counter["vc2-view"], 1)
64 | XCTAssertEqual(counter["vc2-view-subview1"], 1)
65 | XCTAssertEqual(counter["vc2-view-subview1-subview2"], 1)
66 | XCTAssertEqual(counter["modal-vc1"], 1)
67 | XCTAssertEqual(counter["modal-vc2"], 1)
68 | XCTAssertEqual(counter["modal-vc2-view"], 1)
69 | XCTAssertEqual(counter["modal-vc2-view-subview1"], 1)
70 | XCTAssertEqual(counter["modal-vc2-view-subview1-subview2"], 1)
71 |
72 | modalTabVC.environment(\.locale, "pl")
73 |
74 | XCTAssertEqual(counter["vc1"], 1)
75 | XCTAssertEqual(counter["vc2"], 1)
76 | XCTAssertEqual(counter["vc2-view"], 1)
77 | XCTAssertEqual(counter["vc2-view-subview1"], 1)
78 | XCTAssertEqual(counter["vc2-view-subview1-subview2"], 1)
79 | XCTAssertEqual(counter["modal-vc1"], 2)
80 | XCTAssertEqual(counter["modal-vc2"], 2)
81 | XCTAssertEqual(counter["modal-vc2-view"], 2)
82 | XCTAssertEqual(counter["modal-vc2-view-subview1"], 2)
83 | XCTAssertEqual(counter["modal-vc2-view-subview1-subview2"], 2)
84 |
85 | let modal_vc2_view = modalTabVC.children[1].children[1].view!
86 | modal_vc2_view.environment(\.calendar, .init(identifier: .iso8601))
87 |
88 | XCTAssertEqual(counter["vc1"], 1)
89 | XCTAssertEqual(counter["vc2"], 1)
90 | XCTAssertEqual(counter["vc2-view"], 1)
91 | XCTAssertEqual(counter["vc2-view-subview1"], 1)
92 | XCTAssertEqual(counter["vc2-view-subview1-subview2"], 1)
93 | XCTAssertEqual(counter["modal-vc1"], 2)
94 | XCTAssertEqual(counter["modal-vc2"], 2)
95 | XCTAssertEqual(counter["modal-vc2-view"], 3)
96 | XCTAssertEqual(counter["modal-vc2-view-subview1"], 3)
97 | XCTAssertEqual(counter["modal-vc2-view-subview1-subview2"], 3)
98 | }
99 |
100 | // MARK: Helpers
101 | private func makeNavigationController(_ viewControllers: UIViewController...) -> UINavigationController {
102 | let nav = UINavigationController()
103 | nav.viewControllers = viewControllers
104 | return nav
105 | }
106 |
107 | private func makeViewController() -> UIViewController {
108 | return UIViewController()
109 | }
110 |
111 | private final class EnvView: UIView, UIEnvironmentUpdating {
112 | @UIEnvironment(\.userInterfaceStyle) var userInterfaceStyle
113 |
114 | var onUpdateEnvironment: (() -> Void)?
115 | func updateEnvironment() {
116 | onUpdateEnvironment?()
117 | }
118 | }
119 |
120 | private final class EnvViewController: UIViewController, UIEnvironmentUpdating {
121 | @UIEnvironment(\.userInterfaceStyle) var userInterfaceStyle
122 | var envView: EnvView?
123 |
124 | init(onUpdateEnvironment: (() -> Void)? = nil, envView: EnvView? = nil) {
125 | self.envView = envView
126 | self.onUpdateEnvironment = onUpdateEnvironment
127 |
128 | super.init(nibName: nil, bundle: nil)
129 | }
130 |
131 | @available(*, unavailable)
132 | required init?(coder: NSCoder) {
133 | fatalError("init(coder:) has not been implemented")
134 | }
135 |
136 | override func loadView() {
137 | guard let envView = envView else {
138 | super.loadView()
139 | return
140 | }
141 | view = envView
142 | }
143 |
144 | var onUpdateEnvironment: (() -> Void)?
145 | func updateEnvironment() {
146 | onUpdateEnvironment?()
147 | }
148 | }
149 | }
150 |
151 | extension UIView {
152 | @resultBuilder
153 | fileprivate enum ViewBuilder {
154 | public static func buildBlock(_ views: UIView...) -> [UIView] {
155 | return views
156 | }
157 |
158 | public static func buildBlock(_ views: [UIView]...) -> [UIView] {
159 | return views.flatMap { $0 }
160 | }
161 |
162 | public static func buildArray(_ view: [[UIView]]) -> [UIView] {
163 | return view.flatMap { $0 }
164 | }
165 | }
166 |
167 | fileprivate convenience init(@ViewBuilder views: () -> [UIView]) {
168 | self.init(frame: .infinite)
169 | translatesAutoresizingMaskIntoConstraints = false
170 | views().forEach(addSubview)
171 | }
172 | }
173 |
174 | private protocol Configurable {}
175 | extension UIResponder: Configurable {}
176 | extension Configurable {
177 | @discardableResult
178 | fileprivate func then(_ setup: (Self) -> Void) -> Self {
179 | setup(self)
180 | return self
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/XcodeProject/UIEnvironment.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | C8720AFC283422F00046EB6A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8720AF6283422F00046EB6A /* ViewController.swift */; };
11 | C8720AFD283422F00046EB6A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8720AF7283422F00046EB6A /* Assets.xcassets */; };
12 | C8720AFE283422F00046EB6A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C8720AF8283422F00046EB6A /* LaunchScreen.storyboard */; };
13 | C8720AFF283422F00046EB6A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8720AFA283422F00046EB6A /* AppDelegate.swift */; };
14 | C8720B0C283422FA0046EB6A /* UIUserInterfaceStyleEnvironmentKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8720B04283422FA0046EB6A /* UIUserInterfaceStyleEnvironmentKeyTests.swift */; };
15 | C8720B0D283422FA0046EB6A /* UIEnvironmentUpdatingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8720B05283422FA0046EB6A /* UIEnvironmentUpdatingTests.swift */; };
16 | C8720B0E283422FA0046EB6A /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8720B07283422FA0046EB6A /* UIViewController+Extensions.swift */; };
17 | C8720B0F283422FA0046EB6A /* XCTestCase+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8720B08283422FA0046EB6A /* XCTestCase+Extensions.swift */; };
18 | C8720B10283422FA0046EB6A /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8720B09283422FA0046EB6A /* Helpers.swift */; };
19 | C8720B11283422FA0046EB6A /* Locale+ExpressibleByStringLiteral.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8720B0A283422FA0046EB6A /* Locale+ExpressibleByStringLiteral.swift */; };
20 | C8720B12283422FA0046EB6A /* UIEnvironmentValuesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8720B0B283422FA0046EB6A /* UIEnvironmentValuesTests.swift */; };
21 | C8D138C028342750000BDBE9 /* UIEnvironment in Frameworks */ = {isa = PBXBuildFile; productRef = C8D138BF28342750000BDBE9 /* UIEnvironment */; };
22 | C8E0DBB628376CB20022CF69 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8E0DBB528376CB20022CF69 /* View.swift */; };
23 | C8E0DBB828376CE00022CF69 /* MyEnvironmentKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8E0DBB728376CE00022CF69 /* MyEnvironmentKey.swift */; };
24 | /* End PBXBuildFile section */
25 |
26 | /* Begin PBXContainerItemProxy section */
27 | C8720AD92834220B0046EB6A /* PBXContainerItemProxy */ = {
28 | isa = PBXContainerItemProxy;
29 | containerPortal = C8720ABA2834220A0046EB6A /* Project object */;
30 | proxyType = 1;
31 | remoteGlobalIDString = C8720AC12834220A0046EB6A;
32 | remoteInfo = UIEnvironment;
33 | };
34 | /* End PBXContainerItemProxy section */
35 |
36 | /* Begin PBXFileReference section */
37 | C8720AC22834220A0046EB6A /* UIEnvironmentApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UIEnvironmentApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
38 | C8720AD82834220B0046EB6A /* UIEnvironmentTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIEnvironmentTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
39 | C8720AF6283422F00046EB6A /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
40 | C8720AF7283422F00046EB6A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
41 | C8720AF9283422F00046EB6A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
42 | C8720AFA283422F00046EB6A /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
43 | C8720AFB283422F00046EB6A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
44 | C8720B04283422FA0046EB6A /* UIUserInterfaceStyleEnvironmentKeyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIUserInterfaceStyleEnvironmentKeyTests.swift; sourceTree = ""; };
45 | C8720B05283422FA0046EB6A /* UIEnvironmentUpdatingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIEnvironmentUpdatingTests.swift; sourceTree = ""; };
46 | C8720B07283422FA0046EB6A /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = ""; };
47 | C8720B08283422FA0046EB6A /* XCTestCase+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Extensions.swift"; sourceTree = ""; };
48 | C8720B09283422FA0046EB6A /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; };
49 | C8720B0A283422FA0046EB6A /* Locale+ExpressibleByStringLiteral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Locale+ExpressibleByStringLiteral.swift"; sourceTree = ""; };
50 | C8720B0B283422FA0046EB6A /* UIEnvironmentValuesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIEnvironmentValuesTests.swift; sourceTree = ""; };
51 | C8720B162834243C0046EB6A /* UIEnvironment */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = UIEnvironment; path = ..; sourceTree = ""; };
52 | C8E0DBB528376CB20022CF69 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; };
53 | C8E0DBB728376CE00022CF69 /* MyEnvironmentKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyEnvironmentKey.swift; sourceTree = ""; };
54 | /* End PBXFileReference section */
55 |
56 | /* Begin PBXFrameworksBuildPhase section */
57 | C8720ABF2834220A0046EB6A /* Frameworks */ = {
58 | isa = PBXFrameworksBuildPhase;
59 | buildActionMask = 2147483647;
60 | files = (
61 | C8D138C028342750000BDBE9 /* UIEnvironment in Frameworks */,
62 | );
63 | runOnlyForDeploymentPostprocessing = 0;
64 | };
65 | C8720AD52834220B0046EB6A /* Frameworks */ = {
66 | isa = PBXFrameworksBuildPhase;
67 | buildActionMask = 2147483647;
68 | files = (
69 | );
70 | runOnlyForDeploymentPostprocessing = 0;
71 | };
72 | /* End PBXFrameworksBuildPhase section */
73 |
74 | /* Begin PBXGroup section */
75 | C8720AB92834220A0046EB6A = {
76 | isa = PBXGroup;
77 | children = (
78 | C8720B162834243C0046EB6A /* UIEnvironment */,
79 | C8720AF5283422F00046EB6A /* App */,
80 | C8720AC32834220A0046EB6A /* Products */,
81 | C8720B01283422FA0046EB6A /* Tests */,
82 | C8E8B2D62834253900180432 /* Frameworks */,
83 | );
84 | sourceTree = "";
85 | };
86 | C8720AC32834220A0046EB6A /* Products */ = {
87 | isa = PBXGroup;
88 | children = (
89 | C8720AC22834220A0046EB6A /* UIEnvironmentApp.app */,
90 | C8720AD82834220B0046EB6A /* UIEnvironmentTests.xctest */,
91 | );
92 | name = Products;
93 | sourceTree = "";
94 | };
95 | C8720AF5283422F00046EB6A /* App */ = {
96 | isa = PBXGroup;
97 | children = (
98 | C8720AFB283422F00046EB6A /* Info.plist */,
99 | C8720AFA283422F00046EB6A /* AppDelegate.swift */,
100 | C8E0DBB728376CE00022CF69 /* MyEnvironmentKey.swift */,
101 | C8E0DBB528376CB20022CF69 /* View.swift */,
102 | C8720AF6283422F00046EB6A /* ViewController.swift */,
103 | C8720AF7283422F00046EB6A /* Assets.xcassets */,
104 | C8720AF8283422F00046EB6A /* LaunchScreen.storyboard */,
105 | );
106 | path = App;
107 | sourceTree = "";
108 | };
109 | C8720B01283422FA0046EB6A /* Tests */ = {
110 | isa = PBXGroup;
111 | children = (
112 | C8720B02283422FA0046EB6A /* UIEnvironmentValuesTests */,
113 | );
114 | path = Tests;
115 | sourceTree = "";
116 | };
117 | C8720B02283422FA0046EB6A /* UIEnvironmentValuesTests */ = {
118 | isa = PBXGroup;
119 | children = (
120 | C8720B05283422FA0046EB6A /* UIEnvironmentUpdatingTests.swift */,
121 | C8720B0B283422FA0046EB6A /* UIEnvironmentValuesTests.swift */,
122 | C8720B06283422FA0046EB6A /* Helpers */,
123 | C8720B03283422FA0046EB6A /* Predefined Environment Keys Tests */,
124 | );
125 | path = UIEnvironmentValuesTests;
126 | sourceTree = "";
127 | };
128 | C8720B03283422FA0046EB6A /* Predefined Environment Keys Tests */ = {
129 | isa = PBXGroup;
130 | children = (
131 | C8720B04283422FA0046EB6A /* UIUserInterfaceStyleEnvironmentKeyTests.swift */,
132 | );
133 | path = "Predefined Environment Keys Tests";
134 | sourceTree = "";
135 | };
136 | C8720B06283422FA0046EB6A /* Helpers */ = {
137 | isa = PBXGroup;
138 | children = (
139 | C8720B09283422FA0046EB6A /* Helpers.swift */,
140 | C8720B0A283422FA0046EB6A /* Locale+ExpressibleByStringLiteral.swift */,
141 | C8720B07283422FA0046EB6A /* UIViewController+Extensions.swift */,
142 | C8720B08283422FA0046EB6A /* XCTestCase+Extensions.swift */,
143 | );
144 | path = Helpers;
145 | sourceTree = "";
146 | };
147 | C8E8B2D62834253900180432 /* Frameworks */ = {
148 | isa = PBXGroup;
149 | children = (
150 | );
151 | name = Frameworks;
152 | sourceTree = "";
153 | };
154 | /* End PBXGroup section */
155 |
156 | /* Begin PBXNativeTarget section */
157 | C8720AC12834220A0046EB6A /* UIEnvironmentApp */ = {
158 | isa = PBXNativeTarget;
159 | buildConfigurationList = C8720AEC2834220B0046EB6A /* Build configuration list for PBXNativeTarget "UIEnvironmentApp" */;
160 | buildPhases = (
161 | C8720ABE2834220A0046EB6A /* Sources */,
162 | C8720ABF2834220A0046EB6A /* Frameworks */,
163 | C8720AC02834220A0046EB6A /* Resources */,
164 | );
165 | buildRules = (
166 | );
167 | dependencies = (
168 | );
169 | name = UIEnvironmentApp;
170 | packageProductDependencies = (
171 | C8D138BF28342750000BDBE9 /* UIEnvironment */,
172 | );
173 | productName = UIEnvironment;
174 | productReference = C8720AC22834220A0046EB6A /* UIEnvironmentApp.app */;
175 | productType = "com.apple.product-type.application";
176 | };
177 | C8720AD72834220B0046EB6A /* UIEnvironmentTests */ = {
178 | isa = PBXNativeTarget;
179 | buildConfigurationList = C8720AEF2834220B0046EB6A /* Build configuration list for PBXNativeTarget "UIEnvironmentTests" */;
180 | buildPhases = (
181 | C8720AD42834220B0046EB6A /* Sources */,
182 | C8720AD52834220B0046EB6A /* Frameworks */,
183 | C8720AD62834220B0046EB6A /* Resources */,
184 | );
185 | buildRules = (
186 | );
187 | dependencies = (
188 | C8720ADA2834220B0046EB6A /* PBXTargetDependency */,
189 | );
190 | name = UIEnvironmentTests;
191 | productName = UIEnvironmentTests;
192 | productReference = C8720AD82834220B0046EB6A /* UIEnvironmentTests.xctest */;
193 | productType = "com.apple.product-type.bundle.unit-test";
194 | };
195 | /* End PBXNativeTarget section */
196 |
197 | /* Begin PBXProject section */
198 | C8720ABA2834220A0046EB6A /* Project object */ = {
199 | isa = PBXProject;
200 | attributes = {
201 | BuildIndependentTargetsInParallel = 1;
202 | LastSwiftUpdateCheck = 1330;
203 | LastUpgradeCheck = 1330;
204 | TargetAttributes = {
205 | C8720AC12834220A0046EB6A = {
206 | CreatedOnToolsVersion = 13.3.1;
207 | };
208 | C8720AD72834220B0046EB6A = {
209 | CreatedOnToolsVersion = 13.3.1;
210 | TestTargetID = C8720AC12834220A0046EB6A;
211 | };
212 | };
213 | };
214 | buildConfigurationList = C8720ABD2834220A0046EB6A /* Build configuration list for PBXProject "UIEnvironment" */;
215 | compatibilityVersion = "Xcode 13.0";
216 | developmentRegion = en;
217 | hasScannedForEncodings = 0;
218 | knownRegions = (
219 | en,
220 | Base,
221 | );
222 | mainGroup = C8720AB92834220A0046EB6A;
223 | productRefGroup = C8720AC32834220A0046EB6A /* Products */;
224 | projectDirPath = "";
225 | projectRoot = "";
226 | targets = (
227 | C8720AC12834220A0046EB6A /* UIEnvironmentApp */,
228 | C8720AD72834220B0046EB6A /* UIEnvironmentTests */,
229 | );
230 | };
231 | /* End PBXProject section */
232 |
233 | /* Begin PBXResourcesBuildPhase section */
234 | C8720AC02834220A0046EB6A /* Resources */ = {
235 | isa = PBXResourcesBuildPhase;
236 | buildActionMask = 2147483647;
237 | files = (
238 | C8720AFD283422F00046EB6A /* Assets.xcassets in Resources */,
239 | C8720AFE283422F00046EB6A /* LaunchScreen.storyboard in Resources */,
240 | );
241 | runOnlyForDeploymentPostprocessing = 0;
242 | };
243 | C8720AD62834220B0046EB6A /* Resources */ = {
244 | isa = PBXResourcesBuildPhase;
245 | buildActionMask = 2147483647;
246 | files = (
247 | );
248 | runOnlyForDeploymentPostprocessing = 0;
249 | };
250 | /* End PBXResourcesBuildPhase section */
251 |
252 | /* Begin PBXSourcesBuildPhase section */
253 | C8720ABE2834220A0046EB6A /* Sources */ = {
254 | isa = PBXSourcesBuildPhase;
255 | buildActionMask = 2147483647;
256 | files = (
257 | C8720AFF283422F00046EB6A /* AppDelegate.swift in Sources */,
258 | C8E0DBB828376CE00022CF69 /* MyEnvironmentKey.swift in Sources */,
259 | C8720AFC283422F00046EB6A /* ViewController.swift in Sources */,
260 | C8E0DBB628376CB20022CF69 /* View.swift in Sources */,
261 | );
262 | runOnlyForDeploymentPostprocessing = 0;
263 | };
264 | C8720AD42834220B0046EB6A /* Sources */ = {
265 | isa = PBXSourcesBuildPhase;
266 | buildActionMask = 2147483647;
267 | files = (
268 | C8720B0F283422FA0046EB6A /* XCTestCase+Extensions.swift in Sources */,
269 | C8720B0D283422FA0046EB6A /* UIEnvironmentUpdatingTests.swift in Sources */,
270 | C8720B12283422FA0046EB6A /* UIEnvironmentValuesTests.swift in Sources */,
271 | C8720B11283422FA0046EB6A /* Locale+ExpressibleByStringLiteral.swift in Sources */,
272 | C8720B0E283422FA0046EB6A /* UIViewController+Extensions.swift in Sources */,
273 | C8720B10283422FA0046EB6A /* Helpers.swift in Sources */,
274 | C8720B0C283422FA0046EB6A /* UIUserInterfaceStyleEnvironmentKeyTests.swift in Sources */,
275 | );
276 | runOnlyForDeploymentPostprocessing = 0;
277 | };
278 | /* End PBXSourcesBuildPhase section */
279 |
280 | /* Begin PBXTargetDependency section */
281 | C8720ADA2834220B0046EB6A /* PBXTargetDependency */ = {
282 | isa = PBXTargetDependency;
283 | target = C8720AC12834220A0046EB6A /* UIEnvironmentApp */;
284 | targetProxy = C8720AD92834220B0046EB6A /* PBXContainerItemProxy */;
285 | };
286 | /* End PBXTargetDependency section */
287 |
288 | /* Begin PBXVariantGroup section */
289 | C8720AF8283422F00046EB6A /* LaunchScreen.storyboard */ = {
290 | isa = PBXVariantGroup;
291 | children = (
292 | C8720AF9283422F00046EB6A /* Base */,
293 | );
294 | name = LaunchScreen.storyboard;
295 | sourceTree = "";
296 | };
297 | /* End PBXVariantGroup section */
298 |
299 | /* Begin XCBuildConfiguration section */
300 | C8720AEA2834220B0046EB6A /* Debug */ = {
301 | isa = XCBuildConfiguration;
302 | buildSettings = {
303 | ALWAYS_SEARCH_USER_PATHS = NO;
304 | CLANG_ANALYZER_NONNULL = YES;
305 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
306 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
307 | CLANG_ENABLE_MODULES = YES;
308 | CLANG_ENABLE_OBJC_ARC = YES;
309 | CLANG_ENABLE_OBJC_WEAK = YES;
310 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
311 | CLANG_WARN_BOOL_CONVERSION = YES;
312 | CLANG_WARN_COMMA = YES;
313 | CLANG_WARN_CONSTANT_CONVERSION = YES;
314 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
315 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
316 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
317 | CLANG_WARN_EMPTY_BODY = YES;
318 | CLANG_WARN_ENUM_CONVERSION = YES;
319 | CLANG_WARN_INFINITE_RECURSION = YES;
320 | CLANG_WARN_INT_CONVERSION = YES;
321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
322 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
323 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
324 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
325 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
326 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
327 | CLANG_WARN_STRICT_PROTOTYPES = YES;
328 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
329 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
330 | CLANG_WARN_UNREACHABLE_CODE = YES;
331 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
332 | COPY_PHASE_STRIP = NO;
333 | DEBUG_INFORMATION_FORMAT = dwarf;
334 | ENABLE_STRICT_OBJC_MSGSEND = YES;
335 | ENABLE_TESTABILITY = YES;
336 | GCC_C_LANGUAGE_STANDARD = gnu11;
337 | GCC_DYNAMIC_NO_PIC = NO;
338 | GCC_NO_COMMON_BLOCKS = YES;
339 | GCC_OPTIMIZATION_LEVEL = 0;
340 | GCC_PREPROCESSOR_DEFINITIONS = (
341 | "DEBUG=1",
342 | "$(inherited)",
343 | );
344 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
345 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
346 | GCC_WARN_UNDECLARED_SELECTOR = YES;
347 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
348 | GCC_WARN_UNUSED_FUNCTION = YES;
349 | GCC_WARN_UNUSED_VARIABLE = YES;
350 | INFOPLIST_FILE = App/Info.plist;
351 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
352 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
353 | MTL_FAST_MATH = YES;
354 | ONLY_ACTIVE_ARCH = YES;
355 | SDKROOT = iphoneos;
356 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
357 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
358 | };
359 | name = Debug;
360 | };
361 | C8720AEB2834220B0046EB6A /* Release */ = {
362 | isa = XCBuildConfiguration;
363 | buildSettings = {
364 | ALWAYS_SEARCH_USER_PATHS = NO;
365 | CLANG_ANALYZER_NONNULL = YES;
366 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
367 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
368 | CLANG_ENABLE_MODULES = YES;
369 | CLANG_ENABLE_OBJC_ARC = YES;
370 | CLANG_ENABLE_OBJC_WEAK = YES;
371 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
372 | CLANG_WARN_BOOL_CONVERSION = YES;
373 | CLANG_WARN_COMMA = YES;
374 | CLANG_WARN_CONSTANT_CONVERSION = YES;
375 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
376 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
377 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
378 | CLANG_WARN_EMPTY_BODY = YES;
379 | CLANG_WARN_ENUM_CONVERSION = YES;
380 | CLANG_WARN_INFINITE_RECURSION = YES;
381 | CLANG_WARN_INT_CONVERSION = YES;
382 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
383 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
384 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
385 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
386 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
387 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
388 | CLANG_WARN_STRICT_PROTOTYPES = YES;
389 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
390 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
391 | CLANG_WARN_UNREACHABLE_CODE = YES;
392 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
393 | COPY_PHASE_STRIP = NO;
394 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
395 | ENABLE_NS_ASSERTIONS = NO;
396 | ENABLE_STRICT_OBJC_MSGSEND = YES;
397 | GCC_C_LANGUAGE_STANDARD = gnu11;
398 | GCC_NO_COMMON_BLOCKS = YES;
399 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
400 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
401 | GCC_WARN_UNDECLARED_SELECTOR = YES;
402 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
403 | GCC_WARN_UNUSED_FUNCTION = YES;
404 | GCC_WARN_UNUSED_VARIABLE = YES;
405 | INFOPLIST_FILE = App/Info.plist;
406 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
407 | MTL_ENABLE_DEBUG_INFO = NO;
408 | MTL_FAST_MATH = YES;
409 | SDKROOT = iphoneos;
410 | SWIFT_COMPILATION_MODE = wholemodule;
411 | SWIFT_OPTIMIZATION_LEVEL = "-O";
412 | VALIDATE_PRODUCT = YES;
413 | };
414 | name = Release;
415 | };
416 | C8720AED2834220B0046EB6A /* Debug */ = {
417 | isa = XCBuildConfiguration;
418 | buildSettings = {
419 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
420 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
421 | CODE_SIGN_STYLE = Manual;
422 | CURRENT_PROJECT_VERSION = 1;
423 | DEVELOPMENT_TEAM = "";
424 | GENERATE_INFOPLIST_FILE = NO;
425 | INFOPLIST_FILE = App/Info.plist;
426 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
427 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
428 | INFOPLIST_KEY_UIMainStoryboardFile = Main;
429 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
430 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
431 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
432 | LD_RUNPATH_SEARCH_PATHS = (
433 | "$(inherited)",
434 | "@executable_path/Frameworks",
435 | );
436 | MARKETING_VERSION = 1.0;
437 | PRODUCT_BUNDLE_IDENTIFIER = com.plum.uienvironment.UIEnvironment;
438 | PRODUCT_NAME = "$(TARGET_NAME)";
439 | PROVISIONING_PROFILE_SPECIFIER = "";
440 | SWIFT_EMIT_LOC_STRINGS = YES;
441 | SWIFT_VERSION = 5.0;
442 | TARGETED_DEVICE_FAMILY = "1,2";
443 | };
444 | name = Debug;
445 | };
446 | C8720AEE2834220B0046EB6A /* Release */ = {
447 | isa = XCBuildConfiguration;
448 | buildSettings = {
449 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
450 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
451 | CODE_SIGN_STYLE = Manual;
452 | CURRENT_PROJECT_VERSION = 1;
453 | DEVELOPMENT_TEAM = "";
454 | GENERATE_INFOPLIST_FILE = NO;
455 | INFOPLIST_FILE = App/Info.plist;
456 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
457 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
458 | INFOPLIST_KEY_UIMainStoryboardFile = Main;
459 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
460 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
461 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
462 | LD_RUNPATH_SEARCH_PATHS = (
463 | "$(inherited)",
464 | "@executable_path/Frameworks",
465 | );
466 | MARKETING_VERSION = 1.0;
467 | PRODUCT_BUNDLE_IDENTIFIER = com.plum.uienvironment.UIEnvironment;
468 | PRODUCT_NAME = "$(TARGET_NAME)";
469 | PROVISIONING_PROFILE_SPECIFIER = "";
470 | SWIFT_EMIT_LOC_STRINGS = YES;
471 | SWIFT_VERSION = 5.0;
472 | TARGETED_DEVICE_FAMILY = "1,2";
473 | };
474 | name = Release;
475 | };
476 | C8720AF02834220B0046EB6A /* Debug */ = {
477 | isa = XCBuildConfiguration;
478 | buildSettings = {
479 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
480 | BUNDLE_LOADER = "$(TEST_HOST)";
481 | CODE_SIGN_STYLE = Automatic;
482 | CURRENT_PROJECT_VERSION = 1;
483 | GENERATE_INFOPLIST_FILE = YES;
484 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
485 | MARKETING_VERSION = 1.0;
486 | PRODUCT_BUNDLE_IDENTIFIER = com.plum.uienvironment.UIEnvironmentTests;
487 | PRODUCT_NAME = "$(TARGET_NAME)";
488 | SWIFT_EMIT_LOC_STRINGS = NO;
489 | SWIFT_VERSION = 5.0;
490 | TARGETED_DEVICE_FAMILY = "1,2";
491 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UIEnvironmentApp.app/UIEnvironmentApp";
492 | };
493 | name = Debug;
494 | };
495 | C8720AF12834220B0046EB6A /* Release */ = {
496 | isa = XCBuildConfiguration;
497 | buildSettings = {
498 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
499 | BUNDLE_LOADER = "$(TEST_HOST)";
500 | CODE_SIGN_STYLE = Automatic;
501 | CURRENT_PROJECT_VERSION = 1;
502 | GENERATE_INFOPLIST_FILE = YES;
503 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
504 | MARKETING_VERSION = 1.0;
505 | PRODUCT_BUNDLE_IDENTIFIER = com.plum.uienvironment.UIEnvironmentTests;
506 | PRODUCT_NAME = "$(TARGET_NAME)";
507 | SWIFT_EMIT_LOC_STRINGS = NO;
508 | SWIFT_VERSION = 5.0;
509 | TARGETED_DEVICE_FAMILY = "1,2";
510 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UIEnvironmentApp.app/UIEnvironmentApp";
511 | };
512 | name = Release;
513 | };
514 | /* End XCBuildConfiguration section */
515 |
516 | /* Begin XCConfigurationList section */
517 | C8720ABD2834220A0046EB6A /* Build configuration list for PBXProject "UIEnvironment" */ = {
518 | isa = XCConfigurationList;
519 | buildConfigurations = (
520 | C8720AEA2834220B0046EB6A /* Debug */,
521 | C8720AEB2834220B0046EB6A /* Release */,
522 | );
523 | defaultConfigurationIsVisible = 0;
524 | defaultConfigurationName = Release;
525 | };
526 | C8720AEC2834220B0046EB6A /* Build configuration list for PBXNativeTarget "UIEnvironmentApp" */ = {
527 | isa = XCConfigurationList;
528 | buildConfigurations = (
529 | C8720AED2834220B0046EB6A /* Debug */,
530 | C8720AEE2834220B0046EB6A /* Release */,
531 | );
532 | defaultConfigurationIsVisible = 0;
533 | defaultConfigurationName = Release;
534 | };
535 | C8720AEF2834220B0046EB6A /* Build configuration list for PBXNativeTarget "UIEnvironmentTests" */ = {
536 | isa = XCConfigurationList;
537 | buildConfigurations = (
538 | C8720AF02834220B0046EB6A /* Debug */,
539 | C8720AF12834220B0046EB6A /* Release */,
540 | );
541 | defaultConfigurationIsVisible = 0;
542 | defaultConfigurationName = Release;
543 | };
544 | /* End XCConfigurationList section */
545 |
546 | /* Begin XCSwiftPackageProductDependency section */
547 | C8D138BF28342750000BDBE9 /* UIEnvironment */ = {
548 | isa = XCSwiftPackageProductDependency;
549 | productName = UIEnvironment;
550 | };
551 | /* End XCSwiftPackageProductDependency section */
552 | };
553 | rootObject = C8720ABA2834220A0046EB6A /* Project object */;
554 | }
555 |
--------------------------------------------------------------------------------