Restituzione di dati da una chiamata asincrona nella funzione Swift


93

Ho creato una classe di utilità nel mio progetto Swift che gestisce tutte le richieste e le risposte REST. Ho creato una semplice API REST in modo da poter testare il mio codice. Ho creato un metodo di classe che deve restituire un NSArray ma poiché la chiamata API è asincrona, devo tornare dal metodo all'interno della chiamata asincrona. Il problema è che l'asincronia restituisce void. Se lo facessi in Node, userei le promesse di JS ma non riesco a trovare una soluzione che funzioni in 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...
    }
}

5
Questo errore è così comune su Stack Overflow che ho scritto una serie di post sul blog per affrontarlo, a partire da programmingios.net/what-asynchronous-means
matt

Risposte:


96

È possibile passare la richiamata e richiamare la richiamata all'interno di una chiamata asincrona

qualcosa di simile a:

class func getGenres(completionHandler: (genres: NSArray) -> ()) {
    ...
    let task = session.dataTaskWithURL(url) {
        data, response, error in
        ...
        resultsArray = results
        completionHandler(genres: resultsArray)
    }
    ...
    task.resume()
}

e quindi chiama questo metodo:

override func viewDidLoad() {
    Bookshop.getGenres {
        genres in
        println("View Controller: \(genres)")     
    }
}

Grazie per quello. La mia ultima domanda è come chiamare questo metodo di classe dal mio controller di visualizzazione. Il codice è attualmente così: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 offre già Future, che è l'elemento costitutivo di base di una Promessa. Un futuro è una promessa che non può fallire (tutti i termini qui sono basati sull'interpretazione della Scala, dove una promessa è una monade ).

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

Si spera che alla fine si espanderà a una Promessa in stile Scala (potrei scriverla io stesso ad un certo punto; sono sicuro che altri PR sarebbero i benvenuti; non è così difficile con Future già in atto).

Nel tuo caso particolare, probabilmente creerei un Result<[Book]>(basato sulla versione di Alexandros Salazar diResult ). Quindi la firma del tuo metodo sarebbe:

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

Appunti

  • Non consiglio di anteporre alle funzioni get in Swift. Interromperà alcuni tipi di interoperabilità con ObjC.
  • Consiglio di analizzare fino in fondo un Bookoggetto prima di restituire i risultati come file Future. Ci sono diversi modi in cui questo sistema può fallire, ed è molto più conveniente se controlli tutte queste cose prima di avvolgerle in un file Future. Arrivare [Book]è molto meglio per il resto del tuo codice Swift che consegnare un file NSArray.

4
Swiftz non supporta più Future. Ma dai un'occhiata a github.com/mxcl/PromiseKit , funziona benissimo con Swiftz!
badeleux

Mi ci sono voluti alcuni secondi per capire che non hai scritto Swift e hai scritto Swift z
Honey il

4
Sembra che "Swiftz" sia una libreria funzionale di terze parti per Swift. Poiché la tua risposta sembra essere basata su quella libreria, dovresti dichiararlo esplicitamente. (es. "Esiste una libreria di terze parti chiamata 'Swiftz' che supporta costrutti funzionali come Futures, e dovrebbe servire come un buon punto di partenza se vuoi implementare le Promesse.") Altrimenti i tuoi lettori si chiederanno perché hai scritto male " Swift ".
Duncan C

3
Tieni presente che github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift non funziona più.
Ahmad F

1
@Rob Il getprefisso indica il ritorno per riferimento in ObjC (come in -[UIColor getRed:green:blue:alpha:]). Quando ho scritto questo, ero preoccupato che gli importatori avrebbero sfruttato questo fatto (per restituire automaticamente una tupla per esempio). Si è scoperto che non l'hanno fatto. Quando ho scritto questo, probabilmente avevo anche dimenticato che KVC supporta i prefissi "get" per le funzioni di accesso (è qualcosa che ho imparato e dimenticato diverse volte). Così d'accordo; Non ho riscontrato casi in cui il leader getrompe le cose. È solo fuorviante per coloro che conoscono il significato di ObjC "ottenere".
Rob Napier

9

Lo schema di base consiste nell'usare la chiusura dei gestori di completamento.

Ad esempio, nel prossimo Swift 5, useresti 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()
}

E lo chiameresti così:

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

Nota, sopra sto rimandando il gestore di completamento alla coda principale per semplificare gli aggiornamenti del modello e dell'interfaccia utente. Alcuni sviluppatori fanno eccezione a questa pratica e utilizzano qualsiasi coda URLSessionutilizzata o utilizzano la propria coda (richiedendo al chiamante di sincronizzare manualmente i risultati).

Ma non è materiale qui. Il problema chiave è l'uso del gestore di completamento per specificare il blocco di codice da eseguire quando viene eseguita la richiesta asincrona.


Il vecchio modello 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()
}

E lo chiameresti così:

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

Nota, sopra ho ritirato l'uso di NSArray(non usiamo più quei tipi Objective-C con bridge ). Presumo che avessimo un Genretipo e presumibilmente lo abbiamo usato JSONDecoder, piuttosto che JSONSerialization, per decodificarlo. Ma questa domanda non disponeva di informazioni sufficienti sul JSON sottostante per entrare nei dettagli qui, quindi l'ho omesso per evitare di offuscare il problema principale, l'uso di chiusure come gestori di completamento.


Puoi usarlo anche Resultin Swift 4 e versioni precedenti, ma devi dichiarare l'enumerazione tu stesso. Uso questo tipo di pattern da anni.
vadian

Sì, certo, come me. Ma sembra che sia stato accolto favorevolmente da Apple con l'uscita di Swift 5. Sono solo in ritardo per la festa.
Rob il

7

Swift 4.0

Per la richiesta-risposta asincrona è possibile utilizzare il gestore di completamento. Vedi sotto ho modificato la soluzione con il paradigma della maniglia di completamento.

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

Puoi chiamare questa funzione come di seguito:

getGenres { (array) in
    // Do operation with array
}

2

Versione Swift 3 della risposta di @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()
}

2

Spero che tu non sia ancora bloccato su questo, ma la risposta breve è che non puoi farlo in Swift.

Un approccio alternativo consiste nel restituire una richiamata che fornirà i dati necessari non appena saranno pronti.


1
Può anche fare promesse in fretta. Ma l'attuale aproceh consigliato da Apple è quello di utilizzare callbackcon closures come hai indicato o da utilizzare delegationcome le API del cacao più vecchie
Mojtaba Hosseini

Hai ragione su Promesse. Ma Swift non fornisce un'API nativa per questo, quindi deve utilizzare PromiseKit o un'altra alternativa.
LironXYZ

1

Ci sono 3 modi per creare funzioni di richiamata e cioè: 1. Gestore di completamento 2. Notifica 3. Delegati

Gestore di completamento All'interno del set di blocchi viene eseguito e restituito quando la sorgente è disponibile, il gestore attenderà fino all'arrivo della risposta in modo che l'interfaccia utente possa essere aggiornata dopo.

Notifica Mazzo di informazioni viene attivato su tutta l'app, Listner può recuperare e utilizzare tali informazioni. Modo asincrono per ottenere informazioni attraverso il progetto.

Delegati L' insieme di metodi verrà attivato quando viene chiamato il delegato, l'origine deve essere fornita tramite i metodi stessi


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

-1

Esistono principalmente 3 modi per ottenere la richiamata in modo rapido

  1. Gestore di chiusure / completamento

  2. Delegati

  3. Notifiche

Gli osservatori possono essere utilizzati anche per ricevere una notifica una volta completata l'attività asincrona.


-2

Ci sono alcuni requisiti molto generici che vorrebbero soddisfare ogni buon API Manager: implementerà un client API orientato al protocollo.

Interfaccia iniziale di APIClient

protocol APIClient {
   func send(_ request: APIRequest,
              completion: @escaping (APIResponse?, Error?) -> Void) 
}

protocol APIRequest: Encodable {
    var resourceName: String { get }
}

protocol APIResponse: Decodable {
}

Ora controlla la struttura completa dell'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)
        }
    }

}

-2

Questo è un piccolo caso d'uso che potrebbe essere 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()
    }

Durante la chiamata alla funzione: -

testUrlSession(urlStr: "YOUR-URL") { (value) in
            print("Your string value ::- \(value)")
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.