依赖项添加

Point-Free 的 swift-dependencies 的配套库,提供更高级别的依赖项。

CI

菜单

Dependencies 是一个出色的库,可以帮助您以类似于 SwiftUI 处理其 Environment 的方式管理依赖项。Dependencies 已经附带了许多内置的基本依赖项,例如 clockuuiddate 等。

“Dependencies Additions” 旨在扩展这些核心依赖项,并为在 Apple 平台上开发时常用的许多其他依赖项提供连贯且可测试的实现。

该库目前提出了几个底层依赖项来与以下内容交互

它还附带了更多实验性和更高级别的抽象,用于

这些更高级别的依赖项目前都是实验性的,并且它们的目标都以下划线命名。如果它们的大小/行为证明是合理的,它们最终可能会从 Dependencies Additions 中演变出来,成为专门的存储库。

该库还提出了一些对“核心”依赖项的直接扩展,例如一些新的日期和随机数生成器,以及一些帮助混合 AsyncSequence 和 Combine 的工具。

此列表是初步的,并且在未来几周内将向该库添加许多新的依赖项。如果您需要某个特定的依赖项,请随时发起讨论,以便我们找到将其与其他依赖项集成的更好方法。

如何使用 Dependencies Additions

该库提出了许多异构依赖项。将所有这些依赖项捆绑在同一个存储库下有很多好处

您可以简单地导入 DependenciesAdditions umbrella 产品以一次访问所有依赖项。如果您更喜欢更多控制,并且因为它们的每个依赖项都包含在自己的模块中,您可以“按需”逐个文件地仅导入您需要的依赖项。

使用 Xcode 包依赖项

添加 swift-dependencies-additions 包,并且仅选择 “DependenciesAdditions” 产品

使用 SwiftPM

dependencies 部分,添加

.package(url: "https://github.com/tgrapperon/swift-dependencies-additions", from: "0.1.0")

在您需要访问这些依赖项的每个模块中,添加

.target(
  name: "MyModule",
  dependencies: [
    .product(name: "DependenciesAdditions", package: "swift-dependencies-additions")
  ]
),

这将允许访问所有非下划线的依赖项。实验性依赖项需要单独导入。例如

.product(name: "_AppStorage", package: "swift-dependencies-additions")

依赖项快速浏览

我们在此介绍库中当前附带的一些依赖项。如果您对 AppStorage 或类型化的 Notification 等实验性抽象更感兴趣,您可以直接跳转到 更高级别的依赖项 部分。

Application

UIApplication 的抽象,您可以使用它与应用程序的实例进行通信。

例如

class Model {
  @Dependency(\.application) var application

  func setAlternateIcon(name: String) async throws {
    try await self.application.setAlternateIconName(name)
  }
}

然后,在测试时

@MainActor 
func testAlternateIconIsSet() async throws -> Void {
  var alternateIconName = LockIsolated("")
  let model = withDependencies {
    $0.application.$setAlternateIcon = { name in
      alternateIconName.withValue { $0 = name }
    }
  } operation: { Model() }
  try await model.setAlternateIcon(name: "blueprint")
  XCTAssertEqual(alternateIconName.value, "blueprint")
} 

Accessibility

UIAccessibility 的抽象,您可以使用它来监视应用程序实例的可访问性状态。

例如

class Model {
  @Dependency(\.accessibility.isClosedCaptioningEnabled) var isClosedCaptioningEnabled

  func play() -> Void {
    if self.isClosedCaptioningEnabled {
      self.updateClosedCaptions()
    }
  }
}

BundleInfo

这个简单的依赖项公开了一个 BundleInfo 类型,该类型允许简单地检索一些与 info.plist 相关的字段,例如 bundleIdentifier 或应用程序的 version

例如

@Dependency(\.bundleInfo.bundleIdentifier) var bundleIdentifier

由于此值通常用于前缀标识符,因此将此值作为依赖项公开允许您在测试时远程控制它。

Codable

该库公开了两个依赖项,以帮助编码或解码您的 Codable 类型。

@Dependency(\.encode) var encode
@Dependency(\.decode) var decode

struct Point: Codable {
  var x: Double
  var y: Double
}

let point = Point(x: 12, y: 35)
let encoded = try encode(point) // A `Data` value
let decoded = try decode(Point.self, from: encoded) // A `Point` value

如您所见,API 与 JSON 或 PropertyList 编码器和解码器非常相似。

默认情况下,encodedecode 正在生成/使用 JSON 数据。

Compression

encodedecode 相同,该库公开了两个依赖项来压缩和解压缩 Data,使用 Apple 的 Compression 框架

@Dependency(\.compress) var compress
@Dependency(\.decompress) var decompress

let uncompressed = "Lorem ipsum dolor sit amet".data(using: .utf8)!
let compressed = try compress(uncompressed, using: .lzfse)
let decompressed = try decompress(compressed, using: .lzfse)

它们也可以从异步上下文调用,在异步上下文中使用了更有效的变体

let compressed = try await compress(uncompressed)
let decompressed = try await decompress(compressed)

默认情况下,compressdecompress 正在使用 `.zlib` 算法。

Logger

此依赖项公开了一个隐私感知的 Logger 实例。 @Dependency(.logger) var logger

您可以简单地将其用作

logger.log(level: .info, "User with id: \(userID, privacy: .private) did purchase a smoothie")

您可以使用提供的下标简单地创建一个子系统

@Dependency(\.logger["Transactions"]) var transactionsLogger

PersistentContainer

一个 NSPersistentContainer,它公开了 Core Data NSManagedObjectContext。您可以将其用作更精细抽象的基础。

@Dependency(\.persistentContainer) var persistentContainer

默认情况下,预览版本是一个 in-memory 变体,您可以轻松地为您的 SwiftUI 预览设置模拟

var previews: some View {
  let model = withDependencies {
    $0.persistentContainer = .default(inMemory: true).with { context in
      let smoothie = Smoothie(context: context)
      smoothie.flavor = "Banana"
    }
  }
  SmoothieView(model: model)
}

ProcessInfo

ProcessInfo 的简单抽象,允许检索系统上的底层信息。

@Dependency(\.processInfo.thermalState) var thermalState

if thermalState == .critical {
  self.disableFancyAnimations()
}

因为它是一个依赖项,所以您可以非常轻松地对其进行测试,而无需修改您的模型。

UserDefaults

UserDefaults 的抽象,您可以在其中读取和保存用户首选项。该库公开了与 SwiftUI 的 AppStorage 相同的类型,因此您可以简单地存储和检索您的数据。

@Dependency(\.userDefaults) var userDefaults

userDefaults.set(true, forKey: "hasUserPassedOnboarding")

只需一行代码,您就可以使您的整个应用程序写入您的应用程序组用户默认设置、用于测试的内存版本,甚至写入 NSUbiquitousKeyValueStore,后者可以通过 iCloud 同步用户首选项。

您还可以尝试更强大的 _AppStorage 依赖项,该依赖项构建在 \.userDefaults 之上,并且允许使用类似于 SwiftUI 的 AppStorage 的 API (它可以与之互操作) 无缝地观察和分配用户首选项。

其他依赖项

还有许多其他依赖项可用,例如用于显示通知的 UserNotifications、用于与 UIDeviceWKInterfaceDevice 交互的 Device、用于情境化模型树的 Path、由 Clock 控制的点击 DateGenerator(您可以控制它本身)等。

当然,这仅仅是开始,并且在未来几周内将添加许多其他依赖项。我们强烈认为,依赖项频谱越大,您将越多地使用它们,并且您的代码将更具可测试性和结构性。

更高级别的依赖项

该库提出了一些实验性的更高级别的依赖项。它们目前是“下划线的”,这意味着它们的 API 尚未最终确定。将来它们可能会被提取到它们自己的库中。

AppStorage

@Dependency.AppStorage("username") var username: String = "Anonymous"

API 遵循 SwiftUI 的 AppStorage,但由 @Dependency(\.userDefaults) 支持。它可以在您的模型中运行,并可以从异步上下文访问。如果使用相同的 key,它可以与 SwiftUI 自己的 AppStorage 互操作。投影值是此用户首选项值的 AsyncStream<Value>。它们可以从任何异步上下文中观察到

@Dependency.AppStorage("isSoundEnabled") var isSoundEnabled: Bool = false

for await isSoundEnabled in $isSoundEnabled {
  await isSoundEnabled ? audioEngine.start() : audioEngine.stop()
}

Notifications

此依赖项允许将 Notification 作为类型化的 AsyncSequence 公开。

extension Notifications {
  /// A typed `Notification` that publishes the current device's battery level.
  @MainActor
  public var batterLevelDidChange: SystemNotificationOf<Float> {
    .init(UIDevice.batteryLevelDidChangeNotification) { notification in
      @Dependency(\.device.batteryLevel) var level;
      return level
    }
  }
}

然后,您可以使用专用的属性包装器公开此通知

@Dependency.Notification(\.batteryLevelDidChange) var batteryLevel

公开的值是表示 batteryLevelFloat 的异步序列

for await level in batteryLevel {
  if level < 0.2 {
    self.isLowPowerModeEnabled = true
  }
}

SwiftUI Environment

此依赖项将 SwiftUI 的 Environment 带入您的模型

@Dependency.Environment(\.colorScheme) var colorScheme
@Dependency.Environment(\.dismiss) var dismiss

然后,在任何 View 中,您使用 .observeEnvironmentAsDependency(\.colorScheme) 修饰符将此值冒泡到模型中

HStack {  }
  .observeEnvironmentAsDependency(\.colorScheme)
  .observeEnvironmentAsDependency(\.dismiss)

在上面的示例中,self.colorScheme 是一个 ColorScheme?,而 self.dismissAction 是一个 DismissAction?。两者都是可选的,因为它们受 View 的存在的制约,并且如果此视图消失,它们可能会再次变为 nil。您可以通过投影值观察它们的值,投影值是包装值的 AsyncSequence

for await colorScheme in self.$colorScheme.compactMap{ $0 }.dropFirst() {
  self.logger.info("ColorScheme did change: \(colorScheme)")
}

Core Data (WIP)

此依赖项仍在 WIP 中,因为我们希望加强 API 以避免 CoreData 的常见陷阱。但是您可以在 CoreData CaseStudy 中获得它的摘录!

接下来是什么?

这仅仅是开始!还有许多其他依赖项要实现:SpeechVisionKeyChain 等…… 目前唯一的规则是它本身不应需要第三方依赖项,并且应在 AppleLinux 平台上开箱即用。如果您想贡献一个依赖项,请随时在讨论中发起一个主题!

安装

您可以通过将 DependenciesAdditions 作为包添加到您的项目,将其添加到 Xcode 项目中。

https://github.com/tgrapperon/swift-dependencies-additions

如果您想在 SwiftPM 项目中使用 DependenciesAdditions,只需将其添加到您的 Package.swift 即可

dependencies: [
  .package(url: "https://github.com/tgrapperon/swift-dependencies-additions", from: "1.0.0")
]

许可证

该库在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE。