Mengembalikan data dari panggilan async dalam fungsi Swift


93

Saya telah membuat kelas utilitas dalam proyek Swift saya yang menangani semua permintaan dan tanggapan REST. Saya telah membangun REST API sederhana sehingga saya dapat menguji kode saya. Saya telah membuat metode kelas yang perlu mengembalikan NSArray tetapi karena panggilan API adalah asinkron, saya harus kembali dari metode di dalam panggilan async. Masalahnya adalah pengembalian asinkron batal. Jika saya melakukan ini di Node, saya akan menggunakan janji JS tetapi saya tidak dapat menemukan solusi yang berfungsi di 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
Kesalahan ini sangat umum terjadi di Stack Overflow sehingga saya telah menulis serangkaian posting blog untuk mengatasinya, dimulai dengan programmingios.net/what-asynchronous-means
matt

Jawaban:


97

Anda dapat meneruskan callback, dan memanggil callback di dalam panggilan async

sesuatu seperti:

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

lalu panggil metode ini:

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

Terima kasih untuk itu. Pertanyaan terakhir saya adalah bagaimana cara memanggil metode kelas ini dari pengontrol tampilan saya. Kode saat ini seperti ini: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 sudah menawarkan Future, yang merupakan elemen dasar dari Promise. Masa Depan adalah Janji yang tidak bisa gagal (semua istilah di sini didasarkan pada interpretasi Scala, di mana Janji adalah Monad ).

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

Mudah-mudahan akan berkembang menjadi Janji bergaya Scala penuh pada akhirnya (saya mungkin menulisnya sendiri di beberapa titik; saya yakin PR lain akan diterima; tidak terlalu sulit dengan Future yang sudah ada).

Dalam kasus khusus Anda, saya mungkin akan membuat Result<[Book]>(berdasarkan versi Alexandros Salazar dariResult ). Maka tanda tangan metode Anda adalah:

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

Catatan

  • Saya tidak merekomendasikan fungsi awalan dengan getdi Swift. Ini akan merusak jenis interoperabilitas tertentu dengan ObjC.
  • Saya merekomendasikan parsing sepenuhnya ke Bookobjek sebelum mengembalikan hasil Anda sebagai file Future. Ada beberapa cara sistem ini dapat gagal, dan jauh lebih nyaman jika Anda memeriksa semua hal itu sebelum membungkusnya menjadi file Future. Mendapatkan [Book]jauh lebih baik untuk kode Swift Anda yang lain daripada menyerahkan NSArray.

4
Swiftz tidak lagi mendukung Future. Tapi lihat github.com/mxcl/PromiseKit, ini berfungsi baik dengan Swiftz!
badeleux

butuh beberapa detik untuk menyadari bahwa Anda tidak menulis Swift dan menulis Swift z
Honey

4
Sepertinya "Swiftz" adalah pustaka fungsional pihak ketiga untuk Swift. Karena jawaban Anda tampaknya didasarkan pada pustaka itu, Anda harus menyatakannya secara eksplisit. (misalnya, "Ada pustaka pihak ketiga yang disebut 'Swiftz' yang mendukung konstruksi fungsional seperti Futures, dan harus berfungsi sebagai titik awal yang baik jika Anda ingin mengimplementasikan Promises.") Jika tidak, pembaca Anda hanya akan bertanya-tanya mengapa Anda salah mengeja " Cepat".
Duncan C

3
Harap perhatikan bahwa github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift tidak berfungsi lagi.
Ahmad F

1
@Rob getAwalan menunjukkan return-by-reference di ObjC (seperti di -[UIColor getRed:green:blue:alpha:]). Ketika saya menulis ini, saya khawatir bahwa importir akan memanfaatkan fakta itu (untuk mengembalikan tupel secara otomatis misalnya). Ternyata mereka belum melakukannya. Ketika saya menulis ini, saya mungkin juga lupa bahwa KVC mendukung prefiks "get" untuk pengakses (ini adalah sesuatu yang telah saya pelajari dan lupa beberapa kali). Begitu setuju; Saya belum pernah mengalami kasus di mana pemimpin getmerusak sesuatu. Ini hanya menyesatkan bagi mereka yang tahu arti ObjC "get."
Rob Napier

9

Pola dasarnya adalah dengan menggunakan penutupan penangan penyelesaian.

Misalnya, di Swift 5 yang akan datang, Anda akan menggunakan 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()
}

Dan Anda akan menyebutnya seperti ini:

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

Catatan, di atas saya mengirim penangan penyelesaian kembali ke antrean utama untuk menyederhanakan pembaruan model dan UI. Beberapa pengembang mengambil pengecualian untuk praktik ini dan menggunakan antrean apa pun yang URLSessiondigunakan atau menggunakan antrean mereka sendiri (mengharuskan pemanggil untuk menyinkronkan sendiri hasilnya secara manual).

Tapi itu bukan materi di sini. Masalah utamanya adalah penggunaan penangan penyelesaian untuk menentukan blok kode yang akan dijalankan ketika permintaan asinkron selesai.


Pola yang lebih tua, Swift 4 adalah:

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

Dan Anda akan menyebutnya seperti ini:

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

Catatan, di atas saya menghentikan penggunaan NSArray(kami tidak menggunakan tipe Objective-C yang dijembatani itu lagi). Saya berasumsi bahwa kami memiliki Genretipe dan kami mungkin menggunakan JSONDecoder, daripada JSONSerialization, untuk memecahkan kode itu. Tetapi pertanyaan ini tidak memiliki cukup informasi tentang JSON yang mendasari untuk masuk ke detailnya di sini, jadi saya mengabaikannya untuk menghindari mengaburkan masalah inti, penggunaan closure sebagai penangan penyelesaian.


Anda juga dapat menggunakan Resultdi Swift 4 dan yang lebih rendah, tetapi Anda harus mendeklarasikan enum sendiri. Saya menggunakan pola semacam ini selama bertahun-tahun.
vadian

Ya, tentu saja, seperti halnya saya. Tapi sepertinya Apple telah dipeluk oleh Apple dengan merilis Swift 5. Mereka hanya terlambat ke pesta.
Rob

7

Swift 4.0.0

Untuk Request-Response asinkron, Anda bisa menggunakan penangan penyelesaian. Lihat di bawah saya telah memodifikasi solusi dengan paradigma penyelesaian menangani.

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

Anda dapat memanggil fungsi ini seperti di bawah ini:

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

2

Jawaban Swift 3 versi @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

Saya harap Anda tidak terjebak dalam hal ini, tetapi jawaban singkatnya adalah Anda tidak dapat melakukan ini di Swift.

Pendekatan alternatifnya adalah mengembalikan callback yang akan menyediakan data yang Anda butuhkan segera setelah siap.


1
Dia juga bisa melakukan janji dengan cepat. Tetapi pendekatan yang direkomendasikan apel saat ini menggunakan callbackdengan closures seperti yang Anda tunjukkan atau untuk digunakan delegationseperti API kakao yang lebih lama
Mojtaba Hosseini

Anda benar tentang Promises. Tetapi Swift tidak menyediakan API asli untuk ini, jadi dia harus menggunakan PromiseKit atau alternatif lainnya.
LironXYZ

1

Ada 3 cara untuk membuat fungsi panggilan kembali yaitu: 1. Penyelesaian handler 2. Pemberitahuan 3. Delegasi

Completion Handler Di dalam set blok dijalankan dan dikembalikan ketika sumber tersedia, Handler akan menunggu sampai respon datang sehingga UI dapat diperbarui setelahnya.

Pemberitahuan Sekumpulan informasi dipicu di seluruh aplikasi, Listner dapat mengambil dan menggunakan info itu. Cara asinkron untuk mendapatkan info selama proyek.

Delegasi Kumpulan metode akan dipicu saat delegasi dipanggil, Sumber harus disediakan melalui metode itu sendiri


-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

Terutama ada 3 cara untuk mendapatkan panggilan balik dengan cepat

  1. Penutupan / Penyelesaian penangan

  2. Delegasi

  3. Notifikasi

Pengamat juga dapat digunakan untuk mendapatkan pemberitahuan setelah tugas asinkron selesai.


-2

Ada beberapa persyaratan yang sangat umum yang ingin dipenuhi oleh setiap Manajer API yang baik: akan menerapkan Klien API berorientasi protokol.

Antarmuka Awal APIClient

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

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

protocol APIResponse: Decodable {
}

Sekarang Silakan periksa struktur api lengkap

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

Ini adalah kasus penggunaan kecil yang mungkin bisa membantu: -

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

Saat memanggil fungsi: -

testUrlSession(urlStr: "YOUR-URL") { (value) in
            print("Your string value ::- \(value)")
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.