Swift 4调用Hacker News API解码内容顺序异常问题求助
问题分析与解决方案
你的猜测完全正确!问题确实出在异步请求的并发执行上——当你用map批量触发getArticle调用时,每个URLSession.dataTask都是独立异步执行的,它们的响应返回时间完全取决于网络状况,先完成的请求会先把数据塞到articles数组里,自然就打乱了原本的排序。
下面提供两种重构方案,分别适配不同的Swift版本:
方案1:使用Swift Async/Await(推荐,Swift 5.5+ / iOS 15+)
这是最简洁的现代写法,利用Swift的异步并发特性,既能保持请求的顺序性,又能避免回调地狱:
首先重构你的网络请求函数为异步函数:
private func getTopStoriesArray() async { guard let url = URL(string: topStoriesURLString) else { return } do { // 异步获取top stories ID列表 let (data, _) = try await URLSession.shared.data(from: url) let topArticlesArray = try JSONDecoder().decode([Int].self, from: data) let chunk = topArticlesArray.chunked(by: 100) // 按顺序遍历ID,等待每个文章请求完成后再处理下一个 for articleId in chunk[0] { if let article = await getArticle(id: String(articleId)) { self.articles.append(article) // 回到主线程刷新UI(也可以统一刷新,看需求) DispatchQueue.main.async { self.hackerTableView.reloadData() } } } } catch { print("Failed to fetch top stories: \(error)") } } private func getArticle(id: String) async -> Article? { let articleId = id guard let url = URL(string: "\(articleURLBase)\(articleId).json") else { return nil } do { let (data, _) = try await URLSession.shared.data(from: url) let json = try JSONDecoder().decode(Article.self, from: data) // 构造Article对象(如果你的Article已经是Codable,这里可以直接返回json简化代码) return Article( by: json.by ?? "author", descendants: json.descendants ?? 0, id: json.id, kids: json.kids ?? [], score: json.score ?? 0, time: json.time ?? 0, title: json.title, type: json.type ?? "type", url: json.url ?? "url" ) } catch { print("Error fetching article \(id): \(error)") return nil } }
然后在需要调用的地方(比如viewDidLoad)用Task启动异步任务:
override func viewDidLoad() { super.viewDidLoad() // 启动异步任务获取数据 Task { await getTopStoriesArray() } }
优势:代码逻辑清晰,天然保证请求顺序,无需额外的同步机制,同时避免了频繁的UI刷新(也可以改成所有请求完成后一次性刷新,性能更好)。
方案2:使用DispatchGroup + 索引定位(兼容Swift 5.5以下版本)
如果需要适配旧版本Swift,我们可以通过固定位置的数组来保证顺序,用DispatchGroup监听所有请求完成后统一刷新UI:
首先修改getArticle为带回调的版本,然后重构getTopStoriesArray:
private func getTopStoriesArray() { guard let url = URL(string: topStoriesURLString) else { return } URLSession.shared.dataTask(with: url) { [weak self] (data, response, error) in guard let self = self, let data = data else { return } do { let topArticlesArray = try JSONDecoder().decode([Int].self, from: data) let chunk = topArticlesArray.chunked(by: 100) let articleIds = chunk[0] // 初始化固定长度的数组,用来按索引存储文章 var orderedArticles = [Article?](repeating: nil, count: articleIds.count) let group = DispatchGroup() // 遍历ID并记录每个ID的索引 for (index, articleId) in articleIds.enumerated() { group.enter() self.getArticle(id: String(articleId)) { article in // 将文章放到对应的索引位置,不管请求完成顺序如何 if let article = article { orderedArticles[index] = article } group.leave() } } // 所有请求完成后,整理数组并刷新UI group.notify(queue: .main) { // 过滤掉获取失败的文章(nil值) self.articles = orderedArticles.compactMap { $0 } self.hackerTableView.reloadData() } } catch { print("Failed to fetch top stories: \(error)") } }.resume() } // 修改getArticle,添加回调返回获取到的Article private func getArticle(id: String, completion: @escaping (Article?) -> Void) { let articleId = id guard let url = URL(string: "\(articleURLBase)\(articleId).json") else { completion(nil) return } URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data else { completion(nil) return } do { let json = try JSONDecoder().decode(Article.self, from: data) let article = Article( by: json.by ?? "author", descendants: json.descendants ?? 0, id: json.id, kids: json.kids ?? [], score: json.score ?? 0, time: json.time ?? 0, title: json.title, type: json.type ?? "type", url: json.url ?? "url" ) completion(article) } catch { print("Error with getting article \(id): \(error)") completion(nil) } }.resume() }
原理:通过enumerated()拿到每个ID对应的索引,请求完成后把文章放到数组的固定位置,这样不管哪个请求先完成,数组的顺序始终和原始ID列表一致。DispatchGroup用来监听所有请求完成,最后一次性刷新UI,避免了频繁reload的性能问题。
额外优化点
之前的代码每次添加文章都调用reloadData,会导致tableView频繁刷新,建议改成:
- 方案1中可以在所有文章加载完成后统一刷新UI
- 或者分批刷新(比如每加载10篇刷新一次),提升用户体验
内容的提问来源于stack exchange,提问作者Taylor A. Leach
相关产品推荐
相关产品推荐

