Возврат данных из асинхронного вызова в функции Swift

93

Я создал служебный класс в моем проекте Swift, который обрабатывает все запросы и ответы REST. Я создал простой REST API, чтобы проверить свой код. Я создал метод класса, который должен возвращать NSArray, но поскольку вызов API является асинхронным, мне нужно вернуться из метода внутри асинхронного вызова. Проблема в том, что async возвращает void. Если бы я делал это в Node, я бы использовал JS-обещания, но я не могу найти решение, которое работает в 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
Эта ошибка так часто встречается на переполнение стека , что я написал серию сообщений в блоге , чтобы справиться с ней, начиная с programmingios.net/what-asynchronous-means
матовые

Ответы:

97

Вы можете передать обратный вызов и обратный вызов внутри асинхронного вызова

что-то типа:

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

а затем вызовите этот метод:

override func viewDidLoad() {
    Bookshop.getGenres {
        genres in
        println("View Controller: \(genres)")     
    }
}
Алексей Глобчастый
источник
Спасибо за это. Мой последний вопрос: как вызвать этот метод класса из моего контроллера представления. Код в настоящее время такой: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) }
Марк Тайерс
13

Swiftz уже предлагает Future, который является основным строительным блоком Promise. Будущее - это обещание, которое не может потерпеть неудачу (все термины здесь основаны на интерпретации Scala, где обещание - это монада ).

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

Надеюсь, в конечном итоге он будет расширен до полного обещания в стиле Scala (в какой-то момент я могу написать его сам; я уверен, что другие PR будут приветствоваться; это не так сложно, если Future уже существует).

В вашем конкретном случае я бы, вероятно, создал Result<[Book]>(на основе версии Александроса СалазараResult ). Тогда ваша подпись метода будет:

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

Примечания

  • Я не рекомендую ставить перед функциями префикс getв Swift. Это нарушит определенные виды взаимодействия с ObjC.
  • Я рекомендую выполнить синтаксический анализ до Bookобъекта, прежде чем возвращать результаты в виде файла Future. Есть несколько причин, по которым эта система может выйти из строя, и будет намного удобнее, если вы проверите все эти вещи перед тем, как заключить их в файл Future. [Book]Для остальной части вашего кода Swift гораздо лучше добраться, чем передавать NSArray.
Роб Напье
источник
4
Swiftz больше не поддерживает Future. Но взгляните на github.com/mxcl/PromiseKit, он отлично работает со Swiftz!
badeleux
мне потребовалось несколько секунд, чтобы понять, что вы написали не Swift, а Swift z
Дорогая
4
Похоже, «Swiftz» - это сторонняя функциональная библиотека для Swift. Поскольку ваш ответ, похоже, основан на этой библиотеке, вы должны указать это явно. (например, «Существует сторонняя библиотека под названием« Swiftz », которая поддерживает функциональные конструкции, такие как Futures, и должна служить хорошей отправной точкой, если вы хотите реализовать обещания».) В противном случае ваши читатели просто зададутся вопросом, почему вы сделали ошибку » Свифт ».
Duncan C
3
Обратите внимание, что github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift больше не работает.
Ahmad F
1
@Rob getПрефикс указывает на возврат по ссылке в ObjC (например, in -[UIColor getRed:green:blue:alpha:]). Когда я писал это, я был обеспокоен тем, что импортеры воспользуются этим фактом (например, для автоматического возврата кортежа). Оказалось, что нет. Когда я писал это, я, вероятно, также забыл, что KVC поддерживает префиксы "get" для аксессоров (это то, что я выучил и забыл несколько раз). Итак, согласился; Я не встречал случаев, когда ведущий что-то getломает. Это просто вводит в заблуждение тех, кто знает значение слова ObjC «получить».
Роб Напье
9

Основной шаблон - использовать закрытие обработчиков завершения.

Например, в готовящемся к выпуску Swift 5 вы должны использовать 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()
}

И вы бы назвали это так:

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

Обратите внимание, что выше я отправляю обработчик завершения обратно в основную очередь, чтобы упростить обновление модели и пользовательского интерфейса. Некоторые разработчики возражают против этой практики и либо используют любую URLSessionиспользуемую очередь, либо используют свою собственную очередь (требуя, чтобы вызывающий абонент сам вручную синхронизировал результаты).

Но здесь это не материально. Ключевой проблемой является использование обработчика завершения для указания блока кода, который должен быть запущен при выполнении асинхронного запроса.


Более старый шаблон 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()
}

И вы бы назвали это так:

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

Обратите внимание, что выше я отказался от использования NSArray(мы больше не используем эти объединенные типы Objective-C ). Я предполагаю, что у нас был Genreтип, и мы предположительно использовали его JSONDecoder, а не JSONSerializationдля его декодирования. Но в этом вопросе не было достаточно информации о базовом JSON, чтобы вдаваться в подробности, поэтому я пропустил это, чтобы не затушевывать основную проблему, использование замыканий в качестве обработчиков завершения.

Роб
источник
Вы также можете использовать ResultSwift 4 и ниже, но вам нужно объявить перечисление самостоятельно. Я использую такой узор годами.
vadian
Да, конечно, как и я. Но это только выглядит так, как будто Apple приняла это с выпуском Swift 5. Они просто опоздали на вечеринку.
Роб
7

Swift 4.0

Для async Request-Response вы можете использовать обработчик завершения. См. Ниже. Я изменил решение с парадигмой дескриптора завершения.

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

Вы можете вызвать эту функцию, как показано ниже:

getGenres { (array) in
    // Do operation with array
}
Джейдип Вора
источник
2

Swift 3 версия ответа @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

Надеюсь, вы все еще не зациклились на этом, но краткий ответ заключается в том, что вы не можете сделать это в Swift.

Альтернативный подход - вернуть обратный вызов, который предоставит вам необходимые данные, как только они будут готовы.

LironXYZ
источник
1
Он тоже может быстро выполнять обещания. Но текущий рекомендуемый aproceh для Apple используется callbackс closures, как вы указываете, или для использования, delegationкак более старые API-интерфейсы какао
Mojtaba Hosseini
Вы правы насчет обещаний. Но Swift не предоставляет для этого нативный API, поэтому ему приходится использовать PromiseKit или другую альтернативу.
LironXYZ
1

Есть 3 способа создания функций обратного вызова, а именно: 1. Обработчик завершения 2. Уведомление 3. Делегаты

Обработчик завершения Внутренний набор блоков выполняется и возвращается, когда источник доступен, обработчик будет ждать, пока не придет ответ, чтобы пользовательский интерфейс мог быть обновлен после.

Уведомление. Объем информации передается по всему приложению, Listner может получить и использовать эту информацию. Асинхронный способ получения информации по проекту.

Делегаты Набор методов будет срабатывать при вызове делегата, источник должен быть предоставлен через сами методы

ИРАННА САЛУНКЕ
источник
-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
источник
-1

В основном есть 3 способа быстро выполнить обратный вызов.

  1. Закрытие / Обработчик завершения

  2. Делегаты

  3. Уведомления

Наблюдатели также можно использовать для получения уведомлений после завершения асинхронной задачи.

Шубхам Мишра
источник
-2

Есть несколько очень общих требований, которым должен удовлетворять каждый хороший менеджер API: будет реализован протокол-ориентированный клиент API.

Начальный интерфейс APIClient

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

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

protocol APIResponse: Decodable {
}

Теперь проверьте полную структуру 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)
        }
    }

}
Рашпиндер Маан
источник
-2

Это небольшой вариант использования, который может быть полезен: -

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

При вызове функции: -

testUrlSession(urlStr: "YOUR-URL") { (value) in
            print("Your string value ::- \(value)")
}
А.Пало
источник