Retour des données d'un appel asynchrone dans la fonction Swift

93

J'ai créé une classe utilitaire dans mon projet Swift qui gère toutes les demandes et réponses REST. J'ai construit une API REST simple 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 retourner de la méthode à l'intérieur de l'appel async. Le problème est que l'async renvoie nul. Si je faisais cela dans Node, j'utiliserais les promesses 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...
    }
}
Mark Tyers
la source
5
Cette erreur est si courante sur Stack Overflow que j'ai écrit une série d'articles de blog pour y remédier, en commençant par programios.net/what-asynchronous-means
matt

Réponses:

96

Vous pouvez passer le rappel et appeler le 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)")     
    }
}
Alexey Globchastyy
la source
Merci pour ça. Ma dernière question est de savoir comment appeler cette méthode de classe à partir de mon contrôleur de vue. Le code est actuellement comme ceci: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) }
Mark Tyers
13

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 Scala, où une promesse est une monade ).

https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift

J'espère que cela finira par devenir une promesse complète de style Scala (je pourrai l'écrire moi-même à un moment donné; je suis sûr que d'autres PR 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 d'Alexandros Salazar deResult ). Ensuite, votre signature de méthode serait:

class func fetchGenres() -> Future<Result<[Book]>> {

Remarques

  • Je ne recommande pas de préfixer les fonctions avec get dans Swift. Cela rompra certains types d'interopérabilité avec ObjC.
  • Je recommande l'analyse jusqu'à un Bookobjet avant de renvoyer vos résultats sous forme de fichier 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 un fichier Future. Il [Book]est bien meilleur pour le reste de votre code Swift que de remettre un fichier NSArray.
Rob Napier
la source
4
Swiftz ne prend plus en charge Future. Mais jetez un œil à github.com/mxcl/PromiseKit cela fonctionne très bien avec Swiftz!
badeleux
m'a pris quelques secondes pour réaliser que vous n'aviez pas écrit Swift et que vous aviez écrit Swift z
Honey
4
Cela ressemble à "Swiftz" est une bibliothèque fonctionnelle tierce pour Swift. Puisque votre réponse semble être basée sur cette bibliothèque, vous devez le déclarer explicitement. (par exemple, "Il existe une bibliothèque tierce appelée 'Swiftz' qui prend en charge les constructions fonctionnelles comme Futures, et devrait servir de bon point de départ si vous voulez mettre en œuvre Promises.") Sinon, vos lecteurs vont simplement se demander pourquoi vous avez mal orthographié " Rapide".
Duncan C
3
Veuillez noter que github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift ne fonctionne plus.
Ahmad F
1
@Rob Le getpréfixe indique le retour par référence dans ObjC (comme dans -[UIColor getRed:green:blue:alpha:]). Quand j'ai écrit ceci, je craignais que les importateurs exploitent ce fait (pour renvoyer automatiquement un tuple par exemple). Il s'est avéré que non. Quand j'ai écrit ceci, j'avais probablement aussi oublié que KVC supporte les préfixes "get" pour les accesseurs (c'est quelque chose que j'ai appris et oublié plusieurs fois). Donc d'accord; Je n'ai rencontré aucun cas où le leader getcasse les choses. C'est juste trompeur pour ceux qui connaissent la signification de «obtenir» ObjC.
Rob Napier
9

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'appeleriez comme ça:

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

Notez que ci-dessus, je renvoie le gestionnaire d'achèvement dans la file d'attente principale pour simplifier les mises à jour du modèle et de l'interface utilisateur. Certains développeurs s'opposent à cette pratique et utilisent n'importe quelle file d'attenteURLSession utilisée ou utilisent leur propre file d'attente (obligeant l'appelant à synchroniser manuellement les résultats lui-même).

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 requête 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'appeleriez comme ça:

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

Notez que ci-dessus, j'ai retiré l'utilisation de NSArray(nous n'utilisons plus ces types Objective-C pontés ). Je suppose que nous avions un Genretype et que nous l'avons probablement utilisé JSONDecoderplutôt que JSONSerializationpour le décoder. Mais cette question ne contenait pas suffisamment d'informations sur le JSON sous-jacent pour entrer dans les détails ici, j'ai donc omis cela pour éviter de brouiller le problème principal, l'utilisation de fermetures comme gestionnaires d'achèvement.

Rob
la source
Vous pouvez également l'utiliser Resultdans Swift 4 et les versions antérieures, mais vous devez déclarer l'énumération vous-même. J'utilise ce genre de modèle depuis des années.
vadian le
Oui, bien sûr, tout comme moi. Mais il semble que cela ait été adopté par Apple avec la sortie de Swift 5. Ils sont juste en retard à la fête.
Rob
7

Swift 4.0

Pour async Request-Response, 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
}
Jaydeep Vora
la source
2

Version Swift 3 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()
}
Nebojsa Nadj
la source
2

J'espère que vous n'êtes pas encore coincé 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'elles sont prêtes.

LironXYZ
la source
1
Il peut aussi faire des promesses rapidement. Mais l'aproceh actuellement recommandé par Apple utilise callbackavec closures comme vous le faites remarquer ou à utiliser delegationcomme les anciennes API de cacao
Mojtaba Hosseini
Vous avez raison sur les promesses. Mais Swift ne fournit pas d'API native pour cela, il doit donc utiliser PromiseKit ou une autre alternative.
LironXYZ
1

Il existe 3 façons de créer des fonctions de rappel à savoir: 1. Gestionnaire d'achèvement 2. Notification 3. Délégués

Completion Handler L' intérieur du bloc de blocs est exécuté et renvoyé lorsque la source est disponible, le gestionnaire attendra que la réponse arrive pour que l'interface utilisateur puisse être mise à jour après.

Notification Un tas d'informations est déclenché sur toute l'application, Listner peut récupérer n utiliser ces informations. Moyen asynchrone d'obtenir des informations tout au long du projet.

Délégués L' ensemble de méthodes sera déclenché lorsque le délégué est appelé, la source doit être fournie via les méthodes elle-même

IRANNA SALUNKE
la source
-1
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()
CrazyPro007
la source
-1

Il existe principalement 3 façons de réaliser un rappel dans Swift

  1. Gestionnaire de fermetures / achèvement

  2. Délégués

  3. Notifications

Les observateurs peuvent également être utilisés pour être notifié une fois la tâche asynchrone terminée.

Shubham Mishra
la source
-2

Il existe des exigences très génériques que tout bon gestionnaire d'API devrait satisfaire: 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)
        }
    }

}
Rashpinder Maan
la source
-2

Ceci 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()
    }

Lors de l'appel de la fonction: -

testUrlSession(urlStr: "YOUR-URL") { (value) in
            print("Your string value ::- \(value)")
}
a.palo
la source