Skip to content

Commit

Permalink
Re-organise README.
Browse files Browse the repository at this point in the history
  • Loading branch information
johnno1962 committed Nov 30, 2022
1 parent 57c32e6 commit e56dd89
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 104 deletions.
2 changes: 1 addition & 1 deletion InjectionIII/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>7685</string>
<string>7686</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key>
Expand Down
213 changes: 110 additions & 103 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ application might crash during debugging and you'll have to restart it as you wo
had to anyway. Gaining trust in the changes you can inject builds with experience and
with it, the amount of time you save. The `iOSInjection.bundle` is only loaded during
development in the simulator and cannot affect your application when it is deployed
into production.
to a production device.

Always remember to add `"Other Linker Flags"`, `"-Xlinker -interposable"`
to your project or due to details of how a method is dispatched you may
Expand All @@ -66,9 +66,17 @@ is performed on the main thread and generally reliable. A common question for ne
users is: I injected a new version of the code, why can't I see the changes on the screen?
To have effect, the new code needs to be actually executed and it's up to the user to use
either an `@objc func injected()` method or a notification to reload a view controller
or refresh a table view to see changes or perform some user action that forces a redisplay.
or refresh a table view to see changes or perform some user action that forces a redisplay. For example, to force all ViewControllers in your app
to reload when they are injected some people use this code:

If you try InjectionIII and you think it doesn't work, please, please file an issue so we can
```Swift
extension UIViewController {
@objc func injected() {
viewDidLoad()
}
}
```
If you try InjectionIII and you think it doesn't work, please, please open an issue so we can
either explain what is going on, improve the documentation or try to resolve the particular
edge case you have encountered. The project is quite mature now and provided you're
holding it correctly and don't ask too much of it, it should "just work".
Expand Down Expand Up @@ -174,6 +182,93 @@ to your project's targets and download a [binary release](https://github.com/joh
to make available the "iOSInjection.bundle" but no longer need to run
the app (though it still works as it did before if you do).

### SwiftUI Injection

It is possible to inject `SwiftUI` interfaces but it requires some minor
code changes. This is because when you add elements to an interface or
use modifiers that change their type, this changes the return type of the
body property's `Content` across the injection which causes a crash.
To avoid this you need to erase the return type. The easiest way to do
this is to add the code below to your source somewhere then add the
modifier `.eraseToAnyView()` at the very end of any declaration of a
view's body property that you want to inject:

```Swift
#if DEBUG
private var loadInjection: () = {
guard objc_getClass("InjectionClient") == nil else { return }
#if os(macOS) || targetEnvironment(macCatalyst)
let bundleName = "macOSInjection.bundle"
#elseif os(tvOS)
let bundleName = "tvOSInjection.bundle"
#elseif targetEnvironment(simulator)
let bundleName = "iOSInjection.bundle"
#else
let bundleName = "maciOSInjection.bundle"
#endif
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/"+bundleName)!.load()
}()

import Combine

public let injectionObserver = InjectionObserver()

public class InjectionObserver: ObservableObject {
@Published var injectionNumber = 0
var cancellable: AnyCancellable? = nil
let publisher = PassthroughSubject<Void, Never>()
init() {
cancellable = NotificationCenter.default.publisher(for:
Notification.Name("INJECTION_BUNDLE_NOTIFICATION"))
.sink { [weak self] change in
self?.injectionNumber += 1
self?.publisher.send()
}
}
}

extension View {
public func eraseToAnyView() -> some View {
_ = loadInjection
return AnyView(self)
}
public func onInjection(bumpState: @escaping () -> ()) -> some View {
return self
.onReceive(injectionObserver.publisher, perform: bumpState)
.eraseToAnyView()
}
}
#else
extension View {
public func eraseToAnyView() -> some View { return self }
public func onInjection(bumpState: @escaping () -> ()) -> some View {
return self
}
}
#endif
```

To have the view you are working on redisplay automatically when it is injected it's sufficient
to add an `@ObservedObject`, initialised to the `injectionObserver` instance as follows:

```Swift
.eraseToAnyView()
}

#if DEBUG
@ObservedObject var iO = injectionObserver
#endif
```
You can make all these changes automatically once you've opened a project using the
`"Prepare Project"` menu item of the app. If you'd like to execute some code each time your interface is injected, use the
`.onInjection { ... }` modifier instead of .`eraseToAnyView()`.
As an alternative, this code is available in the
[HotSwiftUI](https://github.com/johnno1962/HotSwiftUI)
Swift Package. Another alternative
from someone who has considerably more experience in iOS development
than I do check out the [Inject](https://github.com/krzysztofzablocki/Inject)
Swift Package introduced by this [blog post](https://merowing.info/2022/04/hot-reloading-in-swift/).

### Limitations/FAQ

New releases of InjectionIII use a [different patching technique](http://johnholdsworth.com/dyld_dynamic_interpose.html)
Expand All @@ -189,12 +284,11 @@ using the `-interposable` flag may provoke undefined symbols or the following er
```
Can't find ordinal for imported symbol for architecture x86_64
```
If this is the case, add the following additional "Other linker Flags" and it should go away.
If this is the case, add the following additional "Other linker Flags" and it will become a warning.

```
-Xlinker -undefined -Xlinker dynamic_lookup
```

If you have a project using extensive bridging & Objective-C it's recommended to use
one of the [binary github releases](https://github.com/johnno1962/InjectionIII/releases)
that have the sandbox turned off. This is because the App Store version operates in
Expand All @@ -217,21 +311,15 @@ which is integrated into InjectionIII.
As injection needs to know how to compile Swift files individually it is not compatible with building using
`Whole Module Optimisation`. A workaround for this is to build with `WMO` switched off so there are
logs of individual compiles available then switching `WMO` back on if it suits your workflow better.
You may need to do this each time you open your project as Xcode is now
far for agressive in removing old build logs.

### Resolving issues

Versions > 4.1.1 of InjectionIII have the following environment variables that
can be added to your Xcode launch scheme to customise its behavour or to
get a better idea what InjectionIII is doing.

**INJECTION_PRESERVE_STATICS** This allows you to decide
whether top level variables and static member should be re-initialised
if they are in a file that is injected or they should retain their values.

**INJECTION_DYNAMIC_CAST** This allows you to opt into a slightly
more speculative fix for when you dynamic cast (as? in Swift) to a type
which has been injected and therefore its type identifier may have changed.

**INJECTION_DETAIL** Providing any value for this variable in the
your scheme will produce detailed output of how InjectionIII is
stitching your new implementations into your application. "Swizzling"
Expand All @@ -243,6 +331,14 @@ feature to effectively re-link call sites to the newly loaded versions
(provided the "-Xlinker -interposable" "Other Linker Flag" build
setting has been supplied).

**INJECTION_PRESERVE_STATICS** This allows you to decide
whether top level variables and static member should be re-initialised
if they are in a file that is injected or they should preserve their values.

**INJECTION_DYNAMIC_CAST** This allows you to opt into a slightly
more speculative fix for when you dynamic cast (as? in Swift) to a type
which has been injected and therefore its type identifier may have changed.

In order to implement the `@objc func injected()` call to your
class when an instance is injected, a sweep of all live objects in your
app is performed. This has two limitations. The instance needs to be
Expand Down Expand Up @@ -270,95 +366,6 @@ for larger projects. Otherwise it will still occur "on demand".
root of your project in it's scheme automatiically messaging the InjectionIII
app to change the scope of the file watcher as you switch between projects.

### SwiftUI Injection

It is possible to inject `SwiftUI` interfaces but it requires some minor
code changes. This is because when you add elements to an interface or
use modifiers that change their type, this changes the return type of the
body property's `Content` across the injection which causes a crash.
To avoid this you need to erase the return type. The easiest way to do
this is to add the code below to your source somewhere then add the
modifier `.eraseToAnyView()` at the very end of any declaration of a
view's body property that you want to inject:

```Swift
#if DEBUG
private var loadInjection: () = {
guard objc_getClass("InjectionClient") == nil else { return }
#if os(macOS) || targetEnvironment(macCatalyst)
let bundleName = "macOSInjection.bundle"
#elseif os(tvOS)
let bundleName = "tvOSInjection.bundle"
#elseif targetEnvironment(simulator)
let bundleName = "iOSInjection.bundle"
#else
let bundleName = "maciOSInjection.bundle"
#endif
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/"+bundleName)!.load()
}()

import Combine

public let injectionObserver = InjectionObserver()

public class InjectionObserver: ObservableObject {
@Published var injectionNumber = 0
var cancellable: AnyCancellable? = nil
let publisher = PassthroughSubject<Void, Never>()
init() {
cancellable = NotificationCenter.default.publisher(for:
Notification.Name("INJECTION_BUNDLE_NOTIFICATION"))
.sink { [weak self] change in
self?.injectionNumber += 1
self?.publisher.send()
}
}
}

extension View {
public func eraseToAnyView() -> some View {
_ = loadInjection
return AnyView(self)
}
public func onInjection(bumpState: @escaping () -> ()) -> some View {
return self
.onReceive(injectionObserver.publisher, perform: bumpState)
.eraseToAnyView()
}
}
#else
extension View {
public func eraseToAnyView() -> some View { return self }
public func onInjection(bumpState: @escaping () -> ()) -> some View {
return self
}
}
#endif
```

To have the view you are working on redisplay automatically when it is injected it's sufficient
to add an `@ObservedObject`, initialised to the `injectionObserver` instance as follows:

```Swift
.eraseToAnyView()
}

#if DEBUG
@ObservedObject var iO = injectionObserver
#endif
```
You can make all these changes automatically once you've opened a project using the
`"Prepare Project"` menu item. If you'd like to execute some code each time your interface is injected, use the
`.onInjection { ... }` modifier instead of .`eraseToAnyView()`.
As an alternative, this code is available in the
[HotSwiftUI](https://github.com/johnno1962/HotSwiftUI)
Swift Package though you would have to remember to load the
`iOSInjection.bundle` separately by using the `.loadInjection()`
modifier on a view struct somewhere in your app. Another alternative
from someone who has considerably more experience in iOS development
than I do check out the [Inject](https://github.com/krzysztofzablocki/Inject)
Swift Package introduced by this [blog post](https://merowing.info/2022/04/hot-reloading-in-swift/).

### InjectionIII and "The Composable Architecture"

Applications written using "TCA" can have the "reducer" functions
Expand Down Expand Up @@ -496,4 +503,4 @@ for the code to be evaluated using injection under an MIT license.

The fabulous app icon is thanks to Katya of [pixel-mixer.com](http://pixel-mixer.com/).

$Date: 2022/11/29 $
$Date: 2022/11/30 $

0 comments on commit e56dd89

Please sign in to comment.