Retornando dados de chamada assíncrona na função Swift

93

Eu criei uma classe de utilitário em meu projeto Swift que lida com todas as solicitações e respostas REST. Eu construí uma API REST simples para poder testar meu código. Eu criei um método de classe que precisa retornar um NSArray, mas como a chamada da API é assíncrona, preciso retornar do método dentro da chamada assíncrona. O problema é que o async retorna void. Se eu estivesse fazendo isso no Node, usaria promessas de JS, mas não consigo descobrir uma solução que funcione no 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
fonte
5
Esse erro é tão comum no Stack Overflow que escrevi uma série de postagens de blog para lidar com ele, começando com programmingios.net/what-asynchronous-means
matt

Respostas:

96

Você pode passar callback e callback dentro da chamada assí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()
}

e chame este método:

override func viewDidLoad() {
    Bookshop.getGenres {
        genres in
        println("View Controller: \(genres)")     
    }
}
Alexey Globchastyy
fonte
Obrigado por isso. Minha pergunta final é como chamo esse método de classe do meu controlador de visualização. O código é atualmente assim: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 já oferece Futuro, que é o bloco de construção básico de uma promessa. Um futuro é uma promessa que não pode falhar (todos os termos aqui são baseados na interpretação de Scala, onde uma promessa é uma mônada ).

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

Espero que eventualmente se expanda para uma Promessa completa no estilo Scala (posso escrever sozinho em algum momento; tenho certeza de que outros PRs seriam bem-vindos; não é tão difícil com o Futuro já implementado).

No seu caso particular, eu provavelmente criaria um Result<[Book]>(baseado na versão de Alexandros Salazar deResult ). Então, a assinatura do seu método seria:

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

Notas

  • Eu não recomendo funções de prefixo com getem Swift. Isso quebrará certos tipos de interoperabilidade com ObjC.
  • Eu recomendo analisar todo o caminho até um Bookobjeto antes de retornar seus resultados como um Future. Esse sistema pode falhar de várias maneiras, e é muito mais conveniente verificar todas essas coisas antes de agrupá-las em um Future. Chegar [Book]é muito melhor para o resto do código Swift do que entregar um NSArray.
Rob Napier
fonte
4
Swiftz não oferece mais suporte Future. Mas dê uma olhada em github.com/mxcl/PromiseKit , funciona muito bem com o Swiftz!
badeleux
Levei alguns segundos para perceber que você não escreveu Swift e escreveu Swift z
Honey
4
Parece que "Swiftz" é uma biblioteca funcional de terceiros para Swift. Visto que sua resposta parece estar baseada nessa biblioteca, você deve declarar isso explicitamente. (por exemplo, "Existe uma biblioteca de terceiros chamada 'Swiftz' que oferece suporte a construções funcionais como Futures e deve servir como um bom ponto de partida se você deseja implementar Promises.") Caso contrário, seus leitores ficarão se perguntando por que você digitou incorretamente " Rápido".
Duncan C
3
Observe que github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift não está mais funcionando.
Ahmad F
1
@Rob O getprefixo indica retorno por referência em ObjC (como em -[UIColor getRed:green:blue:alpha:]). Quando escrevi isso, fiquei preocupado que os importadores alavancassem esse fato (para retornar uma tupla automaticamente, por exemplo). Acontece que não. Quando escrevi isso, provavelmente também havia esquecido que KVC suporta prefixos "get" para acessadores (é algo que aprendi e esqueci várias vezes). Então concordou; Não encontrei nenhum caso em que o líder getquebra as coisas. É apenas enganoso para aqueles que sabem o significado de ObjC "get".
Rob Napier
9

O padrão básico é usar o fechamento de manipuladores de conclusão.

Por exemplo, no próximo Swift 5, você usaria 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 você o chamaria assim:

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

Observe, acima, estou despachando o manipulador de conclusão de volta para a fila principal para simplificar o modelo e as atualizações da IU. Alguns desenvolvedores se opõem a esta prática e usam qualquer fila URLSessionusada ou usam sua própria fila (exigindo que o autor da chamada sincronize manualmente os resultados).

Mas isso não é relevante aqui. O principal problema é o uso do manipulador de conclusão para especificar o bloco de código a ser executado quando a solicitação assíncrona for feita.


O mais antigo, o padrão 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 você o chamaria assim:

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

Observe, acima, retirei o uso de NSArray(não usamos mais esses tipos de Objective-C em ponte ). Suponho que tínhamos um Genretipo e provavelmente o usamos JSONDecoder, em vez de JSONSerialization, para decodificá-lo. Mas essa pergunta não tinha informações suficientes sobre o JSON subjacente para entrar em detalhes aqui, então omiti isso para evitar obscurecer o problema principal, o uso de encerramentos como manipuladores de conclusão.

Roubar
fonte
Você também pode usar Resultno Swift 4 e inferior, mas você mesmo deve declarar o enum. Estou usando esse tipo de padrão há anos.
vadian
Sim, claro, assim como eu. Mas parece que só foi abraçado pela Apple com o lançamento do Swift 5. Eles estão atrasados ​​para a festa.
Rob de
7

Swift 4.0

Para Solicitação-Resposta assíncrona, você pode usar o manipulador de conclusão. Veja abaixo, eu modifiquei a solução com paradigma de manipulação de conclusão.

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

Você pode chamar esta função conforme abaixo:

getGenres { (array) in
    // Do operation with array
}
Jaydeep Vora
fonte
2

Versão Swift 3 da resposta 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
fonte
2

Espero que você não esteja ainda preso nisso, mas a resposta curta é que você não pode fazer isso no Swift.

Uma abordagem alternativa seria retornar um retorno de chamada que fornecerá os dados de que você precisa assim que estiverem prontos.

LironXYZ
fonte
1
Ele também pode fazer promessas rapidamente. Mas o aproceh recomendado atualmente pela apple está usando callbackcom closures como você indicou ou delegationcomo as APIs de cacau mais antigas
Mojtaba Hosseini
Você está certo sobre as promessas. Mas Swift não fornece uma API nativa para isso, então ele precisa usar o PromiseKit ou outra alternativa.
LironXYZ
1

Existem 3 maneiras de criar funções de retorno de chamada, a saber: 1. Manipulador de conclusão 2. Notificação 3. Delegados

Completion Handler Dentro do conjunto do bloco é executado e retornado quando a fonte estiver disponível, Handler irá esperar até que a resposta chegue para que a IU possa ser atualizada depois.

Notificação Um monte de informações é acionado em todo o aplicativo, o Listner pode recuperar e fazer uso dessas informações. Maneira assíncrona de obter informações ao longo do projeto.

Delegados O conjunto de métodos será acionado quando o delegado for chamado, a fonte deve ser fornecida pelos próprios métodos

IRANNA SALUNKE
fonte
-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
fonte
-1

Existem basicamente 3 maneiras de obter retorno de chamada rapidamente

  1. Manipulador de fechamentos / conclusão

  2. Delegados

  3. Notificações

Os observadores também podem ser usados ​​para serem notificados quando a tarefa assíncrona for concluída.

Shubham Mishra
fonte
-2

Existem alguns requisitos muito genéricos que gostariam que todo bom API Manager satisfizesse: implementará um API Client orientado a protocolo.

Interface inicial APIClient

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

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

protocol APIResponse: Decodable {
}

Agora, verifique a estrutura API completa

// ******* 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
fonte
-2

Este é um pequeno caso de uso que pode 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()
    }

Ao chamar a função: -

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