Trả về dữ liệu từ cuộc gọi không đồng bộ trong hàm Swift


93

Tôi đã tạo một lớp tiện ích trong dự án Swift của mình để xử lý tất cả các yêu cầu và phản hồi REST. Tôi đã xây dựng một API REST đơn giản để tôi có thể kiểm tra mã của mình. Tôi đã tạo một phương thức lớp cần trả về một NSArray nhưng vì lệnh gọi API không đồng bộ nên tôi cần trả về từ phương thức bên trong lệnh gọi không đồng bộ. Vấn đề là async trả về void. Nếu tôi đang làm điều này trong Node, tôi sẽ sử dụng JS hứa hẹn nhưng tôi không thể tìm ra giải pháp hoạt động trong 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
Sai lầm này là rất phổ biến trên Stack Overflow mà tôi đã viết một loạt bài đăng trên blog để đối phó với nó, bắt đầu với programmingios.net/what-asynchronous-means
mờ

Câu trả lời:


96

Bạn có thể chuyển cuộc gọi lại và gọi lại bên trong cuộc gọi không đồng bộ

cái gì đó như:

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

và sau đó gọi phương thức này:

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

Cảm ơn vì điều đó. Câu hỏi cuối cùng của tôi là làm cách nào để gọi phương thức lớp này từ bộ điều khiển chế độ xem của tôi. Mã hiện tại như sau: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 đã cung cấp Future, là nền tảng cơ bản của một Lời hứa. Tương lai là một Lời hứa không thể thất bại (tất cả các thuật ngữ ở đây đều dựa trên cách giải thích của Scala, trong đó Lời hứa là Đơn nguyên ).

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

Hy vọng rằng cuối cùng sẽ mở rộng thành một Lời hứa đầy đủ theo phong cách Scala (Tôi có thể tự viết nó vào một lúc nào đó; tôi chắc chắn rằng các PR khác sẽ được chào đón; nó không khó lắm với Future đã có sẵn).

Trong trường hợp cụ thể của bạn, tôi có thể sẽ tạo một Result<[Book]>(dựa trên phiên bản của Alexandros SalazarResult ). Sau đó, chữ ký phương thức của bạn sẽ là:

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

Ghi chú

  • Tôi không khuyến khích các hàm tiền tố gettrong Swift. Nó sẽ phá vỡ một số loại khả năng tương tác với ObjC.
  • Tôi khuyên bạn nên phân tích cú pháp đến một Bookđối tượng trước khi trả về kết quả của bạn dưới dạng a Future. Có một số cách mà hệ thống này có thể bị lỗi và sẽ thuận tiện hơn nhiều nếu bạn kiểm tra tất cả những thứ đó trước khi gói chúng vào một Future. Bắt đầu [Book]tốt hơn nhiều cho phần còn lại của mã Swift của bạn hơn là sử dụng một NSArray.

4
Swiftz không còn hỗ trợ Future. Nhưng hãy xem github.com/mxcl/PromiseKit, nó hoạt động rất tốt với Swiftz!
badeleux

Tôi mất vài giây để nhận ra bạn không viết Swift và viết Swift z
Honey

4
Có vẻ như "Swiftz" là một thư viện chức năng của bên thứ ba dành cho Swift. Vì câu trả lời của bạn dường như dựa trên thư viện đó, bạn nên nói rõ điều đó. (ví dụ: "Có một thư viện của bên thứ ba được gọi là 'Swiftz' hỗ trợ các cấu trúc chức năng như Futures và sẽ là điểm khởi đầu tốt nếu bạn muốn triển khai Promises.") Nếu không, người đọc của bạn sẽ tự hỏi tại sao bạn viết sai chính tả " Nhanh".
Duncan C

3
Xin lưu ý rằng github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift không hoạt động nữa.
Ahmad F

1
@Rob getTiền tố chỉ ra từng tham chiếu trả lại trong ObjC (chẳng hạn như in -[UIColor getRed:green:blue:alpha:]). Khi tôi viết điều này, tôi lo ngại rằng các nhà nhập khẩu sẽ tận dụng thực tế đó (ví dụ: để trả về một bộ giá trị tự động). Nó chỉ ra rằng họ đã không. Khi tôi viết điều này, có lẽ tôi cũng đã quên rằng KVC hỗ trợ tiền tố "get" cho các trình truy cập (đó là điều tôi đã học và quên nhiều lần). Vì vậy, đã đồng ý; Tôi chưa gặp phải bất kỳ trường hợp nào dẫn đầu getphá vỡ mọi thứ. Nó chỉ gây hiểu lầm cho những người biết ý nghĩa của ObjC "get."
Rob Napier

9

Mô hình cơ bản là sử dụng đóng trình xử lý hoàn thành.

Ví dụ: trong Swift 5 sắp tới, bạn sẽ sử dụng 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()
}

Và bạn sẽ gọi nó như vậy:

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

Lưu ý, ở trên, tôi đang gửi trình xử lý hoàn thành trở lại hàng đợi chính để đơn giản hóa việc cập nhật mô hình và giao diện người dùng. Một số nhà phát triển có ngoại lệ đối với thực tiễn này và sử dụng bất kỳ hàng đợi nào URLSessionđược sử dụng hoặc sử dụng hàng đợi của riêng họ (yêu cầu người gọi tự đồng bộ hóa kết quả theo cách thủ công).

Nhưng đó không phải là vật chất ở đây. Vấn đề quan trọng là việc sử dụng trình xử lý hoàn thành để chỉ định khối mã sẽ được chạy khi yêu cầu không đồng bộ được thực hiện.


Mẫu Swift 4 cũ hơn là:

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

Và bạn sẽ gọi nó như vậy:

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

Lưu ý, ở trên, tôi đã ngừng sử dụng NSArray(chúng tôi không sử dụng các loại Objective-C bắc cầu đó nữa). Tôi giả định rằng chúng tôi đã có một Genreloại và chúng tôi có lẽ đã sử dụng JSONDecoder, thay vì JSONSerialization, để giải mã nó. Nhưng câu hỏi này không có đủ thông tin về JSON cơ bản để đi sâu vào chi tiết ở đây, vì vậy tôi đã bỏ qua điều đó để tránh làm rõ vấn đề cốt lõi, việc sử dụng các bao đóng làm trình xử lý hoàn thành.


Bạn cũng có thể sử dụng Resulttrong Swift 4 trở xuống, nhưng bạn phải tự khai báo enum. Tôi đang sử dụng kiểu này trong nhiều năm.
vadian 14/02/19

Tất nhiên là có, tôi cũng vậy, nhưng có vẻ như nó đã được Apple chấp nhận với việc phát hành Swift 5. Họ chỉ đến muộn.
Rob

7

Swift 4.0

Đối với Yêu cầu-Phản hồi không đồng bộ, bạn có thể sử dụng trình xử lý hoàn thành. Xem bên dưới Tôi đã sửa đổi giải pháp với mô hình xử lý hoàn thành.

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

Bạn có thể gọi hàm này như sau:

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

2

Câu trả lời của @Alexey Globchastyy phiên bản Swift 3:

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

Tôi hy vọng bạn không vẫn còn mắc kẹt về điều này, nhưng câu trả lời ngắn gọn là bạn không thể làm điều này trong Swift.

Một cách tiếp cận thay thế sẽ là trả về một cuộc gọi lại sẽ cung cấp dữ liệu bạn cần ngay khi nó sẵn sàng.


1
Anh ấy cũng có thể hứa nhanh chóng. Nhưng đề nghị aproceh hiện táo được sử dụng callbackvới closures như bạn chỉ ra hoặc sử dụng delegationnhư API cacao của cũ
Mojtaba Hosseini

Bạn đúng về Lời hứa. Nhưng Swift không cung cấp API gốc cho việc này, vì vậy anh ta phải sử dụng PromiseKit hoặc phương thức thay thế khác.
LironXYZ

1

Có 3 cách để tạo các hàm gọi lại cụ thể là: 1. Trình xử lý hoàn thành 2. Thông báo 3. Đại diện

Hoàn thành Xử lý Bên trong tập hợp khối được thực thi và trả về khi có nguồn, Trình xử lý sẽ đợi cho đến khi có phản hồi để có thể cập nhật giao diện người dùng sau đó.

Thông báo Bó thông tin được kích hoạt trên tất cả các ứng dụng, Người nghe có thể truy xuất để sử dụng thông tin đó. Không đồng bộ hóa cách lấy thông tin thông qua dự án.

Ủy quyền Tập hợp các phương thức sẽ được kích hoạt khi đại biểu được gọi, Nguồn phải được cung cấp thông qua chính các phương thức


-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

Chủ yếu có 3 cách để đạt được cuộc gọi lại nhanh chóng

  1. Trình xử lý Đóng / Hoàn thành

  2. Đại biểu

  3. Thông báo

Người quan sát cũng có thể được sử dụng để nhận thông báo khi nhiệm vụ không đồng bộ đã hoàn thành.


-2

Có một số yêu cầu rất chung chung mà mọi Trình quản lý API tốt đều phải đáp ứng: sẽ triển khai Ứng dụng khách API hướng giao thức.

Giao diện ban đầu APIClient

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

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

protocol APIResponse: Decodable {
}

Bây giờ hãy kiểm tra cấu trúc api hoàn chỉnh

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

Đây là một trường hợp sử dụng nhỏ có thể hữu ích: -

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

Trong khi gọi hàm: -

testUrlSession(urlStr: "YOUR-URL") { (value) in
            print("Your string value ::- \(value)")
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.