Combine中串行与并行网络请求

1个月前 104次点击 来自 iOS

标签: Swift

Combine中串行与并行网络请求

原文链接:Combining Network Requests with Combine and Swift

2种典型网络请求类型

串行网络请求

有以下典型的串行网络请求业务:

  1. 请求用户auth登录。
  2. 获取用户信息。

使用 Alamofire 网络请求如下:

AF.request("https://example.com/auth").response { response in

    // Grab some data from `response`, like a user ID...
    let uid = response.data ···

    // Get favorite movies for this user
    AF.request("https://example.com/movies.json?uid=\(uid)").response { response in
        ···
    }
}

并行网络请求

同时发起多个请求,等待所有请求的结果并进行汇总处理:

var completedA = false
var completedB = false
var movies = [Movie]()
var books = [Book]()

let completionHandler = {
    if completedA && completedB {
        // Do something with `movies` and `books`...
    }
}

AF.request("https://example.com/movies.json").response { response in
    movies += [response.data as ···]
    completedA = true
    completionHandler()
}

AF.request("https://example.com/books.json").response { response in
    books += [response.data as ···]
    completedB = true
    completionHandler()
}

以上代码都有共同弊端,因调试困难或导致意外错误,没有Combine之前,我们还可以使用PromiseKit中提供when()then()等的结合以达到请求合并与否的逻辑判断。

接下来,还是用Combine来改造吧。

Working with Combine in Swift

懒一下,直接抄:

  1. 并行 Parallel: Combining 2 HTTP networking requests with Publishers.Zip, and taking action when both requests have completed.
  2. 串行 Serial: Chaining 2 HTTP networking requests with flatMap(), and using the response from the first request as input for the second.

以下是接下来需要用的2个json文件:

books.json:

[
	{
		"id":     1,
		"title":  "Nineteen Eighty-Four: A Novel",
		"author": "George Orwell"
	}, {
		"id":     2,
		"title":  "Animal Farm",
		"author": "George Orwell"
	}, {
		"id":     3,
		"title":  "Brave New World",
		"author": "Aldous Huxley"
	}, {
		"id":     4,
		"title":  "Fahrenheit 451",
		"author": "Ray Bradbury"
	}, {
		"id":     5,
		"title":  "Blindness",
		"author": "José Saramago"
	}, {
		"id":     6,
		"title":  "Ready Player One",
		"author": "Ernest Cline"
	}, {
		"id":     7,
		"title":  "The Diamond Age",
		"author": "Neal Stephenson"
	}, {
		"id":     8,
		"title":  "The Time Machine",
		"author": "H.G. Wells"
	}, {
		"id":     9,
		"title":  "Altered Carbon",
		"author": "Richard K. Morgan"
	}, {
		"id":     10,
		"title":  "Neuromancer",
		"author": "William Gibson"
	}
]

movies.json:

[
	{
		"id":     1,
		"title":  "Blade Runner",
		"year":   1982
	}, {
		"id":     2,
		"title":  "Tron",
		"year":   1982
	}, {
		"id":     3,
		"title":  "RoboCop 2",
		"year":   1990
	}, {
		"id":     4,
		"title":  "The Matrix",
		"year":   1999
	}, {
		"id":     5,
		"title":  "Minority Report",
		"year":   2002
	}, {
		"id":     6,
		"title":  "District 9",
		"year":   2009
	}, {
		"id":     7,
		"title":  "Elysium",
		"year":   2013
	}, {
		"id":     8,
		"title":  "Ex Machina",
		"year":   2015
	}, {
		"id":     9,
		"title":  "Ghost in the Shell",
		"year":   2017
	}, {
		"id":     10,
		"title":  "Blade Runner 2049",
		"year":   2017
	}
]

定义json数据模型映射:

protocol Item {
    var title: String { get }
}

struct Book: Item, Codable {
    var title: String
    var author: String
}

struct Movie: Item, Codable {
    var title: String
    var year: Int
}

定义API请求类(具体语法Combine规则解释清参考原文):

class API
{
    var cancellables = Set<AnyCancellable>()
    
    func fetchBooks() -> AnyPublisher<[Book], Never>
    {
        let url = URL(string: "https://gist.githubusercontent.com/reinder42/6f5dffd8e9a6a78a56963ab3b1694238/raw/66a6cba5a878bb9a464188dbd1378ec4b0aa4714/books.json")!

        return URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: [Book].self, decoder: JSONDecoder())
            .replaceError(with: [Book]())
            .eraseToAnyPublisher()
    }
 
 func fetchMovies() -> AnyPublisher<[Movie], Never>
    {
        let url = URL(string: "https://gist.githubusercontent.com/reinder42/85def17dba01163ccd0f9adb32394b76/raw/c305631dd011eddc13eb8be787a22d4f8f7a49a2/movies.json")!
    
        return URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: [Movie].self, decoder: JSONDecoder())
            .replaceError(with: [Movie]())
            .eraseToAnyPublisher()
    }
    
}

Publishers.Zip 发起并行请求book与movie数据

向API补充fetchItems方法:

func fetchItems()
{
    let combinedPublisher = Publishers.Zip(fetchBooks(), fetchMovies())
}

Zip方法最终返回Tuple元祖数据类型([Book], [Movie])

Sorting Books and Movies

得到数据后进行Sort排序

func fetchItems()
{
    let combinedPublisher = Publishers.Zip(fetchBooks(), fetchMovies())
        .map { items -> [Item] in
                return (items.0 + items.1).sorted { $0.title < $1.title }
            }
}

Subscribing with sink()

继续完善fetchItems方法,向订阅者发送数据:

combinedPublisher.sink { items in

    for item in items {

        if let book = item as? Book {
            print("\(book.title) - \(book.author)")
        } else if let movie = item as? Movie {
            print("\(movie.title) - \(movie.year)")
        }
    }
}
.store(in: &cancellables)

试一试最终结果:

var api = API()
api.fetchItems()

Chaining Publishers with flatMap() 发起串行请求

定义user模型:

struct User: Codable {
    var uid: Int
    var username: String
}

user.json:

{
	"uid": 			42,
	"username": 	"reinder42",
	"name": 		"Reinder de Vries",
	"likes": 		999,
	"location": 	"Netherlands",
	"job": 			"iOS dev"
}

Authenticating the User

补充API:

func authenticate(username: String, password: String) -> AnyPublisher<User, Never>
{
    print("Authenticating user '\(username)'...")

    let url = URL(string: "https://gist.githubusercontent.com/reinder42/6d4126e85fdc7ef1b6e8379911e859a2/raw/9b6737415eb67fa19705c7d3888f84258ceb1723/auth.json")!

    return URLSession.shared.dataTaskPublisher(for: url)
        .map { $0.data }
        .decode(type: User.self, decoder: JSONDecoder())
        .replaceError(with: User(uid: 0, username: ""))
        .eraseToAnyPublisher()
}

Chaining Auth and Favorite Movies with flatMap

用户完成auth验证后可获取movie信息:

func fetchMovies(for user: User) -> AnyPublisher<[Movie], Never>
{
    print("Fetching movies for user ID = '\(user.uid)'...")

    return fetchMovies()
}

func fetchFavorites()
{
    authenticate(username: "reinder42", password: "abcd1234")
        .flatMap { user in
            return self.fetchMovies(for: user)
        }
        .sink { movies in
            print(movies.map { $0.title }.joined(separator: ", "))
        }
        .store(in: &cancellables)
}

试一下最后结果:

var api = API()
api.fetchFavorites()
Card image cap
开发者雷

尘世间一个小小的开发者,每天增加一些无聊的知识,就不会无聊了

要加油~~~

技术文档 >> 系列应用 >>
热推应用
Let'sLearnSwift
学习Swift的入门教程
PyPie
Python is as good as Pie
标签