Я создал служебный класс в моем проекте Swift, который обрабатывает все запросы и ответы REST. Я создал простой REST API, чтобы проверить свой код. Я создал метод класса, который должен возвращать NSArray, но поскольку вызов API является асинхронным, мне нужно вернуться из метода внутри асинхронного вызова. Проблема в том, что async возвращает 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...
}
}
ios
rest
asynchronous
swift
Марк Тайерс
источник
источник
Ответы:
Вы можете передать обратный вызов и обратный вызов внутри асинхронного вызова
что-то типа:
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)") } }
источник
override func viewDidLoad() { super.viewDidLoad() var genres = Bookshop.getGenres() // Missing argument for parameter #1 in call //var genres:NSArray //Bookshop.getGenres(genres) NSLog("View Controller: %@", genres) }
Swiftz уже предлагает Future, который является основным строительным блоком Promise. Будущее - это обещание, которое не может потерпеть неудачу (все термины здесь основаны на интерпретации Scala, где обещание - это монада ).
https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift
Надеюсь, в конечном итоге он будет расширен до полного обещания в стиле Scala (в какой-то момент я могу написать его сам; я уверен, что другие PR будут приветствоваться; это не так сложно, если Future уже существует).
В вашем конкретном случае я бы, вероятно, создал
Result<[Book]>
(на основе версии Александроса СалазараResult
). Тогда ваша подпись метода будет:class func fetchGenres() -> Future<Result<[Book]>> {
Примечания
get
в Swift. Это нарушит определенные виды взаимодействия с ObjC.Book
объекта, прежде чем возвращать результаты в виде файлаFuture
. Есть несколько причин, по которым эта система может выйти из строя, и будет намного удобнее, если вы проверите все эти вещи перед тем, как заключить их в файлFuture
.[Book]
Для остальной части вашего кода Swift гораздо лучше добраться, чем передаватьNSArray
.источник
Future
. Но взгляните на github.com/mxcl/PromiseKit, он отлично работает со Swiftz!get
Префикс указывает на возврат по ссылке в ObjC (например, in-[UIColor getRed:green:blue:alpha:]
). Когда я писал это, я был обеспокоен тем, что импортеры воспользуются этим фактом (например, для автоматического возврата кортежа). Оказалось, что нет. Когда я писал это, я, вероятно, также забыл, что KVC поддерживает префиксы "get" для аксессоров (это то, что я выучил и забыл несколько раз). Итак, согласился; Я не встречал случаев, когда ведущий что-тоget
ломает. Это просто вводит в заблуждение тех, кто знает значение слова ObjC «получить».Основной шаблон - использовать закрытие обработчиков завершения.
Например, в готовящемся к выпуску 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, чтобы вдаваться в подробности, поэтому я пропустил это, чтобы не затушевывать основную проблему, использование замыканий в качестве обработчиков завершения.источник
Result
Swift 4 и ниже, но вам нужно объявить перечисление самостоятельно. Я использую такой узор годами.Swift 4.0
Для async Request-Response вы можете использовать обработчик завершения. См. Ниже. Я изменил решение с парадигмой дескриптора завершения.
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 }
источник
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.
Альтернативный подход - вернуть обратный вызов, который предоставит вам необходимые данные, как только они будут готовы.
источник
callback
сclosure
s, как вы указываете, или для использования,delegation
как более старые API-интерфейсы какаоЕсть 3 способа создания функций обратного вызова, а именно: 1. Обработчик завершения 2. Уведомление 3. Делегаты
Обработчик завершения Внутренний набор блоков выполняется и возвращается, когда источник доступен, обработчик будет ждать, пока не придет ответ, чтобы пользовательский интерфейс мог быть обновлен после.
Уведомление. Объем информации передается по всему приложению, Listner может получить и использовать эту информацию. Асинхронный способ получения информации по проекту.
Делегаты Набор методов будет срабатывать при вызове делегата, источник должен быть предоставлен через сами методы
источник
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 способа быстро выполнить обратный вызов.
Закрытие / Обработчик завершения
Делегаты
Уведомления
Наблюдатели также можно использовать для получения уведомлений после завершения асинхронной задачи.
источник
Есть несколько очень общих требований, которым должен удовлетворять каждый хороший менеджер 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) } } }
источник
Это небольшой вариант использования, который может быть полезен: -
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)") }
источник