问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Swift并发编程入门:三种异步请求方式详解

创作时间:
作者:
@小白创作中心

Swift并发编程入门:三种异步请求方式详解

引用
CSDN
1.
https://blog.csdn.net/guoyongming925/article/details/140944575

直到现在为止,如果我们想要异步请求数据,应该说至少有三种方式:

  1. 传统的通过闭包(@escaping closure)方式回调处理。
  2. 通过Combine的发布者订阅者机制。
  3. 通过async/await组合的方式。

采用哪种方式,还得因项目而异,本文将对这三种方式做一个简单的总结,以及代码示例。

下面就以下载一个网络图片为例。

首先还是要先定义一个界面和一个对应的ViewModel:

struct DownloadImageDemo: View {
    @StateObject private var viewModel = DownloadImageDemoViewModel()
    
    var body: some View {
        ZStack {
            if let image = viewModel.image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 200, height: 200)
            }
        }
        .onAppear {
            
        }
    }
}
class DownloadImageDemoViewModel: ObservableObject {
    @Published var image: UIImage?
    var downloader: ImageDownloader = ImageDownloader()
    
    func fetchImageWithEscapingClosure() {
     
    }
    
    func fetchImageWithCombine() {
        
    }
    
    func fetchImageWithAsnynAndAwait() {
        
    }
}

在ViewModel中我们先定义了三个方法,分别用于处理不同的请求,另外为了更加符合项目,将图片下载逻辑放到一个我们模拟的网络层处理ImageDownloader

class ImageDownloader {
    let url = URL(string: "https://picsum.photos/200")!
}

至此基本的代码逻辑已经完成,下面重点看一下下载部分的代码,这部分代码统一在ImageDownloader中处理。

escaping closure方式

class ImageDownloader {
    let url = URL(string: "https://picsum.photos/200")!
    
    func handleResponse(data: Data?, response: URLResponse?) -> UIImage? {
        guard let data,
              let image = UIImage(data: data),
              let response = response as? HTTPURLResponse,
              response.statusCode >= 200 && response.statusCode < 300 else {
            return nil
        }
        return image
    }
    
    func fetchImageWithEscapingClosure(_ completion: @escaping (UIImage?, Error?) -> Void) {
        URLSession.shared.dataTask(with: URLRequest(url: url)) { [weak self] data, response, error in
            let image = self?.handleResponse(data: data, response: response)
            completion(image, error)
        }
        .resume()
    }
}

在上面的代码中,我们在ImageDownloader中定义了fetchImageWithEscapingClosure方法,其参数为一个逃逸闭包,用于返回网络请求的结果,想必都不陌生了。

为了简化代码,这里面将错误处理单独拿出来放到handleResponse中处理,并返回一个可选的UIImage

在ViewModel中的方法中调用如下:

func fetchImageWithEscapingClosure() {
    downloader.fetchImageWithEscapingClosure { [weak self] image, _ in
        self?.image = image
    }
}

SwiftUI界面调用如下:

struct DownloadImageDemo: View {
    @StateObject private var viewModel = DownloadImageDemoViewModel()
    
    var body: some View {
        ZStack {
            if let image = viewModel.image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 200, height: 200)
            }
        }
        .onAppear {
            viewModel.fetchImageWithEscapingClosure()
        }
    }
}

Combine方式

首先在ImageDownloader中定义一个方法,具体如下:

func fetchImageWithCombine() -> AnyPublisher<UIImage? ,Error> {
    URLSession.shared.dataTaskPublisher(for: url)
        .map(handleResponse)
        .mapError({ $0 })
        .eraseToAnyPublisher()
}

该方法返回了一个AnyPublisher类型,并定义好泛型类型,以便调用的地方订阅。

URLSession.shared.dataTaskPublisher方法返回了一个Publisher,这样我们可以继续往下走,使用map操作符去做一些类型转换,这里在map操作符里面使用了之前定义的handleResponse方法。因为map方法闭包返回的参数和handleResponse接收的参数相同,所以可以简写,如下图:

另外在map操作符后还用了mapError操作符,将错误类型转换,否则就会报下面的错误:

主要原因是我们尝试将一个返回AnyPublisher<UIImage?, URLError>类型的表达式转换为返回AnyPublisher<UIImage?, Error>类型的表达式,但类型不匹配。可以通过使用.mapError操作符来转换错误类型,将URLError转换为Error,以使类型匹配。

最后使用eraseToAnyPublisher()类型抹除到统一的AnyPublisher

下面在看看调用订阅的地方,在ViewModel中定义了如下方法:

func fetchImageWithCombine() {
    downloader.fetchImageWithCombine()
        .sink { _ in
            
        } receiveValue: { [weak self] image in
            self?.image = image
        }
        .store(in: &cancellable)
}

通过sink添加订阅者,并处理收到的信息,最后别忘了store,否则出了方法作用域订阅就失效了。

在UI部分调用也是非常简单:

struct DownloadImageDemo: View {
    @StateObject private var viewModel = DownloadImageDemoViewModel()
    
    var body: some View {
        ZStack {
            if let image = viewModel.image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 200, height: 200)
            }
        }
        .onAppear {
//            viewModel.fetchImageWithEscapingClosure()
            viewModel.fetchImageWithCombine()
        }
    }
}

async/await方式

async/await方式就用到了上一篇文章中说到的内容了。

首先还是处理网络层ImageDownloader,在其中添加方法,如下:

func fetchImageWithAsyncAndAwait() async throws -> UIImage? {
    do {
        let (data, response) = try await URLSession.shared.data(from: url)
        return handleResponse(data: data, response: response)
    } catch {
        throw error
    }
}

上面这个方法在方法名的后面添加了async,告诉系统这是个异步方法,另外还添加了throws,当错误的时候抛出异常。

在选择URLSession.shared的方法的时候我们看到有下面的这个方法,系统同样提供了一个异步的且抛出异常的data()方法。

所以我们也按照系统的规则去写。方法里面的do-catch等逻辑之前文章有介绍,这里就不多说了。

下面在ViewModel调用的方法里面,调用上面这个方法。

func fetchImageWithAsnynAndAwait() async {
    let image = try? await downloader.fetchImageWithAsyncAndAwait()
    await MainActor.run {
        self.image = image
    }
}

这个方法我们只是添加了async,并没有throws,这里我们暂时忽略异常错误,方法里面也用到了try?

调用async的异步方法,需要在前面加上await,并且刷新UI要回主线程哦。

最后就是在界面调用了:

struct DownloadImageDemo: View {
    @StateObject private var viewModel = DownloadImageDemoViewModel()
    
    var body: some View {
        ZStack {
            if let image = viewModel.image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 200, height: 200)
            }
        }
        .onAppear {
//            viewModel.fetchImageWithEscapingClosure()
//            viewModel.fetchImageWithCombine()
            Task {
                await viewModel.fetchImageWithAsnynAndAwait()
            }
        }
    }
}

因为调用异步方法需要在异步上下文环境中,所以我们将调用方法放到了Task闭包中。关于Task下一篇文章将重点介绍一下。

写在最后

本篇文章主要回顾了一下三种异步请求方式,@escaping closure,Combine,async/await这三种方式,并做了一些代码示例,无论采用哪种方法,都是因人而异,因项目而异,不过还是希望大家跟上最新的步伐,让自己的代码更高效,更稳健,更易维护。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号