Devolver datos de una llamada asíncrona en la función Swift


93

He creado una clase de utilidad en mi proyecto Swift que maneja todas las solicitudes y respuestas REST. He creado una API REST simple para poder probar mi código. He creado un método de clase que debe devolver un NSArray, pero debido a que la llamada a la API es asíncrona, necesito regresar desde el método dentro de la llamada asíncrona. El problema es que el async devuelve vacío. Si estuviera haciendo esto en Node, usaría las promesas de JS pero no puedo encontrar una solución que funcione en 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
Este error es tan común en Stack Overflow que escribí una serie de publicaciones en el blog para tratar con él, comenzando con Programmingios.net/what-asynchronous-means
Matt

Respuestas:


96

Puede pasar la devolución de llamada y la devolución de llamada dentro de la llamada asíncrona

algo como:

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

y luego llame a este método:

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

Gracias por eso. Mi pregunta final es cómo llamo a este método de clase desde mi controlador de vista. El código es actualmente así: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 ya ofrece Future, que es el componente básico de una Promise. Un futuro es una promesa que no puede fallar (todos los términos aquí se basan en la interpretación de Scala, donde una promesa es una mónada ).

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

Con suerte, eventualmente se expandirá a una Promesa completa al estilo Scala (puedo escribirla yo mismo en algún momento; estoy seguro de que otros RP serían bienvenidos; no es tan difícil con Future ya implementado).

En su caso particular, probablemente crearía un Result<[Book]>(basado en la versión de Alexandros Salazar deResult ). Entonces la firma de su método sería:

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

Notas

  • No recomiendo prefijar funciones geten Swift. Romperá ciertos tipos de interoperabilidad con ObjC.
  • Recomiendo analizar todo el camino hasta un Bookobjeto antes de devolver los resultados como Future. Hay varias formas en que este sistema puede fallar, y es mucho más conveniente si verifica todas esas cosas antes de envolverlas en un archivo Future. Llegar [Book]es mucho mejor para el resto de su código Swift que entregar un NSArray.

4
Swiftz ya no es compatible Future. Pero eche un vistazo a github.com/mxcl/PromiseKit , ¡funciona muy bien con Swiftz!
badeleux

Me tomó unos segundos darme cuenta de que no escribiste Swift y no escribiste Swift z
Cariño

4
Parece que "Swiftz" es una biblioteca funcional de terceros para Swift. Dado que su respuesta parece estar basada en esa biblioteca, debe indicarlo explícitamente. (p. ej., "Existe una biblioteca de terceros llamada 'Swiftz' que admite construcciones funcionales como Futures y debería servir como un buen punto de partida si desea implementar Promises"). De lo contrario, sus lectores se preguntarán por qué escribió mal ". Rápido".
Duncan C


1
@Rob El getprefijo indica retorno por referencia en ObjC (como en -[UIColor getRed:green:blue:alpha:]). Cuando escribí esto, me preocupaba que los importadores aprovecharan ese hecho (para devolver una tupla automáticamente, por ejemplo). Resultó que no lo han hecho. Cuando escribí esto, probablemente también había olvidado que KVC admite prefijos "get" para los accesos (es algo que he aprendido y olvidado varias veces). Así acordado; No me he encontrado con ningún caso en el que el líder getrompa cosas. Es engañoso para aquellos que conocen el significado de ObjC "get".
Rob Napier

9

El patrón básico es utilizar el cierre de controladores de finalización.

Por ejemplo, en el próximo Swift 5, usaría 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()
}

Y lo llamarías así:

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

Tenga en cuenta que, arriba, estoy enviando el controlador de finalización a la cola principal para simplificar las actualizaciones del modelo y la interfaz de usuario. Algunos desarrolladores hacen una excepción a esta práctica y usan cualquier colaURLSession utilizada o usan su propia cola (requiriendo que la persona que llama sincronice manualmente los resultados).

Pero eso no es material aquí. El problema clave es el uso del controlador de finalización para especificar el bloque de código que se ejecutará cuando se realice la solicitud asincrónica.


El patrón más antiguo de Swift 4 es:

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

Y lo llamarías así:

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

Tenga en cuenta que, anteriormente, retiré el uso de NSArray(ya no usamos esos tipos de Objective-C puenteados ). Supongo que teníamos un Genretipo y presumiblemente lo usamos JSONDecoder, en lugar de JSONSerializationdecodificarlo. Pero esta pregunta no tenía suficiente información sobre el JSON subyacente para entrar en detalles aquí, así que omití eso para evitar nublar el problema central, el uso de cierres como controladores de finalización.


También puede usar Resulten Swift 4 y versiones inferiores, pero debe declarar la enumeración usted mismo. Estoy usando este tipo de patrón durante años.
vadian

Sí, por supuesto, al igual que yo. Pero parece que Apple lo adoptó con el lanzamiento de Swift 5. Llegan tarde a la fiesta.
Rob

7

Swift 4.0

Para solicitud-respuesta asíncrona, puede usar el controlador de finalización. Vea a continuación. He modificado la solución con el paradigma de control de finalización.

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

Puede llamar a esta función de la siguiente manera:

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

2

Versión Swift 3 de la respuesta 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()
}

2

Espero que no te quedes atascado en esto, pero la respuesta corta es que no puedes hacer esto en Swift.

Un enfoque alternativo sería devolver una devolución de llamada que proporcionará los datos que necesita tan pronto como esté listo.


1
Él también puede hacer promesas rápidamente. Pero el procedimiento recomendado actual de Apple se usa callbackcon closures como usted señala o para usar delegationcomo las API de cacao más antiguas
Mojtaba Hosseini

Tienes razón sobre Promises. Pero Swift no proporciona una API nativa para esto, por lo que debe usar PromiseKit u otra alternativa.
LironXYZ

1

Hay 3 formas de crear funciones de devolución de llamada, a saber: 1. Controlador de finalización 2. Notificación 3. Delegados

Controlador de finalización El conjunto interior del bloque se ejecuta y se devuelve cuando la fuente está disponible, el controlador esperará hasta que llegue la respuesta para que la interfaz de usuario se pueda actualizar después.

Notificación Un montón de información se activa en toda la aplicación, Listner puede recuperar y hacer uso de esa información. Manera asincrónica de obtener información a través del proyecto.

Delegados El conjunto de métodos se activará cuando se llame al delegado, la fuente debe proporcionarse a través de los propios métodos


-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

Hay principalmente 3 formas de lograr la devolución de llamada de forma rápida

  1. Controlador de cierres / finalización

  2. Delegados

  3. Notificaciones

Los observadores también se pueden usar para recibir notificaciones una vez que se haya completado la tarea asíncrona.


-2

Hay algunos requisitos muy genéricos que le gustaría que cumpliera todo buen administrador de API: implementará un cliente API orientado a protocolos.

Interfaz inicial de APIClient

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

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

protocol APIResponse: Decodable {
}

Ahora compruebe la estructura completa de la 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

Este es un pequeño caso de uso que podría ser útil: -

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

Mientras llama a la función: -

testUrlSession(urlStr: "YOUR-URL") { (value) in
            print("Your string value ::- \(value)")
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.