使用完成处理程序 return 来自 NASA API 的项目数组

Using a completion handler to return an array of items from a NASA API

这是我从 NASA 获取一件物品的工作代码API我使用了 Apple Programming book 上介绍的完成处理程序。

class PhotoInfoController {
    func fetchPhotoInfo(completion: @escaping (PhotoInfo?) -> Void) {
        let baseURL = URL(string: "https://api.nasa.gov/planetary/apod")!

    let query: [String:String] = [
        "api_key" : "DEMO_KEY"
    ]

    let url = baseURL.withQueries(query)!

    let task = URLSession.shared.dataTask(with: url) {
        (data, response, error) in
        let jsonDecoder = JSONDecoder()

        if let data = data,
           let photoInfo = try? jsonDecoder.decode(PhotoInfo.self, from: data) {
            completion(photoInfo)
        } else {
            print("Not found or data is not sanitazed.")
            completion(nil)
        }
    }
    task.resume()
   }
}

我很难弄清楚的问题是如何通过完成处理程序 return 数组转到项目 (PhotoInfo)。到目前为止,这是我的代码:

class PhotoInfoController {
    func fetchPhotoInfo(completion: @escaping ([PhotoInfo]?) -> Void) {
        let baseURL = URL(string: "https://api.nasa.gov/planetary/apod")!
    let currentDate = Date()
    let formatter = DateFormatter()
    formatter.dateFormat = "YYYY-MM-d"
    var photoInfoCollection: [PhotoInfo] = []
    
    for i in 0 ... 1 {
        let modifiedDate = Calendar.current.date(byAdding: .day,value: -i ,to: currentDate)!
        let stringDate = formatter.string(from: modifiedDate)
        let query: [String:String] = [
            "api_key" : "DEMO_KEY",
            "date" : stringDate
        ]
        
        let url = baseURL.withQueries(query)!
        let task = URLSession.shared.dataTask(with: url) { (data,
                                                            response,
                                                            error) in
            let jsonDecoder = JSONDecoder()
            
            if let data = data,
               let photoInfo = try? jsonDecoder.decode(PhotoInfo.self, from: data) {
                photoInfoCollection.append(photoInfo)
            } else {
                print("Data was not returned")
            }
        }
        task.resume()
    }
    
    
    completion(photoInfoCollection)
    }
}

任何想法或指导将不胜感激谢谢!

根据建议实施的代码:

class PhotoInfoController {
    private let baseURL = URL(string: "https://api.nasa.gov/planetary/apod")!
    private let currentDate = Date()
    private let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "YYYY-MM-d"
        return formatter
    }()
    
    private let jsonDecoder = JSONDecoder()
    
    func fethPhotoInfo(itemsToFetch: Int, completion: @escaping ([PhotoInfo]?) -> Void) {
        var count = 0
        var photoInfoCollection: [PhotoInfo] = []
        
        for i in 0 ... itemsToFetch {
            let modifiedDate = Calendar.current.date(byAdding: .day, value: -i, to: currentDate)!
            
            let query: [String : String] = [
                "api_key" : "DEMO_KEY",
                "date" : dateFormatter.string(from: modifiedDate)
            ]
            
            let url = baseURL.withQueries(query)!
            let task = URLSession.shared.dataTask(with: url) {
                (data, response, error) in
                
                if let data = data,
                   let photoInfo = try? self.jsonDecoder.decode(PhotoInfo.self, from: data) {
                    photoInfoCollection.append(photoInfo)
                    count += 1
                    
                    if count == itemsToFetch {
                        completion(photoInfoCollection)
                    }
                    
                } else {
                    print("Data for \(self.dateFormatter.string(from: modifiedDate)) not made.")
                }
            }
            task.resume()
        }
    }
}

你的代码不会像写的那样工作。

您使用 for 循环启动 2 个 URLSession dataTask 对象。这些任务是异步的;在发送请求之前立即调用网络请求的代码 returns。

然后在您的 for 循环之外调用您的完成处理程序,甚至在您的网络请求有机会发送之前。您将需要一种机制来跟踪未决请求的数量并在两个请求都完成时调用完成处理程序。

考虑这个模拟您正在做的事情的函数:

func createAsyncArray(itemCount: Int, completion: @escaping ([Int]) -> Void) {
    var count = 0; //Keep track of the number of items we have created
    
    var array = [Int]()  //Create an empty results array
    
    //Loop itemCount times.
    for _ in 1...itemCount {
        let delay = Double.random(in: 0.5...1.0)
        
        //Delay a random time before creating a random number (to simulate an async network response)
        DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
            
            //Add a random number 1...10 to the array
            array.append(Int.random(in: 1...10))
            
            //Increment the number of results we have added to the array
            count += 1
            print("In loop, count = \(count)")
            
            //If we have have added enough items to the array, invoke the completion handler.
            if count == itemCount {
                completion(array)
            }
        }
    }
    print("at this point in the code, count = \(count)")
}

您可以这样调用该代码:

    let itemCount = Int.random(in: 2...10)
    print("\(itemCount) items")
    createAsyncArray(itemCount: itemCount) {   array in
        for i in 0..<itemCount {
            print("array[\(i)] = \(array[i])")
        }
    }

该函数的示例输出可能如下所示:

9 items
at this point in the code, count = 0
In loop, count = 1
In loop, count = 2
In loop, count = 3
In loop, count = 4
In loop, count = 5
In loop, count = 6
In loop, count = 7
In loop, count = 8
In loop, count = 9
array[0] = 8
array[1] = 6
array[2] = 5
array[3] = 4
array[4] = 7
array[5] = 10
array[6] = 2
array[7] = 4
array[8] = 7

请注意,在将任何条目添加到数组之前,输出显示“此时代码中,计数 = 0”。这是因为每次调用 DispatchQueue.main.asyncAfter() returns 都是在闭包内的代码执行之前立即进行的。

上面的函数使用局部变量 count 来跟踪已将多少项添加到数组中。一旦计数达到所需的数量,该函数就会调用完成处理程序。

您应该在代码中使用类似的方法。

编辑:

您应该知道您的网络请求可能会乱序完成。您的代码提交 itemsToFetch+1 不同的请求。您不知道这些请求将按什么顺序完成,而且请求按提交顺序完成的可能性很小。如果您的第二个请求比第一个请求完成得更快,它的关闭将首先执行。

你试图用一种方法做所有事情,这让你自己变得复杂了。想象一下你有 fetchPhotoInfo 函数在工作(它确实有效,所以,到目前为止做得很好):

struct PhotoInfo: Codable {
    let copyright: String
}

class PhotoInfoController {
    private let base = "https://api.nasa.gov/planetary/apod"
    private let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "YYYY-MM-d"
        return formatter
    }()
    
    func fetchPhotoInfo(forDate date: Date, completion: @escaping (PhotoInfo?) -> Void) {
        guard var components = URLComponents(string: base) else {
            completion(nil)
            return
        }
        
        components.queryItems = [
            URLQueryItem(name: "api_key", value: "DEMO_KEY"),
            URLQueryItem(name: "date", value: dateFormatter.string(from: date))
        ]
        
        guard let url = components.url else {
            completion(nil)
            return
        }
        
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard let data = data else {
                completion(nil)
                return
            }
            
            let photoInfo = try? JSONDecoder().decode(PhotoInfo.self, from:data)
            completion(photoInfo)
        }
        task.resume()
    }
}

您的下一个目标是获取多张照片信息。保留您的 class,保留您的方法并添加另一个利用您已有的方法。实现此目的的一种方法是使用 DispatchGroup:

func fetchPhotoInfo(forDates dates: [Date], completion: @escaping ([PhotoInfo]) -> Void) {
    // Group of tasks
    let taskGroup = DispatchGroup()
    
    // Result of photos
    var result: [PhotoInfo] = []
    
    // For each date ...
    dates.forEach {
        // ... enter the group
        taskGroup.enter()
        
        // Fetch photo info
        fetchPhotoInfo(forDate: [=11=]) { photoInfo in
            defer {
                // Whenever the fetchPhotoInfo completion closure ends, leave
                // the task group, but no sooner.
                taskGroup.leave()
            }
            
            // If we've got the photo ...
            if let photoInfo = photoInfo {
                // ... add it to the result. We can safely add it here, because
                // the fetchPhotoInfo completion block is called on the
                // URLSession.shared.delegateQueue which has maxConcurrentOperationCount
                // set to 1 by default. But you should be aware of this,
                // do not rely on it and introduce some kind of synchronization.
                result.append(photoInfo)
            }
        }
    }
    
    // At this point, we told the URLSession (via fetchPhotoInfo) that we'd like to
    // execute data tasks. These tasks already started (or not, we don't know), but
    // they didn't finish yet (most likely not, but we don't know either). That's
    // the reason for our DispatchGroup. We have to wait for completion of all
    // our asynchronous tasks.
    taskGroup.notify(queue: .main) {
        completion(result)
    }
}

你可以这样使用:

let pic = PhotoInfoController()
pic.fetchPhotoInfo(forDate: Date()) { info in
    print(String(describing: info))
}

pic.fetchPhotoInfo(forDates: [Date(), Date().addingTimeInterval(-24*60*60)]) { infos in
    print(infos)
}

输出为:

Optional(NASA.PhotoInfo(copyright: "Stephane Guisard"))
[NASA.PhotoInfo(copyright: "Stephane Guisard"), NASA.PhotoInfo(copyright: "Zixuan LinBeijing Normal U.")]

没有错误处理,需要自己添加。

即使我确实提供了您问题的答案,我也会将其标记为 的副本。您获取单张照片信息的代码有效,您只是在努力了解如何等待多个异步任务。