web-dev-qa-db-fra.com

Renvoi de données d'un appel asynchrone dans la fonction Swift

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...
    }
}
61
Mark Tyers

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)")     
    }
}
68
Alexey Globchastyy

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

  • Je ne recommande pas de préfixer des fonctions avec get dans Swift. Cela rompra certains types d'interopérabilité avec ObjC.
  • Je recommande d’analyser l’ensemble d’un objet 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.
10
Rob Napier

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
}
5
Jaydeep

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()
}
3
Nebojsa Nadj

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.

2
LironXYZ

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.

1
Rob

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.

0
IRANNA SALUNKE

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)")
}
0
a.palo

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

  1. Gestionnaire de fermetures/achèvement

  2. Les délégués

  3. Les notifications

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

0
HSAM

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

}
0
Rashpinder Maan

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

        }

}
0
TalBenAsulo