如何在从函数回调存储数据之前等待 API 请求完成?

How to wait for an API request to finish before storing data from function callback?

在我的个人项目中,我创建了一个 API 调用程序来从 Spotify API 检索用户保存的曲目。我正在使用的 Spotify 端点有一个限制(每个请求最多 50 个曲目)和一个偏移量(请求中第一首曲目的起始索引),这就是为什么我决定使用 FOR 循环来获取一系列曲目页面的原因(每 50 条轨道)并将它们附加到全局数组。数据从主线程加载,在请求数据时,我显示一个带有微调器视图的子视图控制器。数据请求完成后,我删除微调器视图,并转换到另一个视图控制器(将数据作为 属性)传递。

我已经尝试了很多东西,但是在 API 请求之后,曲目数组总是空的。我觉得这与我的请求的同步性有关,或者可能是我没有正确处理它。理想情况下,我想等到 API 的请求完成,然后将结果附加到数组。你对我如何解决这个问题有什么建议吗?非常感谢任何帮助!

func createSpinnerView() {

    let loadViewController = LoadViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
    add(asChildViewController: loadViewController)

    DispatchQueue.main.async { [weak self] in
        if (self?.dropdownButton.dropdownLabel.text == "My saved music") {
            self?.fetchSavedMusic() { tracksArray in
                self?.tracksArray = tracksArray
            }
        }
        ...
        self?.remove(asChildViewController: loadViewController)
        self?.navigateToFilterScreen(tracksArray: self!.tracksArray)
    }
}

private func fetchSavedMusic(completion: @escaping ([Tracks]) -> ()) {
    let limit = 50
    var offset = 0
    var total = 200
    for _ in stride(from: 0, to: total, by: limit) {
        getSavedTracks(limit: limit, offset: offset) { tracks in
            //total = tracks.total
            self.tracksArray.append(tracks)
        }
        print(offset, limit)
        offset = offset + 50
    }
    completion(tracksArray)
}

private func getSavedTracks(limit: Int, offset: Int, completion: @escaping (Tracks) -> ()) {
    APICaller.shared.getUsersSavedTracks(limit: limit, offset: offset) { (result) in
        switch result {
        case .success(let model):
            completion(model)
            print("success")
        case .failure(let error):
            print("Error retrieving saved tracks: \(error.localizedDescription)")
            print(error)
        }
    }
}

private func navigateToFilterScreen(tracksArray: [Tracks]) {
    let vc = FilterViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
    vc.paginatedTracks = tracksArray
    show(vc, sender: self)
}

首先,您需要在加载所有数据时调用完成。在您的情况下,您在任何 getSavedTracks return.

之前调用 completion(tracksArray)

对于这部分,我建议您通过遍历所有页面来递归地积累曲目。有多种更好的工具可以做到这一点,但我将给出一个原始示例:

class TracksModel {
    
    static func fetchAllPages(completion: @escaping ((_ tracks: [Track]?) -> Void)) {
        var offset: Int = 0
        let limit: Int = 50
        var allTracks: [Track] = []
        
        func appendPage() {
            fetchSavedMusicPage(offset: offset, limit: limit) { tracks in
                guard let tracks = tracks else {
                    completion(allTracks) // Most likely an error should be handled here
                    return
                }
                if tracks.count < limit {
                    // This was the last page because we got less than limit (50) tracks
                    completion(allTracks+tracks)
                } else {
                    // Expecting another page to be loaded
                    offset += limit // Next page
                    allTracks += tracks
                    appendPage() // Recursively call for next page
                }
            }
        }

        appendPage() // Load first page
        
    }
    
    private static func fetchSavedMusicPage(offset: Int, limit: Int, completion: @escaping ((_ tracks: [Track]?) -> Void)) {
        APICaller.shared.getUsersSavedTracks(limit: limit, offset: offset) { result in
            switch result {
            case .success(let model):
                completion(model)
            case .failure(let error):
                print(error)
                completion(nil) // Error also needs to call a completion
            }
        }
    }
    
}

我希望评论能澄清一些事情。但关键是我嵌套了一个 appendPage 函数,该函数被递归调用,直到服务器停止发送数据。最后要么发生错误,要么最后一页的曲目数 returns 少于提供的限制。 当然,也转发错误会更好,但为了简单起见,我没有包括它。

无论如何,您现在可以在任何地方 TracksModel.fetchAllPages { } 接收所有曲目。

加载和显示数据时 (createSpinnerView),您还需要等待接收数据才能继续。例如:

func createSpinnerView() {

    let loadViewController = LoadViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
    add(asChildViewController: loadViewController)

    TracksModel.fetchAllPages { tracks in
        DispatchQueue.main.async {
            self.tracksArray = tracks
            self.remove(asChildViewController: loadViewController)
            self.navigateToFilterScreen(tracksArray: tracks)
        }
    }
    
}

一些组件可能已被删除,但我希望你明白这一点。该方法应该已经在主线程上调用了。但是您不确定 API 在哪个线程上调用 return。所以你需要在完成闭包中使用 DispatchQueue.main.async,而不是在它之外。并且还调用在这个闭包中导航,因为这是真正完成的时候。

增加固定请求数的情况

对于固定数量的请求,您可以并行执行所有请求。您已经在代码中这样做了。 最大的问题是您不能保证响应将按照与您的请求开始时相同的顺序返回。例如,如果您执行两个请求 AB,由于网络或任何其他原因,B 将在 A 之前 return 很容易发生。所以你需要偷偷摸摸一点。看下面的代码:

private func loadPage(pageIndex: Int, perPage: Int, completion: @escaping ((_ items: [Any]?, _ error: Error?) -> Void)) {
    // TODO: logic here to return a page from server
    completion(nil, nil)
}

func load(maximumNumberOfItems: Int, perPage: Int, completion: @escaping ((_ items: [Any], _ error: Error?) -> Void)) {
    let pageStartIndicesToRetrieve: [Int] = {
        var startIndex = 0
        var toReturn: [Int] = []
        while startIndex < maximumNumberOfItems {
            toReturn.append(startIndex)
            startIndex += perPage
        }
        return toReturn
    }()
    
    guard pageStartIndicesToRetrieve.isEmpty == false else {
        // This happens if maximumNumberOfItems == 0
        completion([], nil)
        return
    }
    
    enum Response {
        case success(items: [Any])
        case failure(error: Error)
    }
    
    // Doing requests in parallel
    // Note that responses may return in any order time-wise (we can not say that first page will come first, maybe the order will be [2, 1, 5, 3...])
    
    var responses: [Response?] = .init(repeating: nil, count: pageStartIndicesToRetrieve.count) { // Start with all nil
        didSet {
            // Yes, Swift can do this :D How amazing!
            guard responses.contains(where: { [=12=] == nil }) == false else {
                // Still waiting for others to complete
                return
            }
            
            let aggregatedResponse: (items: [Any], errors: [Error]) = responses.reduce((items: [], errors: [])) { partialResult, response in
                switch response {
                case .failure(let error): return (partialResult.items, partialResult.errors + [error])
                case .success(let items): return (partialResult.items + [items], partialResult.errors)
                case .none: return (partialResult.items, partialResult.errors)
                }
            }
            
            let error: Error? = {
                let errors = aggregatedResponse.errors
                if errors.isEmpty {
                    return nil // No error
                } else {
                    // There was an error.
                    return NSError(domain: "Something more meaningful", code: 500, userInfo: ["all_errors": errors]) // Or whatever you wish. Perhaps just "errors.first!"
                }
            }()
            
            completion(aggregatedResponse.items, error)
        }
    }
    
    pageStartIndicesToRetrieve.enumerated().forEach { requestIndex, startIndex in
        loadPage(pageIndex: requestIndex, perPage: perPage) { items, error in
            responses[requestIndex] = {
                if let error = error {
                    return .failure(error: error)
                } else {
                    return .success(items: items ?? [])
                }
            }()
        }
    }
    
}

第一种方法没什么意思。它只加载一个页面。第二种方法现在收集所有数据。

首先发生的事情是我们计算所有可能的请求。我们需要一个起始索引和 per-page。因此,对于每页使用 50 个 145 个项目的 pageStartIndicesToRetrieve 将 return [0, 50, 100]。 (我后来发现在这种情况下我们只需要 count 3 但这取决于 API,所以让我们坚持下去)。我们预计有 3 个请求以项目索引 [0, 50, 100].

开头

接下来我们使用

为我们的回复创建占位符
var responses: [Response?] = .init(repeating: nil, count: pageStartIndicesToRetrieve.count)

对于我们的 145 个项目示例,每页使用 50 个项目,这意味着它创建了一个数组 [nil, nil, nil]。当此数组中的所有值都变为 not-nil 时,所有请求都已 returned,我们可以处理所有数据。这是通过覆盖局部变量的 setter didSet 来完成的。我希望它的内容不言而喻。

现在剩下的就是一次执行所有请求并填充数组。其他一切都应该自行解决。

代码又不是最简单的;有些工具可以使事情变得更容易。但出于学术目的,我希望这种方法能够解释正确完成任务需要做什么。