Point-Free 的 swift-dependencies
的配套库,提供更高级别的依赖项。
Dependencies 是一个出色的库,可以帮助您以类似于 SwiftUI 处理其 Environment
的方式管理依赖项。Dependencies
已经附带了许多内置的基本依赖项,例如 clock
、uuid
、date
等。
“Dependencies Additions” 旨在扩展这些核心依赖项,并为在 Apple 平台上开发时常用的许多其他依赖项提供连贯且可测试的实现。
该库目前提出了几个底层依赖项来与以下内容交互
Accessibility
,对 UIAccessibility
的抽象;Application
,对 UIApplication.shared
的抽象;AssertionDependency
,用于抽象 assert(…)
调用并在测试时将其提升为失败;Dependencies
中可用)BundleInfo
,对应用程序的 info.plist
的抽象;Codable
,用于将 Codable
类型编码/解码为 Data
;Compression
,使用 `Compression` 框架压缩/解压缩 Data
;DataReader/Writer
,用于从 URL
读取/写入 Data
(来自 David Roman 的想法);Logger
,公开一个隐私感知的 Logger
实例;NotificationCenter
;PersistentContainer
,抽象 CoreData NSPersistentContainer
;UserDefaults
;UserNotificationCenter
;Path
,AnyHashable
的通用集合,您可以将标识符推入和弹出以情境化您的模型;ProcessInfo
;Device
(UIDevice
、WKInterfaceDevice
、DCDevice
等)。它还附带了更多实验性和更高级别的抽象,用于
AppStorage
,它提出了一个 @Dependency.AppStorage
属性包装器,该包装器模仿了 SwiftUI
的 @AppStorage
,但可以从您的模型或任何并发上下文中使用。CoreData
,它尝试公开一个安全且方便的接口来访问您的 CoreData
图(WIP)。Notification
,它以类型化和可控的 AsyncSequence
的形式公开 NotificationCenter
的通知。SwiftUI
的 Environment
,它在您的模型中重新发布 SwiftUI
的 Environment
值。这些更高级别的依赖项目前都是实验性的,并且它们的目标都以下划线命名。如果它们的大小/行为证明是合理的,它们最终可能会从 Dependencies Additions
中演变出来,成为专门的存储库。
该库还提出了一些对“核心”依赖项的直接扩展,例如一些新的日期和随机数生成器,以及一些帮助混合 AsyncSequence
和 Combine 的工具。
此列表是初步的,并且在未来几周内将向该库添加许多新的依赖项。如果您需要某个特定的依赖项,请随时发起讨论,以便我们找到将其与其他依赖项集成的更好方法。
该库提出了许多异构依赖项。将所有这些依赖项捆绑在同一个存储库下有很多好处
您可以简单地导入 DependenciesAdditions
umbrella 产品以一次访问所有依赖项。如果您更喜欢更多控制,并且因为它们的每个依赖项都包含在自己的模块中,您可以“按需”逐个文件地仅导入您需要的依赖项。
添加 swift-dependencies-additions
包,并且仅选择 “DependenciesAdditions” 产品
在 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
等实验性抽象更感兴趣,您可以直接跳转到 更高级别的依赖项 部分。
对 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")
}
对 UIAccessibility
的抽象,您可以使用它来监视应用程序实例的可访问性状态。
例如
class Model {
@Dependency(\.accessibility.isClosedCaptioningEnabled) var isClosedCaptioningEnabled
func play() -> Void {
if self.isClosedCaptioningEnabled {
self.updateClosedCaptions()
}
}
}
这个简单的依赖项公开了一个 BundleInfo
类型,该类型允许简单地检索一些与 info.plist
相关的字段,例如 bundleIdentifier
或应用程序的 version
。
例如
@Dependency(\.bundleInfo.bundleIdentifier) var bundleIdentifier
由于此值通常用于前缀标识符,因此将此值作为依赖项公开允许您在测试时远程控制它。
该库公开了两个依赖项,以帮助编码或解码您的 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 编码器和解码器非常相似。
默认情况下,encode
和 decode
正在生成/使用 JSON
数据。
与 encode
和 decode
相同,该库公开了两个依赖项来压缩和解压缩 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)
默认情况下,compress
和 decompress
正在使用 `.zlib` 算法。
此依赖项公开了一个隐私感知的 Logger
实例。 @Dependency(.logger) var logger
您可以简单地将其用作
logger.log(level: .info, "User with id: \(userID, privacy: .private) did purchase a smoothie")
您可以使用提供的下标简单地创建一个子系统
@Dependency(\.logger["Transactions"]) var transactionsLogger
一个 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
的简单抽象,允许检索系统上的底层信息。
@Dependency(\.processInfo.thermalState) var thermalState
if thermalState == .critical {
self.disableFancyAnimations()
}
因为它是一个依赖项,所以您可以非常轻松地对其进行测试,而无需修改您的模型。
对 UserDefaults
的抽象,您可以在其中读取和保存用户首选项。该库公开了与 SwiftUI 的 AppStorage 相同的类型,因此您可以简单地存储和检索您的数据。
@Dependency(\.userDefaults) var userDefaults
userDefaults.set(true, forKey: "hasUserPassedOnboarding")
只需一行代码,您就可以使您的整个应用程序写入您的应用程序组用户默认设置、用于测试的内存版本,甚至写入 NSUbiquitousKeyValueStore
,后者可以通过 iCloud 同步用户首选项。
您还可以尝试更强大的 _AppStorage
依赖项,该依赖项构建在 \.userDefaults
之上,并且允许使用类似于 SwiftUI 的 AppStorage
的 API (它可以与之互操作) 无缝地观察和分配用户首选项。
还有许多其他依赖项可用,例如用于显示通知的 UserNotifications
、用于与 UIDevice
或 WKInterfaceDevice
交互的 Device
、用于情境化模型树的 Path
、由 Clock
控制的点击 DateGenerator
(您可以控制它本身)等。
当然,这仅仅是开始,并且在未来几周内将添加许多其他依赖项。我们强烈认为,依赖项频谱越大,您将越多地使用它们,并且您的代码将更具可测试性和结构性。
该库提出了一些实验性的更高级别的依赖项。它们目前是“下划线的”,这意味着它们的 API 尚未最终确定。将来它们可能会被提取到它们自己的库中。
@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()
}
此依赖项允许将 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
公开的值是表示 batteryLevel
的 Float
的异步序列
for await level in batteryLevel {
if level < 0.2 {
self.isLowPowerModeEnabled = true
}
}
此依赖项将 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)")
}
此依赖项仍在 WIP 中,因为我们希望加强 API 以避免 CoreData 的常见陷阱。但是您可以在 CoreData CaseStudy 中获得它的摘录!
这仅仅是开始!还有许多其他依赖项要实现:Speech
、Vision
、KeyChain
等…… 目前唯一的规则是它本身不应需要第三方依赖项,并且应在 Apple
或 Linux
平台上开箱即用。如果您想贡献一个依赖项,请随时在讨论中发起一个主题!
您可以通过将 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。