此文章翻译自此链接:https://medium.com/@09mejohn/managing-sync-and-async-tasks-in-ios-de5e8c674fa
说明
如果您使用iOS应用程序,则可能遇到了异步任务,例如API请求,图像处理,数据下载等。如果是这样,那么会知道这可能是一个痛苦的过程,尤其是需要串联这些异步任务时。 在本文中,我希望解释如何让这一过程更简单。
我一直在为正在使用的iOS应用程序进行JSON解析。 该功能需要多个API请求以及一些数据处理和解析,然后才能填充结果并将其适当地呈现给用户。 即使使用了委托,完成块,事件处理程序等技术,我仍然发现我的代码处理非常粗糙,难以维护且难以测试。 让我们看一个例子来说明我的沮丧。
func fetchSomeJSON(completion: @escaping Completion) {
let URLPath = "https://...."
do {
let request = try makeGETRequestJob(witURLPath: URLPath)
performNetworkRequestJob(request: request) { [weak self] data, response, error in
guard let responseData = data else {
completion(.failure(error))
return
}
do {
let comments = try self?.parseResponse(fromData: responseData) ?? []
completion(.success(comments))
} catch let error {
completion(.failure(error))
}
}
} catch let error {
completion(.failure(error))
}
}
}
上面是一个非常简单的函数,负责执行网络请求以获取一些JSON。 该函数带有一个完成模块,该模块提供了注释模型的集合。
尽管以上实现看起来合理,但效果并不理想,但我们可以做得更好! 首先,让我们看一下上述功能中的一些问题。
缺乏控制-是的,上述实现不受控制。 此类的调用者无法控制该请求,即在需要时无法继续,取消或暂停该请求。
不可维护-如果由于某种原因另一个任务/工作(例如执行数据转换或调用另一个JSON API),则函数fetchSomeJSON以及测试用例将需要进行很大的更改。
难以测试-上面的代码结构使测试变得非常困难,因为模拟变得很难应用,并且许多人最终编写了变通办法来测试生产代码。
瀑布任务介绍
您可以从Javascript记住此名称。 您是正确的,但请不要拒绝,因为我不会提出JavaScript解决方案,而是提出Swift解决方案(希望能提供更多控制和类型安全性)。
您可以将瀑布任务视为一系列接连运行的任务。 这些任务可以是同步的也可以是异步的,同步任务的一个示例可以是构造URLRequest对象,而对于异步任务可以执行网络请求。
瀑布任务接收一系列任务,并在运行时按顺序执行这些任务,每个任务将其结果传递给数组中的下一个任务。 但是,如果任何任务将错误传递给完成块,则不执行下一个功能,并且立即用错误调用主完成块。 在处理这些任务时,我们始终从第一个任务开始,并在开始下一个任务之前先完成它。 它的行为与FIFO队列(先进先出)相同。
到现在为止,您可能已经猜到了瀑布任务的含义,如果没有,我相信这在最后将更有意义。 因此,让我们看一下如何用代码编写它。
实施瀑布任务
让我们从协议开始。
定义控件的协议。
该协议定义了对我们任务的控制,即在需要时启动,取消或恢复任务。 我们的Waterfall将符合此协议,并且这样做将使我们能够控制任务的执行。 一个很好的例子是,在viewDidDisappear上,您可以告诉Waterfall任务取消所有正在运行的任务,以防止出现任何异步问题。
public protocol Task {
/// Determines whether the task is running.
var isRunning: Bool { get }
/// Boolean stating if the task was cancelled.
var isCancelled: Bool { get }
/// Start the task
func start()
/// Resume a currently suspended or non-started task.
func resume()
/// Cancels the task.
func cancel()
}
定义工作的协议。
该协议定义了向我们的瀑布任务添加多个工作的功能。 通过使瀑布任务适应此问题,我们可以改善前面提到的两个问题:可维护性和可测试性,因为可以按任意顺序添加多个工作。 这些工作也可以被模拟,这使得测试更加容易。
public protocol Tasker: class {
typealias JobType = (TaskResult) throws -> Task
/// 增加一个执行任务
///
/// - 参数 job: 需要执行的任务
func add(job:@escapingJobType)
/// 增加所有的执行任务
///
/// - 参数 jobs: 需要执行的任务列表
func add(jobs: [JobType])
}
结构体连接任务
如在Tasker协议中所见,类型JobType是采用TaskResult的块。 TaskResult对象包含在上一个和下一个任务握手期间使用的属性。 此对象中的userInfo属性是上一个任务的响应,例如,它是网络请求或URLRequest对象的Foundation.Data类型。
TaskResult还包含一个continueWithResults块,该块负责执行切换。 当我们定义Waterfall的具体实现时,这种逻辑将变得显而易见,但简而言之,该块将获得一个Result类型,该类型具有与响应相关联的成功案例或当前运行任务中带有错误的失败案例。
让我们看一下TaskResult的具体代码。
public struct TaskResult {
/// A block to define handshake from previous and next function
public typealias ContinueWithResultsType = (Result) -> Void
/// The data object of the previous function
public var userInfo: Any?
/// This is executed to let the waterfall know it has finished its task
public var continueWithResults: ContinueWithResultsType
public init(userInfo: Any?, continueWithResults:@escapingContinueWithResultsType) {
self.currentTask = currentTask
self.userInfo = userInfo
self.continueWithResults = continueWithResults
}
}
实现瀑布任务。
通过解释瀑布任务的设置,我们终于可以看一下瀑布任务的实现。
瀑布任务类符合我们之前讨论的Task和Tasker协议。 该类使用userInfo和完成块初始化。 将userInfo传递给第一个任务,可以选择省略此选项,这意味着第一个任务不会获得任何userInfo信息,但是借助Swift中的块功能,我们可以传递其他参数而无需通过Waterfall链(在下面的示例中很明显)。 如您所料,完成块在所有作业完成时或引发错误时调用。 该块将传递带有成功和失败案例的Result枚举。
public class Waterfall: Task {
//********** HELPERS *********
public enum TaskError: Error {
case noResult
}
public typealias JobType = Tasker.JobType
public typealias CompletionType = (Result) -> Void
//*****************************
public var isRunning: Bool = false
public var isCancelled: Bool = false
private lazy var jobs: [JobType] = []
private var currentJobTask: Task?
private let userInfo: Any?
private var completionBlock: CompletionType
/// Initialise with a userInfo and completion block.
public init(with userInfo: Any? = nil,
completionBlock:@escapingCompletionType) {
self.userInfo = userInfo
self.completionBlock = completionBlock
}
......
}
以下是瀑布任务类中start,resume和cancel函数的实现细节。 这些功能的实现细节不言自明,并在需要时相应地调用。
public func start() {
guard currentJobTask == nil else {
assertionFailure("Waterfall is already executing, suspended or cancelled")
return
}
isRunning = true
continueBlock(userInfo)
}
public func resume() {
if let current = currentJobTask {
current.resume()
} else {
start()
}
}
public func cancel() {
isRunning = false
isCancelled = true
currentJobTask?.cancel()
}
函数ContinueBlock(下)是瀑布的跳动心脏。 该函数在启动时被调用,并在下一个作业准备好执行时递归调用。
private func continueBlock(_ userInfo: Any?) -> Void {let result = TaskResult(currentTask: self,
userInfo: userInfo) { [weak self] currentTaskResult in
switch currentTaskResult {
case .success(let result):
self?.continueBlock(result)
case .failure(let error):
self?.finish(error: error)
}
}
do {
self.currentJobTask = try self.jobs.removeFirst()(result)
self.currentJobTask?.resume()
} catch let error {
self.finish(error: error)
}
}
使用瀑布任务
现在我们已经完成了实现细节,现在让我们看看如何使用它。
func fetchSomeJSON(completion:@escapingCompletion) -> Task {
let URLPath = "https://...."
let waterfallTask = Waterfall(completionBlock: completion)
waterfallTask.add(jobs: [
self.makeGetRequestTask(withURLPath: URLPath),
self.performNetworkRequestTask(),
self.parseResponseTask()
])
return waterfallTask}
我们之前看到的fetchSomeJSON函数看起来更简单易懂。 由于可以添加新任务并且可以适当地删除或订购现有任务,因此维护该功能也更加容易。 现在可以简化测试,因为可以对添加到块中的函数进行模拟,以确保它们获得适当的结果并按适当的顺序调用。
通过将此技术用于更复杂的任务,例如图像处理,下载或执行任何系列的异步或同步任务,您的项目可以在代码可读性和可维护性方面大大受益。 尝试一下此方法,如果您有任何建议,请告诉我。
完整的实施细节和示例可以在这里找到(https://github.com/Melvin24/WaterfallTask)。
个人感想:
此文章不只是针对处理同步和异步,在另一方面,这个是控制反转、依赖注入、面向接口编程的一个很好的例子。
面向接口编程最新理解:需要将方法抽象化,将具体实现放到特定的地方,让需要使用的类和具体实现类通过接口来相互持有,这样减低耦合性。