注入 (Inject)

一个热重载工作流助手,无论你使用 UIKitAppKit 还是 SwiftUI,都可以每周为你节省大量时间。

如果你想支持我的工作并改进你的工程工作流,请查看我的 SwiftyStack 课程。

TLDR:只需更改一行代码即可实时编码 UIKit 屏幕。

HotReloadingDemo.mp4

阅读关于此的详细文章

繁重的工作由出色的 InjectionIII 完成。 这个库只是一个简单的封装,旨在提供最佳的开发者体验,同时只需最小的努力。

我已经使用它很多年了。

什么是热重载?

热重载是一种技术,允许你摆脱编译整个应用程序,并尽可能避免部署/重启循环,同时允许你编辑正在运行的应用程序代码,并以尽可能接近实时的方式看到更改的反映。

这通过减少你花费在等待应用程序重建、重启、重新导航到应用程序本身之前所在的位置、重新生成所需数据的时间,来显著提高你的工作效率。

这可以为你节省每天的大量开发时间!

它是否会给我的工作流程增加手动开销?

一旦你最初配置好你的项目,它几乎是免费的。

你不需要添加条件编译或从生产应用程序中删除 Inject 代码,它已经被设计为在非调试构建中,会被 LLVM 剥离的无操作内联代码。

这意味着你可以为每个视图启用一次,并继续使用它多年。

集成

初始项目设置

要集成 Inject,只需将其添加为 SPM 依赖项

通过 Xcode

打开你的项目,点击 File → Swift Packages → Add Package Dependency…,输入仓库 URL (https://github.com/krzysztofzablocki/Inject.git),并将包产品添加到你的应用程序目标。

通过 SPM package.swift

dependencies: [
    .package(
      url: "https://github.com/krzysztofzablocki/Inject.git",
      from: "1.2.4"
    )
]

通过 Cocoapods Podfile

pod 'InjectHotReload'

个人开发者设置(每台机器一次)

如果你的项目中的任何人想要使用注入,他们只需要

在 Injection 应用程序中选择项目后,启动应用程序

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

工作流程集成

你可以在项目中的单个文件中添加 import Inject,或者在你的项目目标中使用 @_exported import Inject,以使其在所有文件中自动可用。

SwiftUI

只需 2 个步骤即可在你的 SwiftUI 视图中启用注入

记住,你完成之后不需要删除此代码,它在生产版本中是 NO-OP。

如果你想看到你的更改生效,你可以在 InjectConfiguration.animation 上启用一个可选的 Animation 变量,当有新的源代码注入到你的应用程序时,它将被使用。

InjectConfiguration.animation = .interactiveSpring()

Inject 的使用在这个 示例应用程序 中进行了演示

UIKit / AppKit

对于标准的命令式 UI 框架,我们需要一种方法来在代码注入阶段之间清理状态。

我创建了 Hosts 的概念,它在这种情况下运行良好,共有 2 个

我们如何集成这个? 我们在父级别包装我们想要迭代的类,所以我们不会修改我们想要注入的类,而是修改父调用点。

例如,如果你有一个创建 PaneAPaneBSplitViewController,并且你想在 PaneA 中迭代布局/逻辑代码,你修改 SplitViewController 中的调用点

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

这就是你需要做的所有更改,你的应用程序现在允许你更改 PaneAView 中的任何内容,除了其初始化器 API,并且更改几乎会立即反映在你的应用程序中。

确保在 Inject.ViewControllerHost(...)Inject.ViewHost(...) 内部调用初始化器。 Inject 依赖于 @autoclosure 在热重载发生时重新加载视图。 例子

// WRONG
let viewController = YourViewController()
rootViewController.pushViewController(Inject.ViewControllerHost(viewController), animated: true)

// CORRECT
let viewController = Inject.ViewControllerHost(YourViewController())
rootViewController.pushViewController(viewController, animated: true)

记住,你完成之后不需要删除此代码,它在生产版本中是 NO-OP。

UIKit 的注入钩子

根据你的 UIKit 应用程序中使用的架构,你可能希望附加一个钩子,以便在每次重新加载视图控制器时执行。

例如,你可能希望每次重新加载时将 UIViewController 绑定到 presenter,为了实现这一点,你可以使用 onInjectionHook。例子

myView.onInjectionHook = { hostedViewController in
//any thing here will be executed each time the controller is reloaded
// for example, you might want to re-assign the controller to your presenter
presenter.ui = hostedViewController
}

(可选)自动注入脚本

警告: 此脚本会自动修改你的 Swift 源代码。 它作为一种便利提供,但请谨慎使用! 仔细检查它所做的更改。 它可能不适用于所有项目或编码风格。 考虑使用 Xcode 代码片段进行更多手动控制。

要自动将 import Inject@ObserveInjection var inject.enableInjection() 添加到你的 SwiftUI 视图,你可以将以下脚本作为 Xcode 项目中的“Run Script”构建阶段添加

#!/bin/bash

# Function to modify a single Swift file
modify_swift_file() {
    local filepath="$1"
    local filename=$(basename "$filepath")
    local tempfile="$filepath.tmp"

    # Check if the file should be processed
    if [[ $(grep -c ": View {" "$filepath") -eq 0 ]]; then
        echo "Skipping: $filename (No ': View {' found)"
        return
    fi

    # Create a temporary file for modifications
    cp "$filepath" "$tempfile"

    # 1. Add import Inject if needed
    if ! grep -q "import Inject" "$tempfile"; then
        sed -i '' -e '/^import SwiftUI/a\
import Inject' "$tempfile"
    fi

    # 2. Add @ObserveInjection var inject if needed
    if ! grep -q "@ObserveInjection var inject" "$tempfile"; then
        sed -i '' -e '/struct.*: View {/a\
    @ObserveInjection var inject' "$tempfile"
    fi

    # 3. Add .enableInjection() just before the closing brace of the body
    # Find the start of var body: some View {
    local body_start_line=$(grep -n "var body: some View {" "$tempfile" | cut -d ':' -f 1)

    if [[ -n "$body_start_line" ]]; then
        # Get the line number of the closing brace of the body
        local body_end_line=$(awk -v start="$body_start_line" '
 NR == start { count = 1 }
 NR > start {
 if ($0 ~ /{/) count++
 if ($0 ~ /}/) {
 count--
 if (count == 0) {
 print NR
 exit
 }
 }
 }
 ' "$tempfile")

        if [[ -n "$body_end_line" ]]; then
            # Check if .enableInjection() is already present
            if ! grep -q ".enableInjection()" "$tempfile"; then
                # Insert .enableInjection() before the closing brace of the body
                sed -i '' -e "${body_end_line}i\\
        .enableInjection()" "$tempfile"
            fi
        fi
    fi

    # Check if modifications were made and overwrite the original file
    if ! cmp -s "$filepath" "$tempfile"; then
        mv "$tempfile" "$filepath"
        echo "Modified: $filename"
    else
        echo "No changes for: $filename"
    fi

    rm -f "$tempfile"
}

# Main script
find "$SRCROOT" -name "*.swift" -print0 | while IFS= read -r -d $'\0' filepath; do
    modify_swift_file "$filepath"
done

echo "Inject modification script completed."

iOS 12

你需要将 -weak_framework SwiftUI 添加到 iOS 12 的 Other Linker Flags 中才能工作。

Composable Architecture

自从引入 ReducerProtocol 之后,你可以在没有支持代码的情况下将 Inject 与 TCA 一起使用。