├── 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 | --------------------------------------------------------------------------------