You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:54:23