Rückgabe von Daten aus einem asynchronen Aufruf in der Swift-Funktion


92

Ich habe in meinem Swift-Projekt eine Dienstprogrammklasse erstellt, die alle REST-Anforderungen und -Antworten verarbeitet. Ich habe eine einfache REST-API erstellt, damit ich meinen Code testen kann. Ich habe eine Klassenmethode erstellt, die ein NSArray zurückgeben muss. Da der API-Aufruf jedoch asynchron ist, muss ich von der Methode innerhalb des asynchronen Aufrufs zurückkehren. Das Problem ist, dass die asynchrone Rückgabe ungültig ist. Wenn ich dies in Node tun würde, würde ich JS-Versprechen verwenden, aber ich kann keine Lösung finden, die in Swift funktioniert.

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

3
Dieser Fehler ist bei Stack Overflow so häufig, dass ich eine Reihe von Blog-Posts geschrieben habe, um damit umzugehen, beginnend mit programingsios.net/what-asynchronous-means
matt

Antworten:


94

Sie können einen Rückruf weiterleiten und einen Rückruf innerhalb eines asynchronen Anrufs anrufen

etwas wie:

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

und rufen Sie dann diese Methode auf:

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

Dank dafür. Meine letzte Frage ist, wie ich diese Klassenmethode von meinem View Controller aus aufrufe. Der Code ist derzeit wie folgt: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 bietet bereits Future an, den Grundbaustein eines Versprechens. Eine Zukunft ist ein Versprechen, das nicht scheitern kann (alle Begriffe hier basieren auf der Scala-Interpretation, bei der ein Versprechen eine Monade ist ).

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

Hoffentlich wird es irgendwann zu einem vollständigen Versprechen im Scala-Stil (ich kann es irgendwann selbst schreiben; ich bin sicher, andere PRs wären willkommen; es ist nicht so schwierig, wenn Future bereits vorhanden ist).

In Ihrem speziellen Fall würde ich wahrscheinlich eine erstellen Result<[Book]>(basierend auf Alexandros Salazars Version vonResult ). Dann wäre Ihre Methodensignatur:

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

Anmerkungen

  • Ich empfehle nicht, Funktionen getin Swift voranzustellen. Es wird bestimmte Arten der Interoperabilität mit ObjC unterbrechen.
  • Ich empfehle, bis zu einem BookObjekt zu analysieren, bevor Sie Ihre Ergebnisse als zurückgeben Future. Es gibt verschiedene Möglichkeiten, wie dieses System ausfallen kann, und es ist viel bequemer, wenn Sie nach all diesen Dingen suchen, bevor Sie sie in ein System einpacken Future. Anreise nach [Book]ist viel besser für den Rest Ihres Swift Code als Gabe um ein NSArray.

4
Swiftz unterstützt nicht mehr Future. Aber schauen Sie sich github.com/mxcl/PromiseKit an, es funktioniert großartig mit Swiftz!
Badeleux

Ich habe ein paar Sekunden
Honey

4
Es klingt so, als ob "Swiftz" eine Funktionsbibliothek von Drittanbietern für Swift ist. Da Ihre Antwort auf dieser Bibliothek zu basieren scheint, sollten Sie dies explizit angeben. (zB "Es gibt eine Drittanbieter-Bibliothek namens 'Swiftz', die funktionale Konstrukte wie Futures unterstützt und als guter Ausgangspunkt für die Implementierung von Versprechungen dienen sollte.") Andernfalls werden sich Ihre Leser nur fragen, warum Sie falsch geschrieben haben. " Schnell".
Duncan C

3
Bitte beachten Sie, dass github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift nicht mehr funktioniert.
Ahmad F

1
@Rob Das getPräfix gibt die Referenzrückgabe in ObjC an (z. B. in -[UIColor getRed:green:blue:alpha:]). Als ich dies schrieb, war ich besorgt, dass die Importeure diese Tatsache nutzen würden (um beispielsweise ein Tupel automatisch zurückzugeben). Es hat sich herausgestellt, dass sie nicht haben. Als ich dies schrieb, hatte ich wahrscheinlich auch vergessen, dass KVC "get" -Präfixe für Accessoren unterstützt (das habe ich mehrmals gelernt und vergessen). So vereinbart; Ich bin nicht auf Fälle gestoßen, in denen die Führung getDinge kaputt macht. Es ist nur irreführend für diejenigen, die die Bedeutung von ObjC "bekommen" kennen.
Rob Napier

8

Das Grundmuster besteht darin, den Abschluss der Vervollständigungshandler zu verwenden.

Im kommenden Swift 5 würden Sie beispielsweise Folgendes verwenden 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()
}

Und du würdest es so nennen:

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

Beachten Sie, dass ich oben den Completion-Handler zurück in die Hauptwarteschlange sende, um Modell- und UI-Updates zu vereinfachen. Einige Entwickler nehmen eine Ausnahme von dieser Vorgehensweise und verwenden entweder die verwendete Warteschlange URLSessionoder ihre eigene Warteschlange (der Aufrufer muss die Ergebnisse manuell selbst synchronisieren).

Aber das ist hier nicht wesentlich. Das Hauptproblem ist die Verwendung des Completion-Handlers, um den Codeblock anzugeben, der ausgeführt werden soll, wenn die asynchrone Anforderung ausgeführt wird.


Das ältere Swift 4-Muster lautet:

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

Und du würdest es so nennen:

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

Beachten Sie, dass ich oben die Verwendung von eingestellt habe NSArray(wir verwenden diese überbrückten Objective-C-Typen nicht mehr). Ich gehe davon aus, dass wir einen GenreTyp hatten und ihn vermutlich JSONDecodereher JSONSerializationzum Dekodieren als zum Dekodieren verwendet haben. Diese Frage enthielt jedoch nicht genügend Informationen über den zugrunde liegenden JSON, um hier auf die Details einzugehen. Daher habe ich darauf verzichtet, die Verwendung von Verschlüssen als Abschlusshandler zu verwenden, um eine Trübung des Kernproblems zu vermeiden.


Sie können auch Resultin Swift 4 und niedriger verwenden, müssen die Aufzählung jedoch selbst deklarieren. Ich benutze diese Art von Muster seit Jahren.
Vadian

Ja, natürlich genauso wie ich. Aber es sieht nur so aus, als ob Apple es mit der Veröffentlichung von Swift 5 angenommen hat. Sie kommen gerade zu spät zur Party.
Rob

7

Swift 4.0

Für asynchrone Request-Response können Sie den Completion-Handler verwenden. Siehe unten Ich habe die Lösung mit dem Vervollständigungshandle-Paradigma modifiziert.

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

Sie können diese Funktion wie folgt aufrufen:

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

2

Swift 3-Version von @Alexey Globchastyys Antwort:

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

Ich hoffe, Sie bleiben noch nicht dabei, aber die kurze Antwort lautet, dass Sie dies in Swift nicht tun können.

Ein alternativer Ansatz wäre, einen Rückruf zurückzugeben, der die benötigten Daten bereitstellt, sobald sie bereit sind.


1
Er kann auch schnell Versprechen machen. Aber Apples derzeit empfohlenes Aproceh verwendet callbackmit closures, wie Sie hervorheben , oder delegationwie die älteren Kakao-APIs
Mojtaba Hosseini

Sie haben Recht mit Versprechen. Da Swift jedoch keine native API dafür bereitstellt, muss er PromiseKit oder eine andere Alternative verwenden.
LironXYZ

1

Es gibt drei Möglichkeiten, Rückruffunktionen zu erstellen: 1. Abschlusshandler 2. Benachrichtigung 3. Delegierte

Completion Handler Innerhalb des Blocksatzes wird ausgeführt und zurückgegeben, wenn die Quelle verfügbar ist. Der Handler wartet, bis eine Antwort eingeht, damit die Benutzeroberfläche anschließend aktualisiert werden kann.

Benachrichtigung Eine Reihe von Informationen wird über die gesamte App ausgelöst. Listner kann diese Informationen abrufen und nutzen. Asynchrone Methode, um Informationen durch das Projekt zu erhalten.

Delegaten Eine Reihe von Methoden wird ausgelöst, wenn der Delegat aufgerufen wird. Die Quelle muss über die Methoden selbst bereitgestellt werden


-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

Es gibt hauptsächlich drei Möglichkeiten, um schnell einen Rückruf zu erzielen

  1. Closures / Completion Handler

  2. Delegierte

  3. Benachrichtigungen

Beobachter können auch verwendet werden, um benachrichtigt zu werden, sobald die asynchrone Aufgabe abgeschlossen ist.


-2

Es gibt einige sehr allgemeine Anforderungen, die jeder gute API-Manager erfüllen soll: Er implementiert einen protokollorientierten API-Client.

APIClient Initial Interface

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

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

protocol APIResponse: Decodable {
}

Jetzt überprüfen Sie bitte die vollständige API-Struktur

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

Dies ist ein kleiner Anwendungsfall, der hilfreich sein könnte:

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

Beim Aufruf der Funktion: -

testUrlSession(urlStr: "YOUR-URL") { (value) in
            print("Your string value ::- \(value)")
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.