ProcedureKit
这是一个受 WWDC 2015 Advanced NSOperations 会话启发的 Swift 框架。之前被称为 Operations,由 @danthorpe 开发,并得到了我们出色的社区的大力支持。
资源 | 在哪里可以找到 |
---|---|
会话视频 | developer.apple.com |
旧但更完整的参考文档 | docs.danthorpe.me/operations |
更新但尚未完成的参考文档 | procedure.kit.run/development |
编程指南 | operations.readme.io |
兼容性
ProcedureKit 支持所有当前 Apple 平台。最低要求是
- iOS 9.0+
- macOS 10.11+
- watchOS 3.0+
- tvOS 9.2+
当前发布的 ProcedureKit 版本(5.1.0)支持 Swift 4.2+ 和 Xcode 10.1。《开发》分支与 Swift 5 和 Xcode 10.2 兼容。
框架结构
ProcedureKit 是一个 "多模块" 框架(不要费心去谷歌搜索这个名词,我只是自己定义的)。我的意思是,Xcode 项目有多达多个目标/产品,每个目标/产品都会生成一个 Swift 模块。其中一些模块是跨平台的,其他的则针对特定平台,例如 ProcedureKitNetwork
与 ProcedureKitMobile
。
安装ProcedureKit
请参阅安装ProcedureKit指南。
用法
Procedure
是Foundation.Operation
子类。它是一个抽象类,必须被继承。
import ProcedureKit
class MyFirstProcedure: Procedure {
override func execute() {
print("Hello World")
finish()
}
}
let queue = ProcedureQueue()
let myProcedure = MyFirstProcedure()
queue.add(procedure: myProcedure)
这里的关键点是
- 继承
Procedure
- 重写
execute
但不调用super.execute()
- 在所有工作完成后或在流程被取消后,始终调用
finish()
。这可以异步完成。 - 将流程添加到
ProcedureQueue
实例中。
观察者
观察者附加到Procedure
子类。当生命周期事件发生时,它们会接收回调。生命周期事件包括:did attach、will execute、did execute、did cancel、will add new operation、did add new operation、will finish和did finish。
这些方法通过协议定义,因此可以编写自定义类以符合多个事件。但是,存在基于块的方法来更自然地添加观察者。例如,观察当流程完成时
myProcedure.addDidFinishBlockObserver { procedure, errors in
procedure.log.info(message: "Yay! Finished!")
}
框架还提供了BackgroundObserver
、TimeoutObserver
和NetworkObserver
。
有关更多信息,请参阅关于[[观察者|Observer]]的wiki。
条件
条件附加到Procedure
子类。在流程准备执行之前,它会异步地异步评估它的所有条件。如果有任何条件失败,它将结束并显示错误,而不是执行。例如
myProcedure.add(condition: BlockCondition {
// procedure will execute if true
// procedure will be ignored if false
// procedure will fail if error is thrown
return trueOrFalse // or throw AnError()
}
条件可以相互排斥。这类似于一个锁定阻止执行具有相同排斥的其他操作。
框架提供了以下条件:AuthorizedFor
、BlockCondition
、MutuallyExclusive
、NegatedCondition
、NoFailedDependenciesCondition
、SilentCondition
和UserConfirmationCondition
(在ProcedureKitMobile中)。
有关更多信息,请参阅关于[[条件|Conditions]]的wiki,或查看旧编程指南中的条件|。
能力
“能力”代表了应用访问设备或用户账户能力,或者可能是任何形式的受控资源的能力。例如,位置服务、云容器、日历等或者是网络服务。《CapabiltiyProtocol》提供了一致的模型来
- 使用
GetAuthorizationStatusProcedure
检查当前授权状态 - 使用
AuthorizeCapabilityProcedure
明确请求访问 - 上述两项作为一个名为
AuthorizedFor
的条件。
例如
import ProcedureKit
import ProcedureKitLocation
class DoSomethingWithLocation: Procedure {
override init() {
super.init()
name = "Location Operation"
add(condition: AuthorizedFor(Capability.Location(.whenInUse)))
}
override func execute() {
// do something with Location Services here
finish()
}
}
ProcedureKit提供了以下能力:Capability.CloudKit
和Capability.Location
。
在Operations(本框架的先前版本)中,存在更多的功能(如日历、健康、照片、通讯录等),我们仍在考虑如何在ProcedureKit中提供这些功能。
有关更多信息,请参阅关于[[能力|能力]]的wiki,或者有关能力的旧编程指南。
日志记录
Procedure
有自己内部的日志功能,通过log
属性公开
class LogExample: Procedure {
override func execute() {
log.info("Hello World!")
finish()
}
}
有关更多信息,请参阅有关日志记录和支持第三方日志框架的编程指南。
依赖注入
通常,程序在执行前需要依赖项。在异步/事件特性的应用中,这些依赖项可能在创建时并不知道。因此,它们必须在程序初始化后,但在执行前进行注入。《ProcedureKit》通过一系列协议和类型支持此操作。我们认为这种模式很棒,因为它鼓励小型单一用途程序的组合。这些程序可能更容易测试,并可能实现更大的重用。你会发现,在整个框架中都要使用并鼓励这种依赖注入。
首先,一个值可能是已准备或待定。例如,当程序初始化时,它可能没有所有的依赖项,因此它们处于待定状态。希望它们能在执行时变为准备就绪。
其次,如果程序正在获取另一个程序所需的依赖项,它可能成功,也可能由于错误而失败。因此,有一个简单的Result类型来支持这一点。
第三,有协议来定义input
和output
属性。
《InputProcedure》将一个 Input
类型关联。一个 Procedure
子类可以遵守这个协议,以便进行依赖注入。请注意,仅支持一个 input
属性,因此,创建中间结构类型来包含多个依赖项。当然,input
属性是一个挂起值类型。
《OutputProcedure》通过它的 output
属性公开关联的 Output
类型,这是一个挂起结果类型。
将所有这些整合在一起的是一组在 InputProcedure
上的 API,它允许将依赖项连接起来。如下所示
import ProcedureKitLocation
// This class is part of the framework, it
// conforms to OutputProcedure
let getLocation = UserLocationProcedure()
// Lets assume we've written this, it
// conforms to InputProcedure
let processLocation = ProcessUserLocation()
// This line sets up dependency & injection
// it automatically handles errors and cancellation
processLocation.injectResult(from: getLocation)
// Still need to add both procedures to the queue
queue.add(procedures: getLocation, processLocation)
在上面的例子中,假设 Input
类型与 Output
类型匹配,在这种情况下,是 CLLocation
。然而,也可以使用闭包将输出类型调整为所需的输入类型,例如
import ProcedureKitLocation
// This class is part of the framework, it
// conforms to OutputProcedure
let getLocation = UserLocationProcedure()
// Lets assume we've written this, it
// conforms to InputProcedure, and
// requires a CLLocationSpeed value
let processSpeed = ProcessUserSpeed()
// This line sets up dependency & injection
// it automatically handles errors and cancellation
// and the closure extracts the speed value
processLocation.injectResult(from: getLocation) { $0.speed }
// Still need to add both procedures to the queue
queue.add(procedures: getLocation, processLocation)
那么刚刚发生了什么呢?好的,injectResult
API 有一个可以接受尾部闭包的变体。闭包接收输出值,必须返回输入值(或者抛出错误)。所以,'{ $0.speed }
'将返回用户 CLLocation
实例的速度属性。
这里需要注意的是,此闭包是同步运行的。因此,最好不要在它上面放置繁重的任务。如果您需要进行更复杂的数据映射,请检查TransformProcedure
和AsyncTransformProcedure
。
请参阅有关注入结果的编程指南,以获取更多信息。