SwiftUI Combine的Future

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不能。

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()
            }
    }
}

最后运行,得到结果

SwiftUI Combine的Future

 

上一篇:逃逸闭包和非逃逸闭包


下一篇:A1092. To Buy or Not to Buy