├── .github └── FUNDING.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── InjectHotReload.podspec ├── LICENSE ├── Package.swift ├── README.md └── Sources └── Inject ├── InjectConfiguration.swift └── Integrations ├── Hosts.swift ├── KitFrameworks.swift └── SwiftUI.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.merowing.info/membership 2 | github: krzysztofzablocki 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## macOS finder stuff 9 | .DS_Store 10 | 11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 12 | *.xcscmblueprint 13 | *.xccheckout 14 | 15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 16 | build/ 17 | DerivedData/ 18 | *.moved-aside 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | 54 | # CocoaPods 55 | # 56 | # We recommend against adding the Pods directory to your .gitignore. However 57 | # you should judge for yourself, the pros and cons are mentioned at: 58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 59 | # 60 | # Pods/ 61 | # 62 | # Add this line if you want to avoid checking in source code from the Xcode workspace 63 | # *.xcworkspace 64 | 65 | # Carthage 66 | # 67 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 68 | # Carthage/Checkouts 69 | 70 | Carthage/Build/ 71 | 72 | # Accio dependency management 73 | Dependencies/ 74 | .accio/ 75 | 76 | # fastlane 77 | # 78 | # It is recommended to not store the screenshots in the git repo. 79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 80 | # For more information about the recommended setup visit: 81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 82 | 83 | fastlane/report.xml 84 | fastlane/Preview.html 85 | fastlane/screenshots/**/*.png 86 | fastlane/test_output 87 | 88 | # Code Injection 89 | # 90 | # After new code Injection tools there's a generated folder /iOSInjectionProject 91 | # https://github.com/johnno1962/injectionforxcode 92 | 93 | iOSInjectionProject/ 94 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /InjectHotReload.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "InjectHotReload" 3 | s.version = "1.4.0" 4 | s.summary = "Hot Reloading for Swift applications! " 5 | 6 | s.homepage = "https://github.com/krzysztofzablocki/Inject" 7 | s.license = { :type => "MIT", :file => "LICENSE" } 8 | s.author = { "Krzysztof Zablocki" => "krzysztof.zablocki@pixle.pl" } 9 | s.source = { :git => "https://github.com/krzysztofzablocki/Inject.git", :tag => s.version.to_s } 10 | 11 | s.ios.deployment_target = "11.0" 12 | s.osx.deployment_target = "10.15" 13 | s.tvos.deployment_target = "16.0" 14 | 15 | s.swift_version = "5.0" 16 | 17 | s.source_files = "Sources/Inject/**/*" 18 | end 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Krzysztof Zabłocki 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. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 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: "Inject", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | .iOS(.v11), 11 | .tvOS(.v13) 12 | ], 13 | products: [ 14 | .library( 15 | name: "Inject", 16 | targets: ["Inject"]), 17 | ], 18 | 19 | dependencies: [ 20 | ], 21 | targets: [ 22 | .target( 23 | name: "Inject", 24 | dependencies: []), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inject 2 | Hot reloading workflow helper that enables you to save hours of time each week, regardless if you are using `UIKit`, `AppKit` or `SwiftUI`. 3 | 4 | [**If you'd like to support my work and improve your engineering workflows, check out my SwiftyStack course**](https://www.swiftystack.com/) 5 | 6 | **TLDR: A single line of code** change allows you to live code `UIKit` screen: 7 | 8 | 9 | https://user-images.githubusercontent.com/26660989/161756368-b150bc25-b66f-4822-86ee-2e4aed713932.mp4 10 | 11 | 12 | 13 | [Read detailed article about this](https://merowing.info/2022/04/hot-reloading-in-swift/) 14 | 15 | The heavy lifting is done by the amazing [InjectionIII](https://github.com/johnno1962/InjectionIII). This library is just a thin wrapper to provide the best developer experience possible while requiring minimum effort. 16 | 17 | I've been using it for years. 18 | 19 | ## What is hot reloading? 20 | Hot reloading is a technique allowing you to get rid of compiling your whole application and avoiding deploy/restart cycles as much as possible, all while allowing you to edit your running application code and see changes reflected as close as possible to real-time. 21 | 22 | This makes you significantly more productive by reducing the time you spend waiting for apps to rebuild, restart, re-navigate to the previous location where you were in the app itself, re-produce the data you need. 23 | 24 | This can save you literal hours off development time, **each day**! 25 | 26 | ## Does it add manual overhead to my workflows? 27 | Once you configured your project initially, it's practically free. 28 | 29 | You don’t need to add conditional compilation or remove `Inject` code from your applications for production, it's already designed to behave as no-op inlined code that will get stripped by LLVM in non-debug builds. 30 | 31 | Which means that you can enable it once per view and keep using it for years to come. 32 | 33 | # Integration 34 | ### Initial project setup 35 | 36 | To integrate `Inject` just add it as SPM dependency: 37 | 38 | ### via Xcode 39 | 40 | Open your project, click on File → Swift Packages → Add Package Dependency…, enter the repository url (`https://github.com/krzysztofzablocki/Inject.git`) and add the package product to your app target. 41 | 42 | ### via SPM package.swift 43 | 44 | ```swift 45 | dependencies: [ 46 | .package( 47 | url: "https://github.com/krzysztofzablocki/Inject.git", 48 | from: "1.2.4" 49 | ) 50 | ] 51 | ``` 52 | 53 | ### via Cocoapods Podfile 54 | 55 | ```ruby 56 | pod 'InjectHotReload' 57 | ``` 58 | 59 | ### Individual Developer setup (once per machine) 60 | If anyone in your project wants to use injection, they only need to: 61 | 62 | - You must add "-Xlinker -interposable" (without the double quotes and on separate lines) to the "Other Linker Flags" of all targets in your project for the **Debug** configuration (qualified by the simulator SDK to avoid complications with bitcode), refer to [InjectionForXcode documentation](https://github.com/johnno1962/InjectionIII#limitationsfaq) if you run into any issues 63 | - Download newest version of Xcode Injection from it's [GitHub Page](https://github.com/johnno1962/InjectionIII/releases) 64 | - Unpack it and place under `/Applications` 65 | - Make sure that the Xcode version you are using to compile our projects is under the default location: `/Applications/Xcode.app` 66 | - Run the injection application 67 | - Select open project / open recent from it's menu and pick the right workspace file you are using 68 | 69 | After choosing the project in Injection app, launch the app 70 | - If everything is configured correctly you should see similar log in the console: 71 | 72 | ```bash 73 | 💉 InjectionIII connected /Users/merowing/work/SourceryPro/App.xcworkspace 74 | 💉 Watching files under /Users/merowing/work/SourceryPro 75 | ``` 76 | 77 | ## Workflow integration 78 | You can either add `import Inject` in individual files in your project or use 79 | `@_exported import Inject` in your project target to have it automatically available in all its files. 80 | 81 | #### **SwiftUI** 82 | Just 2 steps to enable injection in your `SwiftUI` Views 83 | 84 | - call `.enableInjection()` at the end of your body definition 85 | - add `@ObserveInjection var inject` to your view struct 86 | 87 | > *Remember you **don't need** to remove this code when you are done, it's NO-OP in production builds.* 88 | 89 | If you want to see your changes in action, you can enable an optional `Animation` variable on `InjectConfiguration.animation` that will be used when ever new source code is injected into your application. 90 | 91 | ```swift 92 | InjectConfiguration.animation = .interactiveSpring() 93 | ``` 94 | 95 | Using `Inject` is demoed in this [example app](https://github.com/MarcoEidinger/InjectSwiftUIExample) 96 | 97 | #### **UIKit / AppKit** 98 | For standard imperative UI frameworks we need a way to clean-up state between code injection phases. 99 | 100 | I create the concept of **Hosts** that work really well in that context, there are 2: 101 | 102 | - `ViewControllerHost` 103 | - `ViewHost` 104 | 105 | How do we integrate this? We wrap the class we want to iterate on at the parent level, so we don’t modify the class we want to be injecting but we modify the parent callsite. 106 | 107 | Eg. If you have a `SplitViewController` that creates `PaneA` and `PaneB `, and you want to iterate on layout/logic code in `PaneA`, you modify the callsite in `SplitViewController`: 108 | 109 | ```swift 110 | paneA = Inject.ViewHost( 111 | PaneAView(whatever: arguments, you: want) 112 | ) 113 | ``` 114 | 115 | That is all the changes you need to do, your app now allows you to change anything in `PaneAView` except for its initialiser API and the changes will be almost immediately reflected in your App. 116 | 117 | Make sure to call initializer inside `Inject.ViewControllerHost(...)` or `Inject.ViewHost(...)`. Inject relies on `@autoclosure` to reload views when hot-reload happens. Example: 118 | ```swift 119 | // WRONG 120 | let viewController = YourViewController() 121 | rootViewController.pushViewController(Inject.ViewControllerHost(viewController), animated: true) 122 | 123 | // CORRECT 124 | let viewController = Inject.ViewControllerHost(YourViewController()) 125 | rootViewController.pushViewController(viewController, animated: true) 126 | ``` 127 | > *Remember you **don't need** to remove this code when you are done, it's NO-OP in production builds.* 128 | 129 | 130 | #### **Injection Hook for UIKit** 131 | depending on the architecture used in your UIKit App, you might want to attach a hook to be executed each time a view controller is reloaded. 132 | 133 | Eg. you might want to bind the `UIViewController` to the presenter each-time there's a reload, to achieve this you can use `onInjectionHook` 134 | Example: 135 | 136 | ```swift 137 | myView.onInjectionHook = { hostedViewController in 138 | //any thing here will be executed each time the controller is reloaded 139 | // for example, you might want to re-assign the controller to your presenter 140 | presenter.ui = hostedViewController 141 | } 142 | ``` 143 | 144 | ## (Optional) Automatic Injection Script 145 | 146 | > **WARNING:** This script automatically modifies your Swift source code. It's provided as a convenience but use it with caution! Review the changes it makes carefully. It might not be suitable for all projects or coding styles. Consider using Xcode code snippets for more manual control. 147 | 148 | To automatically add `import Inject`, `@ObserveInjection var inject`, and `.enableInjection()` to your SwiftUI views, you can add the following script as a "Run Script" build phase in your Xcode project: 149 | 150 | ```sh 151 | #!/bin/bash 152 | 153 | # Function to modify a single Swift file 154 | modify_swift_file() { 155 | local filepath="$1" 156 | local filename=$(basename "$filepath") 157 | local tempfile="$filepath.tmp" 158 | 159 | # Check if the file should be processed 160 | if [[ $(grep -c ": View {" "$filepath") -eq 0 ]]; then 161 | echo "Skipping: $filename (No ': View {' found)" 162 | return 163 | fi 164 | 165 | # Create a temporary file for modifications 166 | cp "$filepath" "$tempfile" 167 | 168 | # 1. Add import Inject if needed 169 | if ! grep -q "import Inject" "$tempfile"; then 170 | sed -i '' -e '/^import SwiftUI/a\ 171 | import Inject' "$tempfile" 172 | fi 173 | 174 | # 2. Add @ObserveInjection var inject if needed 175 | if ! grep -q "@ObserveInjection var inject" "$tempfile"; then 176 | sed -i '' -e '/struct.*: View {/a\ 177 | @ObserveInjection var inject' "$tempfile" 178 | fi 179 | 180 | # 3. Add .enableInjection() just before the closing brace of the body 181 | # Find the start of var body: some View { 182 | local body_start_line=$(grep -n "var body: some View {" "$tempfile" | cut -d ':' -f 1) 183 | 184 | if [[ -n "$body_start_line" ]]; then 185 | # Get the line number of the closing brace of the body 186 | local body_end_line=$(awk -v start="$body_start_line" ' 187 | NR == start { count = 1 } 188 | NR > start { 189 | if ($0 ~ /{/) count++ 190 | if ($0 ~ /}/) { 191 | count-- 192 | if (count == 0) { 193 | print NR 194 | exit 195 | } 196 | } 197 | } 198 | ' "$tempfile") 199 | 200 | if [[ -n "$body_end_line" ]]; then 201 | # Check if .enableInjection() is already present 202 | if ! grep -q ".enableInjection()" "$tempfile"; then 203 | # Insert .enableInjection() before the closing brace of the body 204 | sed -i '' -e "${body_end_line}i\\ 205 | .enableInjection()" "$tempfile" 206 | fi 207 | fi 208 | fi 209 | 210 | # Check if modifications were made and overwrite the original file 211 | if ! cmp -s "$filepath" "$tempfile"; then 212 | mv "$tempfile" "$filepath" 213 | echo "Modified: $filename" 214 | else 215 | echo "No changes for: $filename" 216 | fi 217 | 218 | rm -f "$tempfile" 219 | } 220 | 221 | # Main script 222 | find "$SRCROOT" -name "*.swift" -print0 | while IFS= read -r -d $'\0' filepath; do 223 | modify_swift_file "$filepath" 224 | done 225 | 226 | echo "Inject modification script completed." 227 | ``` 228 | 229 | #### iOS 12 230 | You need to add -weak_framework SwiftUI to Other Linker Flags for iOS 12 to work. 231 | 232 | #### The Composable Architecture 233 | 234 | Since the introduction of ReducerProtocol you can use Inject with TCA without support code. 235 | -------------------------------------------------------------------------------- /Sources/Inject/InjectConfiguration.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import SwiftUI 4 | 5 | #if !os(watchOS) 6 | /// Common protocol interface for classes that support observing injection events 7 | /// This is automatically added to all NSObject subclasses like `ViewController`s or `Window`s 8 | public protocol InjectListener { 9 | associatedtype InjectInstanceType = Self 10 | 11 | func enableInjection() 12 | func onInjection(callback: @escaping (InjectInstanceType) -> Void) -> Void 13 | } 14 | 15 | /// Public namespace for using Inject API 16 | public enum InjectConfiguration { 17 | public static var bundlePath = "/Applications/InjectionIII.app/Contents/Resources/" 18 | @available(iOS 13.0, *) 19 | public static let observer = injectionObserver 20 | public static let load: Void = loadInjectionImplementation 21 | @available(iOS 13.0, *) 22 | public static var animation: SwiftUI.Animation? 23 | } 24 | 25 | public extension InjectListener { 26 | /// Ensures injection is enabled 27 | @inlinable @inline(__always) 28 | func enableInjection() { 29 | _ = InjectConfiguration.load 30 | } 31 | } 32 | 33 | #if DEBUG 34 | private var loadInjectionImplementation: Void = { 35 | guard objc_getClass("InjectionClient") == nil else { return } 36 | // If project has a "Build Phase" running this script, Inject should 37 | // work on a device (requires an InjectionIII github release 4.8.0+): 38 | // /Applications/InjectionIII.app/Contents/Resources/copy_bundle.sh 39 | if let path = Bundle.main.path(forResource: 40 | "iOSInjection", ofType: "bundle") ?? 41 | Bundle.main.path(forResource: 42 | "macOSInjection", ofType: "bundle"), 43 | Bundle(path: path)?.load() == true { 44 | return 45 | } 46 | #if os(macOS) 47 | let bundleName = "macOSInjection.bundle" 48 | #elseif os(tvOS) 49 | let bundleName = "tvOSInjection.bundle" 50 | #elseif os(visionOS) 51 | let bundleName = "xrOSInjection.bundle" 52 | #elseif targetEnvironment(simulator) 53 | let bundleName = "iOSInjection.bundle" 54 | #elseif targetEnvironment(macCatalyst) 55 | let bundleName = "macOSInjection.bundle" 56 | #else 57 | let bundleName = "maciOSInjection.bundle" 58 | #endif // OS and environment conditions 59 | 60 | #if targetEnvironment(simulator) || os(macOS) || targetEnvironment(macCatalyst) 61 | for which in ["III", "Next"] { 62 | let bundlePath = InjectConfiguration.bundlePath 63 | .replacingOccurrences(of: "III", with: which) + bundleName 64 | if let bundle = Bundle(path: bundlePath), bundle.load() { 65 | return 66 | } 67 | } 68 | 69 | print("⚠️ Inject: InjectionIII bundle not found, verify if it's in \(InjectConfiguration.bundlePath)") 70 | #endif 71 | }() 72 | 73 | @available(iOS 13.0, *) 74 | public class InjectionObserver: ObservableObject { 75 | @Published public private(set) var injectionNumber = 0 76 | private var cancellable: AnyCancellable? 77 | 78 | fileprivate init() { 79 | _ = loadInjectionImplementation 80 | cancellable = NotificationCenter.default.publisher(for: Notification.Name("INJECTION_BUNDLE_NOTIFICATION")) 81 | .sink { [weak self] _ in 82 | if let animation = InjectConfiguration.animation { 83 | withAnimation(animation) { 84 | self?.injectionNumber += 1 85 | } 86 | } else { 87 | self?.injectionNumber += 1 88 | } 89 | } 90 | } 91 | } 92 | 93 | @available(iOS 13.0, *) 94 | private let injectionObserver = InjectionObserver() 95 | @available(iOS 13.0, *) 96 | private var injectionObservationKey = arc4random() 97 | 98 | public extension InjectListener where Self: NSObject { 99 | func onInjection(callback: @escaping (Self) -> Void) { 100 | guard #available(iOS 13.0, *) else { 101 | return 102 | } 103 | let observation = injectionObserver.objectWillChange.sink(receiveValue: { [weak self] in 104 | guard let self = self else { return } 105 | callback(self) 106 | }) 107 | 108 | objc_setAssociatedObject(self, &injectionObservationKey, observation, .OBJC_ASSOCIATION_RETAIN) 109 | } 110 | } 111 | 112 | #else 113 | @available(iOS 13.0, *) 114 | public class InjectionObserver: ObservableObject {} 115 | @available(iOS 13.0, *) 116 | private let injectionObserver = InjectionObserver() 117 | private var loadInjectionImplementation: Void = {}() 118 | 119 | public extension InjectListener where Self: NSObject { 120 | @inlinable @inline(__always) 121 | func onInjection(callback: @escaping (Self) -> Void) {} 122 | } 123 | #endif // DEBUG 124 | #endif 125 | -------------------------------------------------------------------------------- /Sources/Inject/Integrations/Hosts.swift: -------------------------------------------------------------------------------- 1 | #if !os(watchOS) 2 | #if canImport(UIKit) 3 | import UIKit 4 | public typealias InjectViewControllerType = UIViewController 5 | public typealias InjectViewType = UIView 6 | #elseif canImport(AppKit) 7 | import AppKit 8 | public typealias InjectViewControllerType = NSViewController 9 | public typealias InjectViewType = NSView 10 | #endif 11 | 12 | #if DEBUG 13 | 14 | public typealias ViewControllerHost = _InjectableViewControllerHost 15 | public typealias ViewHost = _InjectableViewHost 16 | 17 | /// Usage: to create an autoreloading view controller, wrap your 18 | /// view controller that you wish to see changes within `ViewHost`. For example, 19 | /// If you are using a `TestViewController`, you would do the following: 20 | /// `let myView = ViewControllerHost(TestViewController())` 21 | /// And within the parent view, you should add the view above. 22 | @dynamicMemberLookup 23 | open class _InjectableViewControllerHost: InjectViewControllerType { 24 | public private(set) var instance: Hosted 25 | let constructor: () -> Hosted 26 | /// Attaches a hook to be executed each time after a controller is reloaded. 27 | /// 28 | /// Usage: 29 | /// ```swift 30 | /// let myView = ViewControllerHost(TestViewController()) 31 | /// myView.onInjectionHook = { hostedViewController in 32 | /// //any thing here will be executed each time the controller is reloaded 33 | /// // for example, you might want to re-assign the controller to your presenter 34 | /// presenter.ui = hostedViewController 35 | /// } 36 | /// ``` 37 | public var onInjectionHook: ((Hosted) -> Void)? 38 | 39 | public init(_ constructor: @autoclosure @escaping () -> Hosted) { 40 | instance = constructor() 41 | self.constructor = constructor 42 | 43 | super.init(nibName: nil, bundle: nil) 44 | self.enableInjection() 45 | 46 | addAsChild() 47 | onInjection { [weak self] instance in 48 | guard let self else { return } 49 | instance.resetHosted() 50 | self.onInjectionHook?(self.instance) 51 | } 52 | } 53 | 54 | override open func loadView() { 55 | view = InjectViewType(frame: .zero) 56 | } 57 | 58 | private func resetHosted() { 59 | // remove old vc from child list 60 | #if canImport(UIKit) 61 | instance.willMove(toParent: nil) 62 | #endif 63 | instance.view.removeFromSuperview() 64 | instance.removeFromParent() 65 | 66 | instance = constructor() 67 | addAsChild() 68 | } 69 | 70 | private func addAsChild() { 71 | // add the real content as child 72 | addChild(instance) 73 | view.addSubview(instance.view) 74 | #if canImport(UIKit) 75 | instance.didMove(toParent: self) 76 | 77 | title = instance.title 78 | tabBarItem = instance.tabBarItem 79 | definesPresentationContext = instance.definesPresentationContext 80 | modalPresentationStyle = instance.modalPresentationStyle 81 | #if !os(tvOS) 82 | navigationItem.title = instance.navigationItem.title 83 | navigationItem.titleView = instance.navigationItem.titleView 84 | navigationItem.backButtonTitle = instance.navigationItem.backButtonTitle 85 | navigationItem.backBarButtonItem = instance.navigationItem.backBarButtonItem 86 | navigationItem.leftBarButtonItems = instance.navigationItem.leftBarButtonItems 87 | navigationItem.rightBarButtonItems = instance.navigationItem.rightBarButtonItems 88 | navigationItem.largeTitleDisplayMode = instance.navigationItem.largeTitleDisplayMode 89 | navigationItem.searchController = instance.navigationItem.searchController 90 | navigationItem.hidesSearchBarWhenScrolling = instance.navigationItem.hidesSearchBarWhenScrolling 91 | toolbarItems = instance.toolbarItems 92 | hidesBottomBarWhenPushed = instance.hidesBottomBarWhenPushed 93 | #endif 94 | #endif 95 | 96 | instance.view.translatesAutoresizingMaskIntoConstraints = false 97 | [ 98 | instance.view.topAnchor.constraint(equalTo: view.topAnchor), 99 | instance.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), 100 | instance.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), 101 | instance.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) 102 | ] 103 | .forEach { $0.isActive = true } 104 | } 105 | 106 | @available(*, unavailable) 107 | required public init?(coder: NSCoder) { 108 | fatalError("init(coder:) has not been implemented") 109 | } 110 | 111 | #if canImport(UIKit) && os(iOS) 112 | override open var childForStatusBarStyle: InjectViewControllerType? { 113 | instance 114 | } 115 | #endif 116 | 117 | public subscript(dynamicMember keyPath: WritableKeyPath) -> T { 118 | get { instance[keyPath: keyPath] } 119 | set { instance[keyPath: keyPath] = newValue } 120 | } 121 | 122 | public subscript(dynamicMember keyPath: KeyPath) -> T { 123 | instance[keyPath: keyPath] 124 | } 125 | } 126 | 127 | /// Usage: to create an autoreloading view, wrap your 128 | /// view that you wish to see changes within `ViewHost`. For example, 129 | /// If you are using a `TestView`, you would do the following: 130 | /// `let myView = ViewHost(TestView())` 131 | /// And within the parent view, you should add the view above. 132 | @dynamicMemberLookup 133 | public class _InjectableViewHost: InjectViewType { 134 | public private(set) var instance: Hosted 135 | let constructor: () -> Hosted 136 | 137 | public init(_ constructor: @autoclosure @escaping () -> Hosted) { 138 | instance = constructor() 139 | self.constructor = constructor 140 | 141 | super.init(frame: .zero) 142 | self.enableInjection() 143 | addAsChild() 144 | onInjection { instance in 145 | instance.resetHosted() 146 | } 147 | } 148 | 149 | private func resetHosted() { 150 | instance.removeFromSuperview() 151 | 152 | instance = constructor() 153 | addAsChild() 154 | } 155 | 156 | private func addAsChild() { 157 | // add the real content as child 158 | addSubview(instance) 159 | 160 | instance.translatesAutoresizingMaskIntoConstraints = false 161 | [ 162 | instance.topAnchor.constraint(equalTo: topAnchor), 163 | instance.leadingAnchor.constraint(equalTo: leadingAnchor), 164 | instance.bottomAnchor.constraint(equalTo: bottomAnchor), 165 | instance.trailingAnchor.constraint(equalTo: trailingAnchor) 166 | ] 167 | .forEach { $0.isActive = true } 168 | } 169 | 170 | @available(*, unavailable) 171 | required init?(coder: NSCoder) { 172 | fatalError("init(coder:) has not been implemented") 173 | } 174 | 175 | public subscript(dynamicMember keyPath: WritableKeyPath) -> T { 176 | get { instance[keyPath: keyPath] } 177 | set { instance[keyPath: keyPath] = newValue } 178 | } 179 | 180 | public subscript(dynamicMember keyPath: KeyPath) -> T { 181 | instance[keyPath: keyPath] 182 | } 183 | } 184 | 185 | extension InjectConfiguration { 186 | public static func ViewControllerHost(_ viewController: Hosted) -> ViewControllerHost { 187 | Inject.ViewControllerHost(viewController) 188 | } 189 | public static func ViewHost(_ view: Hosted) -> ViewHost { 190 | Inject.ViewHost(view) 191 | } 192 | } 193 | #else 194 | 195 | extension InjectConfiguration { 196 | public static func ViewControllerHost(_ viewController: Hosted) -> Hosted { 197 | viewController 198 | } 199 | public static func ViewHost(_ view: Hosted) -> Hosted { 200 | view 201 | } 202 | } 203 | 204 | #endif 205 | #endif 206 | -------------------------------------------------------------------------------- /Sources/Inject/Integrations/KitFrameworks.swift: -------------------------------------------------------------------------------- 1 | #if !os(watchOS) 2 | #if canImport(UIKit) 3 | import Foundation 4 | import UIKit 5 | 6 | extension UIView: InjectListener {} 7 | extension UIViewController: InjectListener {} 8 | #elseif canImport(AppKit) 9 | import AppKit 10 | import Foundation 11 | 12 | extension NSView: InjectListener {} 13 | extension NSViewController: InjectListener {} 14 | extension NSWindow: InjectListener {} 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /Sources/Inject/Integrations/SwiftUI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | #if !os(watchOS) 5 | #if DEBUG 6 | @available(iOS 13.0, *) 7 | public extension SwiftUI.View { 8 | func enableInjection() -> some SwiftUI.View { 9 | _ = InjectConfiguration.load 10 | 11 | // Use AnyView in case the underlying view structure changes during injection. 12 | // This is only in effect in debug builds. 13 | return AnyView(self) 14 | } 15 | 16 | func onInjection(callback: @escaping (Self) -> Void) -> some SwiftUI.View { 17 | onReceive(InjectConfiguration.observer.objectWillChange, perform: { 18 | callback(self) 19 | }) 20 | .enableInjection() 21 | } 22 | } 23 | 24 | @available(iOS 13.0, *) 25 | @propertyWrapper @preconcurrency @MainActor 26 | public struct ObserveInjection: DynamicProperty { 27 | @ObservedObject private var iO = InjectConfiguration.observer 28 | public nonisolated init() {} 29 | // Use a computed property rather than directly storing the value to work around https://github.com/swiftlang/swift/issues/62003 30 | public var wrappedValue: InjectConfiguration.Type { InjectConfiguration.self } 31 | } 32 | 33 | #else 34 | @available(iOS 13.0, *) 35 | public extension SwiftUI.View { 36 | @inlinable @inline(__always) 37 | func enableInjection() -> Self { self } 38 | 39 | @inlinable @inline(__always) 40 | func onInjection(callback: @escaping (Self) -> Void) -> Self { 41 | self 42 | } 43 | } 44 | 45 | @available(iOS 13.0, *) 46 | @propertyWrapper @preconcurrency @MainActor 47 | public struct ObserveInjection: DynamicProperty { 48 | public nonisolated init() {} 49 | // Use a computed property rather than directly storing the value to work around https://github.com/swiftlang/swift/issues/62003 50 | public var wrappedValue: InjectConfiguration.Type { InjectConfiguration.self } 51 | } 52 | #endif 53 | #endif 54 | --------------------------------------------------------------------------------