Возврат данных из асинхронного вызова в функции Swift
Я создал служебный класс в своем проекте Swift, который обрабатывает все запросы и ответы REST. Я создал простой REST API, чтобы проверить свой код. Я создал метод класса, который должен возвращать NSArray, но поскольку вызов API является асинхронным, мне нужно возвращаться из метода внутри асинхронного вызова. Проблема в том, что асинхронный возврат void. Если бы я делал это в Node, я бы использовал обещания JS, но я не могу найти решение, которое работает в Swift.
import Foundation
class Bookshop {
class func getGenres() -> NSArray {
println("Hello inside getGenres")
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
println(urlPath)
let url: NSURL = NSURL(string: urlPath)
let session = NSURLSession.sharedSession()
var resultsArray:NSArray!
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
println("Task completed")
if(error) {
println(error.localizedDescription)
}
var err: NSError?
var options:NSJSONReadingOptions = NSJSONReadingOptions.MutableContainers
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: options, error: &err) as NSDictionary
if(err != nil) {
println("JSON Error \(err!.localizedDescription)")
}
//NSLog("jsonResults %@", jsonResult)
let results: NSArray = jsonResult["genres"] as NSArray
NSLog("jsonResults %@", results)
resultsArray = results
return resultsArray // error [anyObject] is not a subType of 'Void'
})
task.resume()
//return "Hello World!"
// I want to return the NSArray...
}
}
15 ответов
Вы можете передать обратный вызов и обратный вызов внутри асинхронного вызова
что-то вроде:
class func getGenres(completionHandler: (genres: NSArray) -> ()) {
...
let task = session.dataTaskWithURL(url) {
data, response, error in
...
resultsArray = results
completionHandler(genres: resultsArray)
}
...
task.resume()
}
и затем вызовите этот метод:
override func viewDidLoad() {
Bookshop.getGenres {
genres in
println("View Controller: \(genres)")
}
}
Основной шаблон заключается в использовании закрытия обработчиков завершения.
Например, в предстоящем Swift 5 вы бы использовали Result
:
func fetchGenres(completion: @escaping (Result<[Genre], Error>) -> Void) {
...
URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
// parse response here
let results = ...
DispatchQueue.main.async {
completion(.success(results))
}
}.resume()
}
И вы бы назвали это так:
fetchGenres { results in
switch results {
case .success(let genres):
// use genres here, e.g. update model and UI
case .failure(let error):
print(error.localizedDescription)
}
}
// but don’t try to use genres here, as the above runs asynchronously
Обратите внимание, что выше я отправляю обработчик завершения обратно в основную очередь, чтобы упростить обновления модели и пользовательского интерфейса. Некоторые разработчики возражают против этой практики и используют любую очередь URLSession
использовать или использовать собственную очередь (требуя, чтобы вызывающая сторона самостоятельно синхронизировала результаты).
Но это не материал здесь. Ключевой вопрос заключается в использовании обработчика завершения для указания блока кода, который будет выполняться при выполнении асинхронного запроса.
Более старая модель Swift 4:
func fetchGenres(completion: @escaping ([Genre]?, Error?) -> Void) {
...
URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error {
DispatchQueue.main.async {
completion(nil, error)
}
return
}
// parse response here
let results = ...
DispatchQueue.main.async {
completion(results, error)
}
}.resume()
}
И вы бы назвали это так:
fetchGenres { genres, error in
guard let genres = genres, error == nil else {
// handle failure to get valid response here
return
}
// use genres here
}
// but don’t try to use genres here, as the above runs asynchronously
Обратите внимание, выше я удалился от использования NSArray
(мы больше не используем эти соединенные типы Objective-C). Я предполагаю, что у нас был Genre
типа и мы предположительно использовали JSONDecoder
, скорее, чем JSONSerialization
, чтобы расшифровать его. Но у этого вопроса не было достаточно информации о базовом JSON, чтобы вдаваться в подробности, поэтому я пропустил это, чтобы не затуманивать основную проблему - использование замыканий в качестве обработчиков завершения.
Swiftz уже предлагает Future, который является основным строительным блоком Promise. Будущее - это Обещание, которое не может не исполниться (все термины здесь основаны на интерпретации Scala, где Обещание - это Монада).
https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift
Надеюсь, в конечном итоге расширится до полного Обещания в стиле Scala (я могу написать его сам в какой-то момент; я уверен, что другие пиарщики будут приветствоваться; это не так уж и сложно с Future уже на месте).
В вашем конкретном случае я бы, вероятно, создал Result<[Book]>
(по версии Александра СалазараResult
). Тогда ваша подпись метода будет:
class func fetchGenres() -> Future<Result<[Book]>> {
Заметки
- Я не рекомендую префиксные функции с
get
в Свифте. Это нарушит определенные виды взаимодействия с ObjC. - Я рекомендую разбирать весь путь до
Book
объект, прежде чем возвращать свои результаты какFuture
, Есть несколько способов, которыми эта система может выйти из строя, и гораздо удобнее, если вы проверите все эти вещи, прежде чем свернуть их вFuture
, Добраться до[Book]
гораздо лучше для остальной части вашего кода Swift, чем передатьNSArray
,
Swift 4.0
Для асинхронного запроса-ответа вы можете использовать обработчик завершения. Смотрите ниже, я изменил решение с парадигмой дескриптора завершения.
func getGenres(_ completion: @escaping (NSArray) -> ()) {
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
print(urlPath)
guard let url = URL(string: urlPath) else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary {
let results = jsonResult["genres"] as! NSArray
print(results)
completion(results)
}
} catch {
//Catch Error here...
}
}
task.resume()
}
Вы можете вызвать эту функцию, как показано ниже:
getGenres { (array) in
// Do operation with array
}
Другой пример:
class func getExchangeRate(#baseCurrency: String, foreignCurrency:String, completion: ((result:Double?) -> Void)!){
let baseURL = kAPIEndPoint
let query = String(baseCurrency)+"_"+String(foreignCurrency)
var finalExchangeRate = 0.0
if let url = NSURL(string: baseURL + query) {
NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
if ((data) != nil) {
let jsonDictionary:NSDictionary = NSJSONSerialization.JSONObjectWithData(data!, options: nil, error: nil) as NSDictionary
if let results = jsonDictionary["results"] as? NSDictionary{
if let queryResults = results[query] as? NSDictionary{
if let exchangeRate = queryResults["val"] as? Double{
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
dispatch_async(dispatch_get_main_queue()) {
completion(result: exchangeRate)
}
}
}
}
}
}
else {
completion(result: nil)
}
}.resume()
}
}
Вызов:
Currency.getExchangeRate(baseCurrency: "USD", foreignCurrency: "EUR") { (result) -> Void in
if let exchangeValue = result {
print(exchangeValue)
}
}
Swift 5.5, решение на основе async / wait
Исходный тестовый URL-адрес, предоставленный исходным плакатом, больше не работает, поэтому мне пришлось немного изменить ситуацию. Это решение основано на найденном мной API шуток. Этот API возвращает единственную шутку, но я возвращаю ее как массив String (
[String]
), чтобы он как можно больше соответствовал исходному сообщению.
class Bookshop {
class func getGenres() async -> [String] {
print("Hello inside getGenres")
let urlPath = "https://geek-jokes.sameerkumar.website/api?format=json"
print(urlPath)
let url = URL(string: urlPath)!
let session = URLSession.shared
typealias Continuation = CheckedContinuation<[String], Never>
let genres = await withCheckedContinuation { (continuation: Continuation) in
let task = session.dataTask(with: url) { data, response, error in
print("Task completed")
var result: [String] = []
defer {
continuation.resume(returning: result)
}
if let error = error {
print(error.localizedDescription)
return
}
guard let data = data else {
return
}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: [.mutableContainers])
print("jsonResult is \(jsonResult)")
if let joke = (jsonResult as? [String: String])?["joke"] {
result = [joke]
}
} catch {
print("JSON Error \(error.localizedDescription)")
print("data was \(String(describing: String(data: data, encoding: .utf8)))")
return
}
}
task.resume()
}
return genres
}
}
async {
let final = await Bookshop.getGenres()
print("Final is \(final)")
}
В
withCheckedContinuation
как ты сделал Свифт
async
функция фактически выполняется в отдельной задаче / потоке.
Версия Swift 3 @Alexey Globchastyy:
class func getGenres(completionHandler: @escaping (genres: NSArray) -> ()) {
...
let task = session.dataTask(with:url) {
data, response, error in
...
resultsArray = results
completionHandler(genres: resultsArray)
}
...
task.resume()
}
Я надеюсь, что вы все еще не застряли в этом, но короткий ответ заключается в том, что вы не можете сделать это в Swift.
Альтернативным подходом будет возврат обратного вызова, который предоставит необходимые данные, как только он будет готов.
Существует 3 способа создания функций обратного вызова, а именно: 1. Обработчик завершения 2. Уведомление 3. Делегаты
Обработчик завершения Внутри набора блоков выполняется и возвращается, когда источник доступен, Обработчик будет ждать ответа до тех пор, пока пользовательский интерфейс не будет обновлен.
Уведомление Куча информации запускается по всему приложению, Listner может извлечь и использовать эту информацию. Асинхронный способ получения информации через проект.
Делегаты Набор методов будет запущен при вызове делегата, источник должен быть предоставлен через сами методы.
Swift 5.5:
TL;DR: Swift 5.5 еще не выпущен (на момент написания). Чтобы использовать swift 5.5, загрузите снимок разработки быстрой инструментальной цепочки отсюда и добавьте флаг компилятора.
-Xfrontend -enable-experimental-concurrency
. Подробнее читайте здесь
Этого легко добиться с помощью
async/await
характерная черта.
Для этого вы должны отметить свою функцию, а затем выполнить операцию внутри
withUnsafeThrowingContinuation
блок как следующий.
class Bookshop {
class func getGenres() async throws -> NSArray {
print("Hello inside getGenres")
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
print(urlPath)
let url = URL(string: urlPath)!
let session = URLSession.shared
return try await withUnsafeThrowingContinuation { continuation in
let task = session.dataTask(with: url, completionHandler: {data, response, error -> Void in
print("Task completed")
if(error != nil) {
print(error!.localizedDescription)
continuation.resume(throwing: error!)
return
}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any]
let results: NSArray = jsonResult!["genres"] as! NSArray
continuation.resume(returning: results)
} catch {
continuation.resume(throwing: error)
}
})
task.resume()
}
}
}
И вы можете вызвать эту функцию как
@asyncHandler
func check() {
do {
let genres = try await Bookshop.getGenres()
print("Result: \(genres)")
} catch {
print("Error: \(error)")
}
}
Имейте в виду, что при звонке
Bookshop.getGenres
метод вызывающего объекта должен быть либо
async
или помечено как
@asyncHandler
Используйте блоки завершения и активируйте затем в главном потоке.
Основным потоком является поток пользовательского интерфейса. Каждый раз, когда вы выполняете асинхронную задачу и хотите обновить пользовательский интерфейс, вы должны выполнять все изменения пользовательского интерфейса в потоке пользовательского интерфейса.
пример:
func asycFunc(completion: () -> Void) {
URLSession.shared.dataTask(with: request) { data, _, error in
// This is an async task...!!
if let error = error {
}
DispatchQueue.main.async(execute: { () -> Void in
//When the async taks will be finished this part of code will run on the main thread
completion()
})
}
}
Это небольшой пример использования, который может быть полезен:
func testUrlSession(urlStr:String, completionHandler: @escaping ((String) -> Void)) {
let url = URL(string: urlStr)!
let task = URLSession.shared.dataTask(with: url){(data, response, error) in
guard let data = data else { return }
if let strContent = String(data: data, encoding: .utf8) {
completionHandler(strContent)
}
}
task.resume()
}
При вызове функции:-
testUrlSession(urlStr: "YOUR-URL") { (value) in
print("Your string value ::- \(value)")
}
Существуют некоторые очень общие требования, которые должны удовлетворять все хорошие API-менеджеры: реализовать API-клиент, ориентированный на протокол.
APIClient Начальный интерфейс
protocol APIClient {
func send(_ request: APIRequest,
completion: @escaping (APIResponse?, Error?) -> Void)
}
protocol APIRequest: Encodable {
var resourceName: String { get }
}
protocol APIResponse: Decodable {
}
Теперь, пожалуйста, проверьте полную структуру API
// ******* This is API Call Class *****
public typealias ResultCallback<Value> = (Result<Value, Error>) -> Void
/// Implementation of a generic-based API client
public class APIClient {
private let baseEndpointUrl = URL(string: "irl")!
private let session = URLSession(configuration: .default)
public init() {
}
/// Sends a request to servers, calling the completion method when finished
public func send<T: APIRequest>(_ request: T, completion: @escaping ResultCallback<DataContainer<T.Response>>) {
let endpoint = self.endpoint(for: request)
let task = session.dataTask(with: URLRequest(url: endpoint)) { data, response, error in
if let data = data {
do {
// Decode the top level response, and look up the decoded response to see
// if it's a success or a failure
let apiResponse = try JSONDecoder().decode(APIResponse<T.Response>.self, from: data)
if let dataContainer = apiResponse.data {
completion(.success(dataContainer))
} else if let message = apiResponse.message {
completion(.failure(APIError.server(message: message)))
} else {
completion(.failure(APIError.decoding))
}
} catch {
completion(.failure(error))
}
} else if let error = error {
completion(.failure(error))
}
}
task.resume()
}
/// Encodes a URL based on the given request
/// Everything needed for a public request to api servers is encoded directly in this URL
private func endpoint<T: APIRequest>(for request: T) -> URL {
guard let baseUrl = URL(string: request.resourceName, relativeTo: baseEndpointUrl) else {
fatalError("Bad resourceName: \(request.resourceName)")
}
var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true)!
// Common query items needed for all api requests
let timestamp = "\(Date().timeIntervalSince1970)"
let hash = "\(timestamp)"
let commonQueryItems = [
URLQueryItem(name: "ts", value: timestamp),
URLQueryItem(name: "hash", value: hash),
URLQueryItem(name: "apikey", value: "")
]
// Custom query items needed for this specific request
let customQueryItems: [URLQueryItem]
do {
customQueryItems = try URLQueryItemEncoder.encode(request)
} catch {
fatalError("Wrong parameters: \(error)")
}
components.queryItems = commonQueryItems + customQueryItems
// Construct the final URL with all the previous data
return components.url!
}
}
// ****** API Request Encodable Protocol *****
public protocol APIRequest: Encodable {
/// Response (will be wrapped with a DataContainer)
associatedtype Response: Decodable
/// Endpoint for this request (the last part of the URL)
var resourceName: String { get }
}
// ****** This Results type Data Container Struct ******
public struct DataContainer<Results: Decodable>: Decodable {
public let offset: Int
public let limit: Int
public let total: Int
public let count: Int
public let results: Results
}
// ***** API Errro Enum ****
public enum APIError: Error {
case encoding
case decoding
case server(message: String)
}
// ****** API Response Struct ******
public struct APIResponse<Response: Decodable>: Decodable {
/// Whether it was ok or not
public let status: String?
/// Message that usually gives more information about some error
public let message: String?
/// Requested data
public let data: DataContainer<Response>?
}
// ***** URL Query Encoder OR JSON Encoder *****
enum URLQueryItemEncoder {
static func encode<T: Encodable>(_ encodable: T) throws -> [URLQueryItem] {
let parametersData = try JSONEncoder().encode(encodable)
let parameters = try JSONDecoder().decode([String: HTTPParam].self, from: parametersData)
return parameters.map { URLQueryItem(name: $0, value: $1.description) }
}
}
// ****** HTTP Pamater Conversion Enum *****
enum HTTPParam: CustomStringConvertible, Decodable {
case string(String)
case bool(Bool)
case int(Int)
case double(Double)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else if let double = try? container.decode(Double.self) {
self = .double(double)
} else {
throw APIError.decoding
}
}
var description: String {
switch self {
case .string(let string):
return string
case .bool(let bool):
return String(describing: bool)
case .int(let int):
return String(describing: int)
case .double(let double):
return String(describing: double)
}
}
}
/// **** This is your API Request Endpoint Method in Struct *****
public struct GetCharacters: APIRequest {
public typealias Response = [MyCharacter]
public var resourceName: String {
return "characters"
}
// Parameters
public let name: String?
public let nameStartsWith: String?
public let limit: Int?
public let offset: Int?
// Note that nil parameters will not be used
public init(name: String? = nil,
nameStartsWith: String? = nil,
limit: Int? = nil,
offset: Int? = nil) {
self.name = name
self.nameStartsWith = nameStartsWith
self.limit = limit
self.offset = offset
}
}
// *** This is Model for Above Api endpoint method ****
public struct MyCharacter: Decodable {
public let id: Int
public let name: String?
public let description: String?
}
// ***** These below line you used to call any api call in your controller or view model ****
func viewDidLoad() {
let apiClient = APIClient()
// A simple request with no parameters
apiClient.send(GetCharacters()) { response in
response.map { dataContainer in
print(dataContainer.results)
}
}
}
self.urlSession.dataTask(with: request, completionHandler: { (data, response, error) in
self.endNetworkActivity()
var responseError: Error? = error
// handle http response status
if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode > 299 , httpResponse.statusCode != 422 {
responseError = NSError.errorForHTTPStatus(httpResponse.statusCode)
}
}
var apiResponse: Response
if let _ = responseError {
apiResponse = Response(request, response as? HTTPURLResponse, responseError!)
self.logError(apiResponse.error!, request: request)
// Handle if access token is invalid
if let nsError: NSError = responseError as NSError? , nsError.code == 401 {
DispatchQueue.main.async {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
let message = apiResponse.message()
// Unautorized access
// User logout
return
}
}
else if let nsError: NSError = responseError as NSError? , nsError.code == 503 {
DispatchQueue.main.async {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
let message = apiResponse.message()
// Down time
// Server is currently down due to some maintenance
return
}
}
} else {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
self.logResponse(data!, forRequest: request)
}
self.removeRequestedURL(request.url!)
DispatchQueue.main.async(execute: { () -> Void in
completionHandler(apiResponse)
})
}).resume()
Есть в основном 3 способа достижения обратного вызова в Swift
Закрытие / Обработчик завершения
Делегаты
Уведомления
Наблюдатели также можно использовать для получения уведомлений после выполнения асинхронной задачи.