【问题标题】:Updating screen fields when background task ends后台任务结束时更新屏幕字段
【发布时间】:2020-03-07 07:29:54
【问题描述】:

我正在使用一些 API 来获取数据。这些是作为 session.dataTask 启动的,我使用类来封装每个不同 API 的 API 调用、方法和返回的属性。当 API 会话结束并且数据可用时,我应该如何配置我的代码以更新相关的屏幕标签和子视图?

AstronomicalTimes 类初始化的相关部分是:

init (date: Date, lat: Float, long: Float) {
    let coreURL = "https://api.sunrise-sunset.org/json?"
    let position = "lat=\(lat)&lng=\(long)"
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd"
    let dateString = "&date=" + dateFormatter.string(from: date)
    //let dateString = "&date=2020-06-21"
    let urlString = coreURL + position + dateString + "&formatted=0"

    let session = URLSession.shared
    let url = URL(string: urlString)!
    let request = URLRequest(url: url)

    session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in

        if let error = error {
                let nsError = error as NSError
                print("Astronomical Times API call failed with error \(nsError.code)")
                return
            }

            if let response = response as? HTTPURLResponse {
                print("Astronomical Times API call response is \(response.statusCode)")
            }

            if let data = data {
                do {
                    let astronomicalTimesResponse = try JSONDecoder().decode(AstronomicalTimesResponse.self, from: data)
                    print("Astronomical times successfully parsed")
                    self.fillFields(astronomicalTimesResponse.results) //completes all class properties from parsed data
                } catch {
                    print("Error while tide details parsing: \(error)")
                }
            }
    }).resume()

在 viewDidLoad() 中为 API 调用的结果分配一个标签:

currentAstronomicalTimes = AstronomicalTimes(date: savedDate, lat: currentSelection.station.lat, long: currentSelection.station.long)
    lblAstDawn.text = currentAstronomicalTimes.strings.astronomicalTwilightBegin

很明显,这不起作用,因为在 API 返回数据之前,屏幕呈现为标签和子视图为空白。我不知道如何在 API 完成时向 ViewController 发出信号,然后如何重绘标签等。我尝试更新 API 调用闭包表达式中的 viewController 字段,但我无法从另一个类更新 UILabels (而且我认为这种方法很混乱,因为标签更新逻辑确实应该在 ViewController 中)

任何帮助表示赞赏。

在 Rob 的 cmets 之后更新:

我已按照建议更改了我的类定义,它成功地从 API 加载了数据。类定义如下,注意我添加了一个函数,它获取加载的数据并将其转换为时间字符串和 date() 以便于在 viewController 中使用(这些似乎都在 API 调用后正确填充)

import Foundation

枚举 AstronomicalTimesError: 错误 { case invalidResponse(Data?, URLResponse?) }

类天文时间{

//structures for decoding daylight times
 struct AstronomicalTimesResponse: Decodable {
     public var results: AstronomicalTimes
     public var status: String
 }

 struct AstronomicalTimes: Decodable {
    var sunrise = String()
    var sunset = String()
    var solarNoon = String()
    var dayLength = 0
    var civilTwilightBegin = String()
    var civilTwilightEnd = String()
    var nauticalTwilightBegin = String()
    var nauticalTwilightEnd = String()
    var astronomicalTwilightBegin = String()
    var astronomicalTwilightEnd = String()
    private enum CodingKeys : String, CodingKey {
        case sunrise = "sunrise"
        case sunset = "sunset"
        case solarNoon = "solar_noon"
        case dayLength = "day_length"
        case civilTwilightBegin = "civil_twilight_begin"
        case civilTwilightEnd = "civil_twilight_end"
        case nauticalTwilightBegin = "nautical_twilight_begin"
        case nauticalTwilightEnd = "nautical_twilight_end"
        case astronomicalTwilightBegin = "astronomical_twilight_begin"
        case astronomicalTwilightEnd = "astronomical_twilight_end"
    }
}
//used to hold string values to enter to label, i.e. time strings for labels
var strings = AstronomicalTimes()

//struct and variable used to hold specific date/times for gradient calculation
struct Times {
    var sunrise = Date()
    var sunset = Date()
    var solarNoon = Date()
    var dayLength = 0
    var civilTwilightBegin = Date()
    var civilTwilightEnd = Date()
    var nauticalTwilightBegin = Date()
    var nauticalTwilightEnd = Date()
    var astronomicalTwilightBegin = Date()
    var astronomicalTwilightEnd = Date()
}
var times = Times()

let date: Date
let latitude: Float
let longitude: Float

init (date: Date, latitude: Float, longitude: Float) {
    self.date = date
    self.latitude = latitude
    self.longitude = longitude
}

func start(completion: @escaping (Result<AstronomicalTimesResponse, Error>) -> Void) {
    var components = URLComponents(string: "https://api.sunrise-sunset.org/json")!

    let dateFormatter = DateFormatter()
    dateFormatter.locale = Locale(identifier: "en_US_POSIX") // just in case your end user isn't using Gregorian calendar
    dateFormatter.dateFormat = "yyyy-MM-dd"

    components.queryItems = [
        URLQueryItem(name: "lat", value: "\(latitude)"),
        URLQueryItem(name: "lng", value: "\(longitude)"),
        URLQueryItem(name: "date", value: dateFormatter.string(from: date)),
        URLQueryItem(name: "formatted", value: "0")
    ]

    let session = URLSession.shared
    let url = components.url!
    let request = URLRequest(url: url)

    session.dataTask(with: request) { data, response, error in
        if let error = error {
            DispatchQueue.main.async {
                completion(.failure(error))
            }
            return
        }

        guard
            let responseData = data,
            let httpResponse = response as? HTTPURLResponse,
            200 ..< 300 ~= httpResponse.statusCode
        else {
            DispatchQueue.main.async {
                completion(.failure(AstronomicalTimesError.invalidResponse(data, response)))
            }
            return
        }

        do {
            print("Astronomical times api completed with status code ", httpResponse.statusCode)
            let astronomicalTimesResponse = try JSONDecoder().decode(AstronomicalTimesResponse.self, from: responseData)
            DispatchQueue.main.async {
                completion(.success(astronomicalTimesResponse))
                self.fillFields(astronomicalTimesResponse.results)
            }
        } catch let jsonError {
            DispatchQueue.main.async {
                completion(.failure(jsonError))
            }
        }
    }.resume()
}

func fillFields(_ input: AstronomicalTimes) -> Void {
    //formats output fields into Date() or String (HH:mm) format
    let dateFormatter = DateFormatter()
    dateFormatter.locale = Locale(identifier: "en_US_POSIX")
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" //Your date format

    times.sunrise = dateFormatter.date(from: input.sunrise) ?? Date()
    times.sunset = dateFormatter.date(from: input.sunset) ?? Date()
    times.solarNoon = dateFormatter.date(from: input.solarNoon) ?? Date()
    times.dayLength = input.dayLength
    times.civilTwilightBegin = dateFormatter.date(from: input.civilTwilightBegin) ?? Date()
    times.civilTwilightEnd = dateFormatter.date(from: input.civilTwilightEnd) ?? Date()
    times.nauticalTwilightBegin = dateFormatter.date(from: input.nauticalTwilightBegin) ?? Date()
    times.nauticalTwilightEnd = dateFormatter.date(from: input.nauticalTwilightEnd) ?? Date()
    times.astronomicalTwilightBegin = dateFormatter.date(from: input.astronomicalTwilightBegin) ?? Date()
    times.astronomicalTwilightEnd = dateFormatter.date(from: input.astronomicalTwilightEnd) ?? Date()

    let timeFormatter = DateFormatter()
    timeFormatter.dateFormat = "HH:mm"

    strings.sunrise = timeFormatter.string(from: times.sunrise)
    strings.sunset = timeFormatter.string(from: times.sunset)
    strings.solarNoon = timeFormatter.string(from: times.solarNoon)
    strings.dayLength = input.dayLength
    strings.civilTwilightBegin = timeFormatter.string(from: times.civilTwilightBegin)
    strings.civilTwilightEnd = timeFormatter.string(from: times.civilTwilightEnd)
    strings.nauticalTwilightBegin = timeFormatter.string(from: times.nauticalTwilightBegin)
    strings.nauticalTwilightEnd = timeFormatter.string(from: times.nauticalTwilightEnd)
    strings.astronomicalTwilightBegin = timeFormatter.string(from: times.astronomicalTwilightBegin)
    strings.astronomicalTwilightEnd = timeFormatter.string(from: times.astronomicalTwilightEnd)
}

}

然后我从 viewController 中的函数调用它:

func getAstronomicalTimes(date: Date, latitude: Float, longitude: Float) -> Void {
    let astronomicalTimes = AstronomicalTimes(date: date, latitude: latitude, longitude: longitude)

    astronomicalTimes.start { result in
        switch result {
        case .success(let astronomicalTimesResponse):
            print("astronomical times response ", astronomicalTimesResponse)
            print("label", astronomicalTimes.strings.astronomicalTwilightBegin)
            self.lblAstDawn.text = astronomicalTimes.strings.astronomicalTwilightBegin

        case .failure(let error):
            print(error)
        }
    }
}

这个函数在 viewDidLoad() 中被调用:

getAstronomicalTimes(date: savedDate, latitude: currentSelection.station.lat, longitude: currentSelection.station.long)

但是,getAstronomicalTimes(date:latitude:longitude) 并没有像我希望的那样更新 lblAstDawn.text。

关于我在哪里弄错的任何线索?

【问题讨论】:

  • DispatchQueue.main.async { self.fillFields(astronomicalTimesResponse.results) }
  • 谢谢,但这仍然不起作用。 fillFields() 填充类属性。这些属性是在 ViewController 中访问的——我在 viewDidLoad() 中创建了一个类的实例(它获取数据然后运行 ​​fillFields()。该实例有空白作为初始属性值,这些是 viewDidLoad 中返回的值()。属性仅在 API 完成时才完整,因此在分配标签后更新 - 因此在 viewDidLoad() 中填充屏幕标签的代码在 API 返回的数据在实例中可用之前执行。

标签: swift multithreading


【解决方案1】:

您需要为您的AstronomicalTimes 请求提供一个完成处理程序,以便它可以告诉您的视图控制器何时检索到数据,然后视图控制器可以更新各个字段。

因此:

enum AstronomicalTimesError: Error {
    case invalidResponse(Data?, URLResponse?)
}

class AstronomicalTimes {
    let date: Date
    let latitude: Float // generally we use Double, but for your purposes, this might be adequate
    let longitude: Float

    init (date: Date, latitude: Float, longitude: Float) {
        self.date = date
        self.latitude = latitude
        self.longitude = longitude
    }

    func start(completion: @escaping (Result<AstronomicalTimesResponse, Error>) -> Void) {
        var components = URLComponents(string: "https://api.sunrise-sunset.org/json")!

        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "en_US_POSIX") // just in case your end user isn't using Gregorian calendar
        dateFormatter.dateFormat = "yyyy-MM-dd"

        components.queryItems = [
            URLQueryItem(name: "lat", value: "\(latitude)"),
            URLQueryItem(name: "lng", value: "\(longitude)"),
            URLQueryItem(name: "date", value: dateFormatter.string(from: date)),
            URLQueryItem(name: "formatted", value: "0")
        ]

        let session = URLSession.shared
        let url = components.url!
        let request = URLRequest(url: url)

        session.dataTask(with: request) { data, response, error in
            if let error = error {
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
                return
            }

            guard
                let responseData = data,
                let httpResponse = response as? HTTPURLResponse,
                200 ..< 300 ~= httpResponse.statusCode
            else {
                DispatchQueue.main.async {
                    completion(.failure(AstronomicalTimesError.invalidResponse(data, response)))
                }
                return
            }

            do {
                let astronomicalTimesResponse = try JSONDecoder().decode(AstronomicalTimesResponse.self, from: responseData)
                DispatchQueue.main.async {
                    completion(.success(astronomicalTimesResponse))
                }
            } catch let jsonError {
                DispatchQueue.main.async {
                    completion(.failure(jsonError))
                }
            }
        }.resume()
    }
}

那么您的viewDidLoad 可能会执行以下操作:

override viewDidLoad() {
    super.viewDidLoad()

    let astronomicalTimes = AstronomicalTimes(date: someDate, latitude: someLatitude, longitude: someLongitude)

    astronomicalTimes.start { result in
        switch result {
        case .success(let astronomicalTimesResponse):
            // populate your fields here

        case .failure(let error):
            print(error)
        }
    }
}

【讨论】:

  • 罗布,谢谢。我真的很感激它,它在使我的 API 调用更加优雅方面帮助了我很多。但是,在 api 方法完成后,我仍然无法更新 UILabel。标签保持空白。我已经在上面发布了我修改后的代码的更新。 API 方法工作正常,类实例的所有属性都已正确返回和格式化,但是当 API 完成时标签仍未更新。
  • AstronomicalTimes 正在调用fillFields,但它不应该。视图控制器应该这样做。 AstronomicalTimes 没有对调用 start 的视图控制器的引用。
  • 好的,我已经编辑了 fillFields(),现在直接从 viewDidLoad() 中创建的 astronomicalTimes 实例的实例更新 UILabels,它可以工作了!但是,我不明白为什么。 fillFields() 所做的只是从 JSON 解码中获取原始结果并将它们格式化为 AstronomicalTimes 的类属性。为什么这些属性无法传递给 viewController?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多