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