J'ai créé une classe utilitaire dans mon projet Swift qui gère toutes les demandes et réponses REST. J'ai construit une simple API REST pour pouvoir tester mon code. J'ai créé une méthode de classe qui doit renvoyer un NSArray mais, comme l'appel d'API est asynchrone, je dois renvoyer à partir de la méthode contenue dans l'appel asynchrone. Le problème est que le retour asynchrone est nul .. Si je faisais cela dans Node, j'utiliserais les promesses de JS mais je ne peux pas trouver une solution qui fonctionne dans 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...
}
}
Vous pouvez passer un rappel et appeler un rappel dans un appel asynchrone
quelque chose comme:
class func getGenres(completionHandler: (genres: NSArray) -> ()) {
...
let task = session.dataTaskWithURL(url) {
data, response, error in
...
resultsArray = results
completionHandler(genres: resultsArray)
}
...
task.resume()
}
puis appelez cette méthode:
override func viewDidLoad() {
Bookshop.getGenres {
genres in
println("View Controller: \(genres)")
}
}
Swiftz propose déjà Future, qui est la pierre angulaire d’une promesse. Un avenir est une promesse qui ne peut pas échouer (tous les termes ici sont basés sur l'interprétation de Scala, où une promesse est une monade ).
https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.Swift
Espérons que cela se transformera éventuellement en une promesse de style Scala (je pourrais l’écrire moi-même à un moment donné; je suis sûr que d’autres RP seraient les bienvenus; ce n’est pas si difficile avec Future déjà en place).
Dans votre cas particulier, je créerais probablement un Result<[Book]>
(basé sur la version de Alexandros Salazar de Result
). Ensuite, votre signature de méthode serait:
class func fetchGenres() -> Future<Result<[Book]>> {
Remarques
get
dans Swift. Cela rompra certains types d'interopérabilité avec ObjC.Book
avant de renvoyer vos résultats sous la forme Future
. Ce système peut échouer de plusieurs manières et il est beaucoup plus pratique de vérifier toutes ces choses avant de les regrouper dans une Future
. Obtenir le [Book]
est beaucoup mieux pour le reste de votre code Swift que de manipuler une NSArray
.Swift 4.0
Pour async demande-réponse, vous pouvez utiliser le gestionnaire d'achèvement. Voir ci-dessous, j'ai modifié la solution avec le paradigme de la poignée d'achèvement.
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()
}
Vous pouvez appeler cette fonction comme ci-dessous:
getGenres { (array) in
// Do operation with array
}
Swift 3 version de la réponse de @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()
}
J'espère que vous n'êtes pas encore bloqué là-dessus, mais la réponse courte est que vous ne pouvez pas faire cela dans Swift.
Une autre approche consisterait à renvoyer un rappel qui fournira les données dont vous avez besoin dès qu’il est prêt.
Le modèle de base consiste à utiliser la fermeture des gestionnaires d'achèvement.
Par exemple, dans le prochain Swift 5, vous utiliseriez 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()
}
Et vous l’appelez ainsi:
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
Remarque: ci-dessus, je renvoie le gestionnaire d'achèvement dans la file d'attente principale pour simplifier les mises à jour de modèles et d'interface utilisateur. Certains développeurs s'opposent à cette pratique et utilisent la file URLSession
utilisée ou utilisent leur propre file d'attente (ce qui oblige l'appelant à synchroniser manuellement les résultats eux-mêmes).
Mais ce n’est pas important ici. Le problème clé est l'utilisation du gestionnaire d'achèvement pour spécifier le bloc de code à exécuter lorsque la demande asynchrone est effectuée.
Le modèle plus ancien, Swift 4 est:
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()
}
Et vous l’appelez ainsi:
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
Remarque: ci-dessus, j'ai abandonné l'utilisation de NSArray
(nous n'utilisons pas ceux de type Objective-C pontés pas plus). Je suppose que nous avions un type Genre
et que nous avons probablement utilisé JSONDecoder
, plutôt que JSONSerialization
, pour le décoder. Mais cette question ne contenait pas suffisamment d’informations sur le JSON sous-jacent pour entrer dans les détails, c’est pourquoi j’ai omis de ne pas assombrir le problème fondamental, à savoir l’utilisation de fermetures comme gestionnaires d’achèvement.
Il existe 3 façons de créer des fonctions de rappel, à savoir: 1. Gestionnaire de complétion 2. Notification 3. Les délégués
Completion Handler À l'intérieur du jeu de bloc est exécuté et renvoyé lorsque la source est disponible, le gestionnaire attendra la réponse pour pouvoir mettre à jour l'interface utilisateur.
Notification Une foule d’informations est déclenchée sur l’ensemble de l’application, Listner peut alors récupérer et utiliser ces informations. Manière asynchrone d'obtenir des informations tout au long du projet.
Délégués Un ensemble de méthodes sera déclenché à l'appel du délégué, la source doit être fournie via les méthodes elles-mêmes.
C'est un petit cas d'utilisation qui pourrait être utile: -
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()
}
En appelant la fonction: -
testUrlSession(urlStr: "YOUR-URL") { (value) in
print("Your string value ::- \(value)")
}
Gestionnaire de fermetures/achèvement
Les délégués
Les notifications
Les observateurs peuvent également être utilisés pour être averti une fois la tâche asynchrone terminée.
Certaines exigences très génériques voudraient que tout bon API Manager satisfasse: implémentera un client API orienté protocole.
Interface initiale APIClient
protocol APIClient {
func send(_ request: APIRequest,
completion: @escaping (APIResponse?, Error?) -> Void)
}
protocol APIRequest: Encodable {
var resourceName: String { get }
}
protocol APIResponse: Decodable {
}
Maintenant, veuillez vérifier la structure complète de l'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)
}
}
}
Utilisez des blocs de complétion et activez ensuite sur le thread principal.
Le fil principal est le fil de l'interface utilisateur. Chaque fois que vous créez une tâche async et que vous souhaitez mettre à jour l'interface utilisateur, vous devez effectuer toutes les modifications de l'interface utilisateur sur le fil de l'interface utilisateur
exemple:
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()
})
}
}