Возврат данных из асинхронного вызова в функции Swift
Я создал класс утилиты в моем проекте Swift, который обрабатывает все запросы и ответы REST. Я создал простой REST API, чтобы проверить свой код. Я создал метод класса, который должен вернуть NSArray, но поскольку вызов API является асинхронным, мне нужно вернуться из метода внутри асинхронного вызова. Проблема заключается в том, что async возвращает void.
Если бы я делал это в Node, я использовал бы JS promises, но я не могу найти решение, которое работает в 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...
}
}
Ответы
Ответ 1
Вы можете передать обратный вызов и вызвать обратный вызов внутри асинхронного вызова
что-то вроде:
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)")
}
}
Ответ 2
Swiftz уже предлагает Future, который является основным строительным блоком Promise. Будущее - это обещание, которое не может потерпеть неудачу (все термины здесь основаны на интерпретации Scala, где Promise является монадой).
https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift
Надеюсь, что в конце концов мы сможем написать его в полной мере Scala -style Promise (я могу написать его сам в какой-то момент, я уверен, что другие PR будут приветствоваться, это не так сложно с будущим уже на месте).
В вашем конкретном случае я, вероятно, создаю Result<[Book]>
(на основе Alexandros Salazar версии Result
). Тогда ваша подпись метода будет:
class func fetchGenres() -> Future<Result<[Book]>> {
Примечания
- Я не рекомендую функции префикса
get
в Swift. Это нарушит определенные виды совместимости с ObjC.
- Прежде чем возвращать результаты в качестве
Future
, я рекомендую разделить весь объект до объекта Book
. Существует несколько способов, с помощью которых эта система может выйти из строя, и гораздо удобнее, если вы проверите все эти вещи, прежде чем обернуть их в Future
. Переход на [Book]
намного лучше для остальной части вашего кода Swift, чем передача NSArray
.
Ответ 3
Другой пример:
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)
}
}
Ответ 4
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
}
Ответ 5
Свифт 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()
}
Ответ 6
Основной шаблон заключается в использовании закрытия обработчиков завершения.
Например, в предстоящем 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 dont try to use genres here, as the above runs asynchronously
Обратите внимание, что выше я отправляю обработчик завершения обратно в основную очередь, чтобы упростить обновления модели и пользовательского интерфейса. Некоторые разработчики URLSession
против этой практики и используют либо используемую очередь 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 dont try to use genres here, as the above runs asynchronously
Обратите внимание, что выше я NSArray
использование NSArray
(мы больше не используем эти соединенные типы Objective-C). Я предполагаю, что у нас был тип Genre
и мы предположительно использовали JSONDecoder
, а не JSONSerialization
, чтобы декодировать его. Но у этого вопроса не было достаточно информации о базовом JSON, чтобы вдаваться в подробности, поэтому я пропустил это, чтобы избежать затуманивания основной проблемы - использования замыканий в качестве обработчиков завершения.
Ответ 7
Я надеюсь, что вы все еще не застряли в этом, но короткий ответ заключается в том, что вы не можете сделать это в Swift.
Альтернативным подходом будет возврат обратного вызова, который предоставит необходимые данные, как только он будет готов.
Ответ 8
Существует 3 способа создания функций обратного вызова, а именно: 1. Обработчик завершения 2. Уведомление 3. Делегаты
Обработчик завершения Внутри набора блоков выполняется и возвращается, когда источник доступен, Обработчик будет ждать ответа до тех пор, пока пользовательский интерфейс не будет обновлен.
Уведомление Куча информации запускается по всему приложению, Listner может извлечь и использовать эту информацию. Асинхронный способ получения информации через проект.
Делегаты Набор методов будет запущен при вызове делегата, источник должен быть предоставлен через сами методы.
Ответ 9
Используйте блоки завершения и активируйте затем в главном потоке.
Основным потоком является поток пользовательского интерфейса. Каждый раз, когда вы выполняете асинхронную задачу и хотите обновить пользовательский интерфейс, вы должны выполнять все изменения пользовательского интерфейса в потоке пользовательского интерфейса.
пример:
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()
})
}
}
Ответ 10
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()
Ответ 11
Есть в основном 3 способа достижения обратного вызова в Swift
-
Закрытие/Обработчик завершения
-
Делегаты
-
Уведомления
Наблюдатели также можно использовать для получения уведомлений после выполнения асинхронной задачи.
Ответ 12
Существуют некоторые очень общие требования, которые должны удовлетворять все хорошие 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 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)
}
}
}
Ответ 13
Это небольшой пример использования, который может быть полезен: -
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)")
}