Zwracanie danych z wywołania asynchronicznego w funkcji Swift

93

W moim projekcie Swift utworzyłem klasę narzędziową, która obsługuje wszystkie żądania i odpowiedzi REST. Zbudowałem prosty interfejs API REST, dzięki czemu mogę przetestować swój kod. Utworzyłem metodę klasy, która musi zwrócić NSArray, ale ponieważ wywołanie API jest asynchroniczne, muszę wrócić z metody wewnątrz wywołania asynchronicznego. Problem polega na tym, że asynchroniczne zwraca void. Gdybym robił to w Node, użyłbym obietnic JS, ale nie mogę znaleźć rozwiązania, które działa w 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
źródło
5
Ten błąd jest tak powszechny w Stack Overflow, że napisałem serię postów na blogu, aby sobie z nim poradzić, zaczynając odmingios.net/what-asynchronous-means
Matt

Odpowiedzi:

97

Możesz przekazać wywołanie zwrotne i oddzwonienie w ramach połączenia asynchronicznego

coś jak:

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

a następnie wywołaj tę metodę:

override func viewDidLoad() {
    Bookshop.getGenres {
        genres in
        println("View Controller: \(genres)")     
    }
}
Alexey Globchastyy
źródło
Dziękuję za to. Moje ostatnie pytanie brzmi: jak wywołać tę metodę klasy z mojego kontrolera widoku. Obecnie kod wygląda następująco: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 oferuje już przyszłość, która jest podstawowym elementem składowym obietnicy. Przyszłość to obietnica, która nie może zawieść (wszystkie podane tutaj terminy są oparte na interpretacji Scali, gdzie obietnica to monada ).

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

Mam nadzieję, że w końcu rozwinie się do pełnej obietnicy w stylu Scala (może kiedyś napiszę ją sam; jestem pewien, że inne PR byłyby mile widziane; nie jest to takie trudne, gdy Future już jest na miejscu).

W twoim konkretnym przypadku prawdopodobnie stworzyłbym plik Result<[Book]>(oparty na wersji Alexandrosa SalazaraResult ). Wtedy podpis Twojej metody wyglądałby tak:

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

Uwagi

  • Nie polecam poprzedzania funkcji getw języku Swift. Zepsuje pewne rodzaje interoperacyjności z ObjC.
  • Zalecam przeanalizowanie całej drogi do Bookobiektu przed zwróceniem wyników jako pliku Future. Ten system może zawieść na kilka sposobów i znacznie wygodniej jest sprawdzić wszystkie te elementy przed zapakowaniem ich do pliku Future. Dotarcie do [Book]jest znacznie lepsze dla pozostałej części kodu Swift niż przekazanie pliku NSArray.
Rob Napier
źródło
4
Swiftz już nie obsługuje Future. Ale spójrz na github.com/mxcl/PromiseKit , działa świetnie ze Swiftz!
badeleux
zajęło mi kilka sekund, zanim zdałem sobie sprawę, że nie napisałeś Swift i napisałeś Swift z
Honey
4
Wygląda na to, że „Swiftz” jest biblioteką funkcjonalną innej firmy dla języka Swift. Ponieważ twoja odpowiedź wydaje się opierać na tej bibliotece, powinieneś to wyraźnie zaznaczyć. (np. „Istnieje biblioteka strony trzeciej o nazwie„ Swiftz ”, która obsługuje konstrukcje funkcjonalne, takie jak Futures, i powinna służyć jako dobry punkt wyjścia, jeśli chcesz wdrożyć obietnice.”) W przeciwnym razie czytelnicy będą się po prostu zastanawiać, dlaczego błędnie napisałeś ” Szybki".
Duncan C,
3
Pamiętaj, że github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift już nie działa.
Ahmad F
1
@Rob getPrefiks wskazuje powrót przez odniesienie w ObjC (na przykład w -[UIColor getRed:green:blue:alpha:]). Kiedy to pisałem, obawiałem się, że importerzy wykorzystają ten fakt (na przykład, aby automatycznie zwrócić krotkę). Okazało się, że nie. Kiedy to pisałem, prawdopodobnie zapomniałem również, że KVC obsługuje prefiksy „pobierz” dla akcesorów (jest to coś, czego nauczyłem się i zapomniałem kilka razy). Tak się zgodził; Nie spotkałem żadnych przypadków, w których prowadzący getwszystko psuje. To po prostu mylące dla tych, którzy znają znaczenie ObjC „dostać”.
Rob Napier,
9

Podstawowym wzorcem jest użycie zamknięcia obsługi zakończenia.

Na przykład w nadchodzącej wersji Swift 5 użyjesz 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()
}

I tak to nazwałbyś:

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

Uwaga, powyżej wysyłam procedurę obsługi zakończenia z powrotem do głównej kolejki, aby uprościć aktualizacje modelu i interfejsu użytkownika. Niektórzy programiści robią wyjątek od tej praktyki i używają dowolnej URLSessionużywanej kolejki lub używają własnej kolejki (wymagając od wywołującego ręcznej synchronizacji wyników).

Ale to nie jest tutaj istotne. Kluczową kwestią jest użycie procedury obsługi zakończenia do określenia bloku kodu, który ma zostać uruchomiony po wykonaniu żądania asynchronicznego.


Starszy wzór Swift 4 to:

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

I tak to nazwałbyś:

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

Uwaga, powyżej zrezygnowałem z używania NSArray(nie używamy już tych połączonych typów Objective-C ). Zakładam, że mieliśmy Genretyp i prawdopodobnie użyliśmy go JSONDecoderraczej niż JSONSerializationdo dekodowania. Ale to pytanie nie zawierało wystarczających informacji o bazowym formacie JSON, aby uzyskać szczegółowe informacje, więc pominąłem je, aby uniknąć zaciemnienia podstawowego problemu, czyli użycia zamknięć jako programów obsługi zakończenia.

Obrabować
źródło
Możesz używać Resultw Swift 4 i niższych, ale musisz sam zadeklarować wyliczenie. Używam tego wzoru od lat.
vadian
Tak, oczywiście, tak jak ja. Ale wygląda na to, że Apple przyjął go tylko wraz z wydaniem Swift 5. Po prostu spóźnili się na imprezę.
Rob
7

Swift 4.0

W przypadku asynchronicznej odpowiedzi żądanie-odpowiedź można użyć modułu obsługi zakończenia. Zobacz poniżej Zmodyfikowałem rozwiązanie z paradygmatem uchwytu uzupełniania.

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

Możesz wywołać tę funkcję jak poniżej:

getGenres { (array) in
    // Do operation with array
}
Jaydeep Vora
źródło
2

Wersja Swift 3 odpowiedzi @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
źródło
2

Mam nadzieję, że nadal nie jesteś na tym miejscu, ale krótka odpowiedź jest taka, że ​​nie możesz tego zrobić w Swift.

Alternatywnym podejściem byłoby zwrócenie wywołania zwrotnego, które dostarczy potrzebne dane, gdy tylko będą gotowe.

LironXYZ
źródło
1
Potrafi też szybko składać obietnice. Ale obecnie zalecany aproceh firmy Apple używa callbackz closures, jak wskazałeś , lub do używania delegationjak starsze API dla kakao
Mojtaba Hosseini
Masz rację co do obietnic. Ale Swift nie zapewnia do tego natywnego interfejsu API, więc musi użyć PromiseKit lub innej alternatywy.
LironXYZ
1

Istnieją 3 sposoby tworzenia funkcji oddzwaniania, a mianowicie: 1. Osoba obsługująca zakończenie 2. Powiadomienie 3. Delegaci

Procedura obsługi zakończenia Wewnątrz zestawu bloków jest wykonywany i zwracany, gdy źródło jest dostępne, obsługa czeka na odpowiedź, aby można było zaktualizować interfejs użytkownika.

Powiadomienie Zbiór informacji jest uruchamiany w całej aplikacji, Listner może pobrać n przy użyciu tych informacji. Asynchroniczny sposób uzyskiwania informacji w ramach projektu.

Delegaci Zestaw metod zostanie wyzwolony po wywołaniu delegata, źródło musi być dostarczone za pośrednictwem samych metod

IRANNA SALUNKE
źródło
-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
źródło
-1

Istnieją głównie 3 sposoby szybkiego oddzwonienia

  1. Zamknięcia / obsługa zakończenia

  2. Delegaci

  3. Powiadomienia

Obserwatorzy mogą również służyć do otrzymywania powiadomień po zakończeniu zadania asynchronicznego.

Shubham Mishra
źródło
-2

Jest kilka bardzo ogólnych wymagań, które chciałby spełnić każdy dobry menedżer API: zaimplementuje klienta API zorientowanego na protokół.

Interfejs początkowy APIClient

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

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

protocol APIResponse: Decodable {
}

Teraz sprawdź pełną strukturę 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
źródło
-2

To jest mały przypadek użycia, który może być pomocny: -

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

Podczas wywoływania funkcji: -

testUrlSession(urlStr: "YOUR-URL") { (value) in
            print("Your string value ::- \(value)")
}
a.palo
źródło