Skip to content

Commit

Permalink
chore: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
krzysztofzablocki committed Apr 1, 2022
0 parents commit 57ba3af
Show file tree
Hide file tree
Showing 9 changed files with 510 additions and 0 deletions.
90 changes: 90 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## User settings
xcuserdata/

## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout

## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3

## Obj-C/Swift specific
*.hmap

## App packaging
*.ipa
*.dSYM.zip
*.dSYM

## Playgrounds
timeline.xctimeline
playground.xcworkspace

# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm

.build/

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build/

# Accio dependency management
Dependencies/
.accio/

# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Krzysztof Zabłocki

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
25 changes: 25 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "Inject",
platforms: [
.macOS(.v10_15),
.iOS(.v13)
],
products: [
.library(
name: "Inject",
targets: ["Inject"]),
],

dependencies: [
],
targets: [
.target(
name: "Inject",
dependencies: []),
]
)
96 changes: 96 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Inject
Hot reloading workflow helper that enables you to save hours of time each week, regardless if you are using `UIKit`, `AppKit` or `SwiftUI`.

The heavy lifting is done by the amazing [InjectionForXcode](https://github.com/johnno1962/InjectionIII). This library is just a think wrapper to provide the best developer experience possible while requiring minimum effort.

I've been using it for years.

## What is hot reloading?
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.

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.

This can save you literal hours off development time, **each day**!

## Does it add manual overhead to my workflows?
Once you configured your project initially, it's practically free.

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.

Which means that you can enable it once per view and keep using it for years to come.

# Integration
### Initial project setup

To integrate `Inject` just add it as SPM dependency:

### via Xcode

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.

### via SPM package.swift

```swift
dependencies: [
.package(
name: "Inject",
url: "https://github.com/krzysztofzablocki/Inject.git",
from: "1.0.0"
)
]
```
### Individual Developer setup (once per machine)
If anyone in your project wants to use injection, they only need to:

- You must add "-Xlinker -interposable" (without the double quotes) to your project's "Other Linker Flags" for the Debug target (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
- Download newest version of Xcode Injection from it's [GitHub Page](https://github.com/johnno1962/InjectionIII/releases)
- Unpack it and place under `/Applications`
- Make sure that the Xcode version you are using to compile our projects is under the default location: `/Applications/Xcode.app`
- Run the injection application
- Select open project / open recent from it's menu and pick the right workspace file you are using

After choosing the project in Injection app, launch the app
- If everything is configured correctly you should see similar log in the console:

```bash
💉 InjectionIII connected /Users/merowing/work/SourceryPro/App.xcworkspace
💉 Watching files under /Users/merowing/work/SourceryPro
```

## Workflow integration
You can either add `import Inject` in individual files in your project or use
`@_exported import Inject` in your project target to have it automatically available in all its files.

#### **SwiftUI**
Just 2 steps to enable injection in your `SwiftUI` Views

- Add `@ObservedObject private var iO = Inject.observer` variable
- call `.enableInjection()` at the end of your body definition

> *Remember you **don't need** to remove this code when you are done, it's NO-OP in production builds.*
#### **UIKit / AppKit**
For standard imperative UI frameworks we need a way to clean-up state between code injection phases.

I create the concept of **Hosts** that work really well in that context, there are 2:

- `Inject.ViewControllerHost`
- `Inject.ViewHost`

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.

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`:

```swift
paneA = Inject.ViewHost(
PaneAView(whatever: arguments, you: want)
)
```

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.

> Host changes can’t be fully inlined, so those classes are removed in release builds, to accommodate for that inconvenience the easiest way is to simply make a separate commit that swaps this one-liner and then remove it at the end of your workflow.
#### The Composable Architecture

If like myself you love [PointFree](https://pointfree.co/) Composable Architecture, you’d probably want to inject reducer code, this isn’t possible in vanilla TCA because reducer code is a free function which isn’t as straightforward to replace with injection, but [our fork](https://github.com/thebrowsercompany/swift-composable-architecture) at [The Browser Company](https://thebrowser.company/) supports it.
79 changes: 79 additions & 0 deletions Sources/Inject/Inject.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import Foundation
import Combine

/// Common protocol interface for classes that support observing injection events
/// This is automatically added to all NSObject subclasses like `ViewController`s or `Window`s
public protocol InjectListener {
associatedtype InjectInstanceType = Self

func enableInjection()
func onInjection(callback: @escaping (InjectInstanceType) -> Void) -> Void
}

/// Public namespace for using Inject API
public enum Inject {
public static let observer = injectionObserver
public static let load: Void = loadInjectionImplementation
}

public extension InjectListener {
/// Ensures injection is enabled
@inlinable @inline(__always)
func enableInjection() {
_ = Inject.load
}
}

#if DEBUG
private var loadInjectionImplementation: Void = {
#if os(macOS)
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()
}()

public class InjectionObserver: ObservableObject {
@Published public private(set) var injectionNumber = 0
private var cancellable: AnyCancellable?

fileprivate init() {
cancellable = NotificationCenter.default.publisher(for: Notification.Name("INJECTION_BUNDLE_NOTIFICATION"))
.sink { [weak self] _ in
self?.injectionNumber += 1
}
}
}

private let injectionObserver = InjectionObserver()
private var injectionObservationKey = arc4random()

public extension InjectListener where Self: NSObject {
func onInjection(callback: @escaping (Self) -> Void) {
let observation = injectionObserver.objectWillChange.sink(receiveValue: { [weak self] in
guard let self = self else {
return
}

callback(self)
})

objc_setAssociatedObject(self, &injectionObservationKey, observation, .OBJC_ASSOCIATION_RETAIN)
}
}

#else
public class InjectionObserver: ObservableObject {}
private let injectionObserver = InjectionObserver()
private var loadInjectionImplementation: Void = {}()

public extension InjectionListener where Self: NSObject {
@inlinable @inline(__always)
func onInjection(callback: @escaping (Self) -> Void) {}
}
#endif
Loading

0 comments on commit 57ba3af

Please sign in to comment.