Future和Promise
Future在苹果官方文档的解释为:最终发布一个数据并立即结束。
这个解释过于简单了,其实Future的作用是:我将在未来的某一时刻,发布一个数据,并伴随着成功或失败的状态。
这个功能你可能听起来比较耳熟,对,他就是逃逸闭包(@escaping closure)的功能。
查看Future的源代码,我们可以看到
final public class Future<Output, Failure> : Publisher where Failure : Error {
/// A type that represents a closure to invoke in the future, when an element or error is available.
///
/// The promise closure receives one parameter: a `Result` that contains either a single element published by a ``Future``, or an error.
public typealias Promise = (Result<Output, Failure>) -> Void
/// Creates a publisher that invokes a promise closure when the publisher emits an element.
///
/// - Parameter attemptToFulfill: A ``Future/Promise`` that the publisher invokes when the publisher emits an element or terminates with an error.
public init(_ attemptToFulfill: @escaping (@escaping Future<Output, Failure>.Promise) -> Void)
/// Attaches the specified subscriber to this publisher.
///
/// Implementations of ``Publisher`` must implement this method.
///
/// The provided implementation of ``Publisher/subscribe(_:)-4u8kn``calls this method.
///
/// - Parameter subscriber: The subscriber to attach to this ``Publisher``, after which it can receive values.
final public func receive<S>(subscriber: S) where Output == S.Input, Failure == S.Failure, S : Subscriber
}
它包含一个Promise类型和一个带有@escaping闭包的初始化函数。
Future和PassthroughSubject、CurrentValueSubject一样,是一个类,不同的是Future实现的是Publiser协议,而PassthroughtSubject和CurrentValueSubject实现的是Subject协议,所以后两个可以使用send方法,而Future不能。
Promise是Future的最终结果。就像英文名所表现的那样,一个是承诺,一个是未来。我们要用承诺来初始化一个未来。Future和Promise需要配套使用。
从Future源码可以看到,Future的初始化Init函数中需要实现一个@escaping闭包,闭包类型就是Promise。Promise的本质可以理解为是一个接收单个Result类型参数的闭包。
let future = Future<Int, Never> { promise in
// 1
promise(.success(1))
}
上面的代码是Future的初始化及发布的简单过程,注释为1的行,表示的动作就是,我承诺给你一个数据为Int(1),可以是现在就给予,也可以是在未来给予,比如使用DispatchQueue.main.async 包裹 promise
Future基础用法
让我们按照惯例先在playground中测试下Future的基本用法。
新建一个playground文件,输入以下代码
let futurePublisher = Future<Int, Never> { promise in
promise(.success(10))
}
let subscription = futurePublisher
.print("_Future_")
.receive(on: RunLoop.main)
.sink { completion in
switch completion {
case .finished:
print("finished")
case .failure(let error):
print("Got a error: \(error)")
}
} receiveValue: { value in
print(value)
}
// Output
_Future_: receive subscription: (Future)
_Future_: request unlimited
_Future_: receive value: (10)
_Future_: receive finished
我们首先创建了一个Future的publisher,可以看到,他和Pass和Current两个Publisher不同,创建时需要实现@escaping闭包,闭包的参数是Promise类型,我们将他命名为promise。
在闭包中,我们发布一个10的整型值。紧接着我们订阅并接收这个publihser,从打印信息中可以看到,一切都和PassthroughSubject和CurrentValueSubject没什么区别。
但是Future带有一个@escaping闭包,意味着我们可以在闭包中进行异步调用,比如DispathQueue,我们试一下。我们将闭包声明改为
let futurePublisher = Future<Int, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
promise(.success(10))
}
}
订阅方式不变,此时运行,打印信息会先打印
_Future_: receive subscription: (Future)
_Future_: request unlimited
过2秒后,接着打印
_Future_: receive value: (10)
_Future_: receive finished
10
finished
注意:如果使用订阅的时候,没有使用futurePublisher.sink后的结果,也就是没有赋值给subscription的话,会出现 request cancel的打印,同时会不能接收到2秒后的数据。这涉及到Combine的内存管理的部分,我们在其他章节再讨论。
Future实战应用
既然Future有@escaping闭包的功能,我们在实际工程里,可以将它用来获取网络数据上。
(实际使用中,Future通常被用来当作函数返回值或计算属性来使用)
这里我们利用https://jsonplaceholder.typicode.com/todos/1?来测试Future的功能,他返回的json如下
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
首先创建一个SwiftUI的工程,在View中先加入网络处理部分的代码
// MARK:- 数据结构
struct JSONData: Decodable {
var userId: Int
var id: Int
var title: String
var completed: Bool
}
// MARK:- 错误枚举
enum NetworkError: Error {
case someError
}
// MARK:- 网络数据处理类
class NetworkService {
static let shared = NetworkService()
private init() { }
var cancellables = Set<AnyCancellable>()
// @escaping closure的写法
func getWebDataNormal(completion: @escaping (Result<String, Error>) -> Void) {
URLSession.shared.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/todos/1")!)
.print("_webdata_")
.filter { ($0.response as! HTTPURLResponse).statusCode == 200 }
.map { $0.data }
.decode(type: JSONData.self, decoder: JSONDecoder())
.sink { result in
switch result {
case .finished:
print("finished")
case .failure(let error):
print("Got a error: \(error)")
completion(.failure(NetworkError.someError))
}
} receiveValue: { data in
completion(.success(data.title))
}
.store(in: &self.cancellables)
}
// Combine Future的写法
func getWebData() -> Future<String, Error> {
return Future() { [weak self] promise in
if let self = self {
URLSession.shared.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/todos/1")!)
.print("_webdata_")
.filter { ($0.response as! HTTPURLResponse).statusCode == 200 }
.map { $0.data }
.decode(type: JSONData.self, decoder: JSONDecoder())
.sink { completion in
switch completion {
case .finished:
print("finished")
case .failure(let error):
print("Got a error: \(error)")
// "承诺失败",发布错误
promise(.failure(NetworkError.someError))
}
} receiveValue: { data in
// "承诺成功",发布数据
promise(.success(data.title))
}
.store(in: &self.cancellables)
}
}
}
}
在代码中,我们只想要显示获取json中的“title”数据。我们在Future的闭包中,调用dataTaskPublisher来获取网络数据,并接收,如果有错误,用promise(.failure())来发布错误,没有错误,用promise(.success())来发布数据。
我们用两种写法实现了获取数据的功能,一个是@escaping闭包的方式,一个是Combine Future的方式,大家可以看下区别。
使用combine时,future可以替代@escaping闭包的功能。
然后我们新建一个ViewModel,来调用网络数据处理的功能
extension FutureView {
class ViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
// MARK:- 刷新视图用的变量
@Published var title: String = ""
func fetchData() {
// getWebData的返回值是publisher,所以用Combine方式处理
NetworkService.shared.getWebData()
.print("_fetchData_")
.receive(on: RunLoop.main)
.sink { completion in
switch completion {
case .failure(let err):
// promise发布的错误
print("Error is \(err.localizedDescription)")
case .finished:
print("Finished")
}
}
receiveValue: { [weak self] data in
print("fetchWebData: \(data)")
// promise发布的数据,存储到title
self?.title = data
}
.store(in: &cancellables)
}
}
}
最后实现我们的View
struct FutureView: View {
@StateObject var vm = ViewModel()
private var cancellables = Set<AnyCancellable>()
var body: some View {
Text(vm.title)
.onAppear {
vm.fetchData()
}
}
}
最后运行,得到结果
|