License Compatibility Compatibility Compatibility

Stitch

一个轻量级的、受 SwiftUI 启发的、编译时安全的依赖注入 (DI) 库,提供依赖映射,无需代码生成工具。

Stitch 的编译时安全性确保在整个开发过程中都充满信心。如果编译通过,则配置正确。不会像典型的依赖容器实现那样,在未注册依赖项时发生运行时故障。

Stitch 使用新的 Swift 5.9+ 宏来建模其依赖关系图,以便于使用,并为从图中检索对象提供适当的 @propertyWrapper 实现。

Stitch 提供以下关键功能

  1. 嵌入在 SwiftUI 生命周期中,具有 SwiftUI 人体工程学 (就像使用其他 SwiftUI 属性包装器一样使用它)
  2. SwiftUI 视图中的协议 DI (扩展 ObservableObject 以使用协议类型并触发视图更新)。请参阅:StitchObservable
  3. Combine 发布者访问协议类型属性 (无需用 Publisher<Object,Never> vars 注释您的协议并在您的对象中转发) 请参阅:StitchPublished

紧急情况下的 Stitch

在您的应用程序中使用 Stitch 非常简单,旨在以最少的样板代码或代码生成提供丰富的功能。Stitch 通过使用 Swift 宏来实现其编译时安全性,该宏有助于将依赖项缝合在一起以进行解析和注入。

要注册依赖项

  1. 使用 @Stitchify 注释您的类或结构
  2. 就是这样! 真的很简单。
/// Protocol abstraction for our dependency
@Stitchify
struct Logger {
    func log(message: String) {
        ...
    }
}

现在,可以使用 Stich 属性包装器从代码库中的任何位置解析此依赖项,并使用依赖项的类型进行查找。

class Model {
    @Stitch(Logger.self) var logger
    
    func doSomething() {
        logger.log("Logging a message")
    }
}

这就是 Stitch 在一个高层次上的概述,易于实现,并且具有对具体类型和协议 DI 的灵活性。 有关详细说明和高级实现,请参阅高级缝合部分。

开始缝合

要开始使用 Stitch,您必须首先将其作为依赖项安装在您的项目中。

使用 Swift Package Manager您可以通过将其添加为包依赖项来将 Stitch 添加到 Xcode 项目中

  1. 选择 File -> Add packages
  2. 输入 https://github.com/entrhq/stitch.git 作为包存储库 URL。
  3. 将 Stitch 库添加到您所需的目标。

将 Stitch 添加到 Swift 包

dependencies: [
    .package(url: "https://github.com/entrhq/stitch.git", .upToNextMajor(from: "VERSION")),
],
targets: [
    .target(
        name: "YOUR PACKAGE",
        dependencies: [
            "Stitch",
        ]
    ),
],

示例

Stitch 存储库中包含一个示例项目,演示了设置和使用 Stitch。它涵盖了

高级缝合

通过协议使用 Stitch

有时我们希望使用协议而不是其具体类型来注入依赖项。当您的架构更倾向于抽象时,这是一个重要的考虑因素。无论您有什么原因需要对具体类型进行抽象; 无论是为了松散耦合、模块化架构,还是仅仅为了创建模拟和测试替身来代替网络调用,都可以简单地通过其协议类型而不是其具体类型来“键控”您的依赖注入。

protocol SomeNetworkAbstraction {
    func post(resource String) -> Response
}

@Stitchify(by: SomeNetworkAbstraction.self)
struct NetworkImplementation {
    ...
}

只需将 by: 属性添加到 Stitchify 宏,并提供您想要通过其键控依赖项的协议类型。现在,当您使用任何 Stitch 的 @propertyWrappers 访问依赖项时,您将通过其抽象而不是其具体类型来获得依赖项。

struct SomeInteractor {
    @Stitch(NetworkImplementation.self) private var network
    
    func someAction() {
        print(type(of: network)) // == SomeNetworkAbstraction.self
        network.post("/hello")
    }
}

注意:依赖项的键仍然是您用 @Stitchify 注释的实现,这是由于 Swift 宏对全局命名空间中的 peer 宏的限制。

使用 StitchObservable

Stitch 旨在提供在 SwiftUI 环境中灵活使用依赖项,从而使用进一步的功能扩展传统的 SwiftUI 实现。 开箱即用,SwiftUI 同时提供 ObservableObject@ObservedObject 属性包装器,允许开发人员将状态推送到其自己的对象中以管理其生命周期。 这在使用视图中的具体依赖项时效果很好

class Model: ObservableObject {
    @Published var name: String
}

struct HomeView: View {
    @ObservedObject var model: Model
    
    init(model: Model) {
        self.model = model
    }
    
    var body: some View {
        Text("Welcome, \(model.name)")
    }
}

上述实现易于管理,并且您的预览仍然是可管理的

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        HomeView(model: Model())
    }
}

现在在现实世界中,我们的依赖关系可能不会那么简单。 他们可能会与其他层交互并检索数据,或者发出网络请求。 反过来,这将破坏我们的预览,我们如何才能为我们的预览实例创建一个有用的对象。

class Model: ObservableObject {
    var service: Service
    var database: Database
    
    init(service: Service, database: Database) {
        self.service = service
        self.database = database
    }
    
    @Published var name: String
    @Published var isLoaded: Bool
    
    func fetchName() { ... }
}

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        HomeView(
            model: Model(
                service: // How do we create our service nicely?
                database: // How do we create our database nicely?
            )
        )
    }
}

您可能会开始使用协议来帮助清理我们的模型并隐藏其实现细节。 毕竟,我们的视图实际上只关心名称和 isLoaded @Published 属性,而不是其他依赖项和实现细节。

protocol Modelling: ObservableObject {
    var name: String { get set }
    var isLoaded: Bool { get set }
}

class Model: Modelling {
    var service: Service
    var database: Database
    
    init(service: Service, database: Database) {
        self.service = service
        self.database = database
    }
    
    @Published var name: String
    @Published var isLoaded: Bool
    
    func fetchName() { ... }
}

这成功地隐藏了我们模型的实现细节,并且看起来可以帮助我们为预览创建一个有意义的模拟。 但是,我们很快意识到,SwiftUI 的 @ObservedObject 属性包装器仅适用于具体类型。

Stitch 引入了一个新的属性包装器 @StitchObservable,作为通过其协议类型将此依赖项注入到 SwiftUI 视图中的一种方式,并且仍然利用其 @Published 属性状态更新来进行视图重组。

为此,只需将 AnyObservableObject 一致性添加到您已经一致的 ObservableObject,如上所示为 DependencyMap 提供一个条目,并将 @ObservedObject 包装器替换为 @StitchObservable

protocol Modelling: ObservableObject, AnyObservableObject {
    var name: String { get set }
    var isLoaded: Bool { get set }
}

// add our stitchify macro to wire the dependency injection
// using the by property to key the dependency by its protocol instead
// of concrete type
@Stitchify(by: Modelling.self)
class Model: Modelling {
    ...
}

struct HomeView: View {
    // use stitch observable to inject the model
    @StitchObservable(Model.self) var model
    
    var body: some View {
        Text("Welcome, \(model.name)")
    }
}

现在,我们的视图将像以前使用 @ObservedObject 一样进行重组,只要我们的 @Published 属性发生更改,但只会暴露于协议合同中声明的属性。 除此之外,我们的预览实现变得更加容易。 现在我们可以为预览提供一个模拟,允许我们在开发视图时显示测试数据。

struct HomeView_Previews: PreviewProvider, DependencyMocker {
    class MockModel: Modelling {
        @Published var name = "Mock name"
        @Published var isLoading = false
    }
    
    static var previews: some View {
        mockInViewScope(Model.self, mock: MockModel())
        HomeView()
    }
}

现在可以轻松地模拟我们的模型,而无需从预览范围中提供不相关/难以创建的依赖项。

使用 StitchPublished

Stitch 扩展了协议类型中 @Published 属性的功能,并为外部消费者提供与这些属性相同的 Publisher 行为。 通常,可以在具体类中访问属性的 Publisher,但是如上所述,在隐藏实现细节并启用 IOC / DI 时,我们将失去此功能。 我们的协议定义并没有暗示我们的属性用 @Published 注释,也不再允许消费者订阅这些发布者。

这就是 Stitch 引入 @StitchPublished 属性包装器的地方。 与上面的 @StitchObservable 类似,此属性包装器包装我们的协议类型并将发布事件转发给属性包装器的消费者。 但是,StitchPublished 使消费者可以访问视图范围之外的内容,从而为他们提供协议中属性上的订阅者。

这使我们可以订阅对象之外的状态更改,并对已突变的值执行操作。 以下代码订阅了身份验证状态,并在用户身份验证状态更改时执行功能

@StitchPublished 包装器提供了一个投影值访问器前缀 ($),它允许动态成员查找对象的属性。 每个属性都返回 Publisher 包装的值。 此发布者将在其基础值更改时发布更新的值事件,而不是在对象或对象的部分单独更改时发布。 由于 @Published 包装器通知 objectWillChange 发布者其值已更改,但并未明确指定更改了哪个值,因此 StitchPublished 包装器在内部进行自己的差异比较,并且仅将状态更改传播到其值已更改的属性发布者。 这类似于 SwiftUI 如何准备更新视图的方式,仅更新完成差异比较后更改的值。

注意:由于内部差异比较需要可相等泛型一致性,因此只能通过 $ 前缀访问符合 Equatable 的属性。

protocol AuthStoring: ObservableObject, AnyObservableObject {
    var isLoggedIn: Bool { get set }
}

@Stitchify(by: AuthStoring.self)
class AuthStore: AuthStoring {
    @Published var isLoggedIn = false
}

class SomeService {
    @StitchPublished(AuthStore.self) var store
    
    ...
    
    func setup() {
        $store.isLoggedIn.sink { loggedIn in
            if loggedIn {
                // do something logged in
            } else {
                // do something logged out
            }
        }
        .store(in: &cancellables)
    }
}

当我们想将相关状态存储在它们自己单独的 ObservableObjects 中,同时仍然具有交叉的交互时,这变得很有用,从而进一步帮助我们遵循基于反应式组合的体系结构。

许可证

Stitch 是一个开源和免费软件,根据 Apache 2.0 发布