Swift işlevinde eşzamansız aramadan veri döndürme


93

Swift projemde tüm REST isteklerini ve yanıtlarını işleyen bir yardımcı program sınıfı oluşturdum. Kodumu test edebilmek için basit bir REST API oluşturdum. Bir NSArray döndürmesi gereken bir sınıf yöntemi oluşturdum, ancak API çağrısı zaman uyumsuz olduğundan, zaman uyumsuz çağrının içindeki yöntemden dönmem gerekiyor. Sorun, zaman uyumsuzun void döndürmesidir. Bunu Node'da yapıyor olsaydım JS vaatlerini kullanırdım ama Swift'de çalışan bir çözüm bulamıyorum.

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
Stack Overflow'da bu hata o kadar yaygındır ki, programlamaios.net/what-asynchronous-means
matt

Yanıtlar:


97

Geri aramayı geçebilir ve eşzamansız arama içinde geri aramayı geri arayabilirsiniz

gibi bir şey:

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

ve sonra bu yöntemi çağırın:

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

Bunun için teşekkürler. Son sorum, bu sınıf yöntemini görünüm denetleyicimden nasıl çağıracağım. Kod şu anda şöyle: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, bir Sözün temel yapı taşı olan Geleceği zaten sunuyor. Bir Gelecek, başarısız olamayacak bir Sözdür (buradaki tüm terimler, bir Sözün bir Monad olduğu Scala yorumlamasına dayanmaktadır ).

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

Umarım eninde sonunda Scala tarzı bir Promise'a genişleriz (bir noktada kendim yazabilirim; eminim diğer PR'lar da memnuniyetle karşılanır; Future zaten yerinde olduğunda o kadar da zor değil).

Sizin özel durumunuzda, muhtemelen bir Result<[Book]>( Alexandros Salazar'ın versiyonuna dayanarakResult ) yaratırdım . O zaman yöntem imzanız şöyle olur:

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

Notlar

  • getSwift ile önek fonksiyonlarını önermiyorum . ObjC ile belirli türden birlikte çalışabilirliği bozacaktır.
  • BookSonuçlarınızı. Olarak döndürmeden önce bir nesneye tamamen ayrıştırmanızı tavsiye ederim Future. Bu sistemin başarısız olabileceği birkaç yol vardır ve tüm bunları bir Future. [Book]Swift kodunuzun geri kalanı için ulaşmak , bir NSArray.

4
Swiftz artık desteklemiyor Future. Ancak github.com/mxcl/PromiseKit'e bir göz atın Swiftz ile harika çalışıyor!
badeleux

Swift z
Honey

4
"Swiftz", Swift için üçüncü taraf bir işlevsel kitaplık gibi görünüyor. Cevabınız bu kitaplığa dayanıyor gibi göründüğünden, bunu açıkça belirtmelisiniz. (örneğin "Vadeli İşlemler gibi işlevsel yapıları destekleyen ve Promises'i uygulamak istiyorsanız iyi bir başlangıç ​​noktası olarak hizmet etmelidir 'Swiftz' adında bir üçüncü taraf kitaplığı var.") Aksi takdirde okuyucularınız neden yanlış yazdığınızı merak edeceklerdir. " Swift ".
Duncan C

3
Lütfen github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift'in artık çalışmadığını unutmayın .
Ahmad F

1
@Rob getÖnek, ObjC'de (örneğin, içinde -[UIColor getRed:green:blue:alpha:]) referansla dönüşü belirtir . Bunu yazdığımda, ithalatçıların bu gerçeği kullanacağından endişelendim (örneğin bir demeti otomatik olarak döndürmek için). Yapmadıkları ortaya çıktı. Bunu yazdığımda, muhtemelen KVC'nin erişimciler için "alma" öneklerini desteklediğini de unutmuştum (bu, birkaç kez öğrendiğim ve unuttuğum bir şey). Kabul etti; Önde gelenlerin işleri getbozduğu hiçbir vakayla karşılaşmadım . Bu sadece ObjC "elde etmenin" anlamını bilenler için yanıltıcıdır.
Rob Napier

9

Temel model, tamamlama işleyicileri kapatma kullanmaktır.

Örneğin, yakında çıkacak Swift 5'te şunları kullanacaksınız 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()
}

Ve buna şöyle diyorsun:

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

Yukarıda, model ve kullanıcı arayüzü güncellemelerini basitleştirmek için tamamlama işleyicisini ana kuyruğa geri gönderiyorum. Bazı geliştiriciler bu uygulamayı istisna ederler ve ya kullanılan kuyruğu URLSessionkullanırlar ya da kendi kuyruğunu kullanırlar (arayanın sonuçları kendilerinin manuel olarak senkronize etmesini gerektirir).

Ama burada önemli değil. Temel sorun, eşzamansız istek yapıldığında çalıştırılacak kod bloğunu belirtmek için tamamlama işleyicisinin kullanılmasıdır.


Daha eski olan Swift 4 kalıbı:

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

Ve buna şöyle diyorsun:

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

Not, yukarıda kullanımını emekli ettim NSArray(artık bu köprülü Objective-C türlerini kullanmıyoruz ). Bir tipimiz olduğunu varsayıyorum Genreve muhtemelen kodunu çözmek JSONDecoderyerine kullandık JSONSerialization. Ancak bu soru, buradaki ayrıntılara girmek için temeldeki JSON hakkında yeterli bilgiye sahip değildi, bu yüzden, temel sorunu, tamamlama işleyicileri olarak kapatma kullanımlarını bulanıklaştırmaktan kaçınmak için bunu atladım.


ResultSwift 4 ve daha düşük sürümlerde de kullanabilirsiniz , ancak sıralamayı kendiniz beyan etmeniz gerekir. Bu tür kalıpları yıllardır kullanıyorum.
vadian

Evet, tabii ki benim de öyle. Ama görünen o ki Apple tarafından Swift 5'in piyasaya sürülmesiyle kucaklanmış gibi görünüyor. Partiye henüz geç kaldılar.
Rob

7

Swift 4.0

Zaman uyumsuz İstek-Yanıtı için tamamlama işleyicisini kullanabilirsiniz. Aşağıya bakın Çözümü tamamlama tutacağı paradigması ile değiştirdim.

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

Bu işlevi aşağıdaki gibi çağırabilirsiniz:

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

2

@Alexey Globchastyy'nin cevabının Swift 3 versiyonu:

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

Umarım buna hala takılıp kalmamışsındır, ancak kısa cevap, bunu Swift'de yapamazsın.

Alternatif bir yaklaşım, ihtiyacınız olan verileri hazır olur olmaz sağlayacak bir geri aramayı geri döndürmektir.


1
O da hızlıca sözler verebilir. Ancak, Apple'ın şu anda önerilen yöntemi, belirttiğiniz gibi veya eski kakao API'leri gibi kullanmak callbackiçin closures ile kullanmaktırdelegation
Mojtaba Hosseini

Promises konusunda haklısın. Ancak Swift bunun için yerel bir API sağlamaz, bu nedenle PromiseKit veya başka bir alternatifi kullanması gerekir.
LironXYZ

1

Geri arama işlevi oluşturmanın 3 yolu vardır: 1. Tamamlama işleyicisi 2. Bildirim 3. Temsilciler

Tamamlama İşleyicisi İç blok kümesi yürütülür ve kaynak mevcut olduğunda geri gönderilir, İşleyici yanıt gelene kadar bekleyecektir, böylece UI daha sonra güncellenebilir.

Bildirim Tüm uygulama üzerinde çok sayıda bilgi tetiklenir, Listner bu bilgileri alabilir ve kullanabilir. Proje üzerinden bilgi almanın zaman uyumsuz yolu.

Temsilciler Yöntemler grubu, temsilci çağrıldığında tetiklenecek, Kaynak, yöntemlerin kendisi aracılığıyla sağlanmalıdır


-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

Hızlı bir şekilde geri aramayı başarmanın başlıca 3 yolu vardır

  1. Kapanışlar / Tamamlama işleyicisi

  2. Delegeler

  3. Bildirimler

Gözlemciler, eşzamansız görev tamamlandıktan sonra bildirim almak için de kullanılabilir.


-2

Her iyi API Yöneticisinin karşılamasını isteyen çok genel bazı gereksinimler vardır: protokol odaklı bir API İstemcisi uygulayacaktır .

APIClient İlk Arayüzü

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

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

protocol APIResponse: Decodable {
}

Şimdi lütfen tam api yapısını kontrol edin

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

Bu, yardımcı olabilecek küçük bir kullanım durumudur: -

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

İşlevi çağırırken: -

testUrlSession(urlStr: "YOUR-URL") { (value) in
            print("Your string value ::- \(value)")
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.