การคืนข้อมูลจากการโทรแบบ async ในฟังก์ชัน Swift


93

ฉันได้สร้างคลาสยูทิลิตี้ในโครงการ Swift ของฉันที่จัดการคำขอ REST และการตอบกลับทั้งหมด ฉันได้สร้าง REST API อย่างง่ายเพื่อให้ฉันสามารถทดสอบโค้ดของฉันได้ ฉันได้สร้างเมธอดคลาสที่ต้องการส่งคืน NSArray แต่เนื่องจากการเรียก API เป็นแบบ async ฉันต้องส่งคืนจากเมธอดภายในการเรียก async ปัญหาคือ async คืนค่าเป็นโมฆะ ถ้าฉันทำสิ่งนี้ใน 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
ข้อผิดพลาดนี้เกิดขึ้นบ่อยใน Stack Overflow ซึ่งฉันได้เขียนบล็อกโพสต์ไว้หลายชุดเพื่อจัดการกับมันโดยเริ่มจากprogrammingios.net/what-asynchronous-means
แมตต์

คำตอบ:


97

คุณสามารถส่งการโทรกลับและโทรกลับภายในการโทรแบบ async

สิ่งที่ต้องการ:

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) }
Mark Tyers

13

Swiftz นำเสนอ Future ซึ่งเป็นส่วนประกอบพื้นฐานของ Promise อนาคตคือคำสัญญาที่ไม่สามารถล้มเหลวได้ (คำศัพท์ทั้งหมดที่นี่อ้างอิงจากการตีความของสกาล่าโดยที่คำสัญญาคือโมนาด )

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

หวังว่าจะขยายไปสู่ ​​Promise แบบเต็มรูปแบบในที่สุด (ฉันอาจจะเขียนเองในบางจุดฉันมั่นใจว่า PR อื่น ๆ จะยินดีต้อนรับไม่ใช่เรื่องยากสำหรับอนาคตที่มีอยู่แล้ว)

ในกรณีเฉพาะของคุณฉันอาจจะสร้างResult<[Book]>(ตามเวอร์ชันของ Alexandros SalazarResult ) จากนั้นลายเซ็นวิธีของคุณจะเป็น:

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

หมายเหตุ

  • ฉันไม่แนะนำให้ใช้ฟังก์ชันนำหน้าgetใน Swift มันจะทำลายความสามารถในการทำงานร่วมกันบางประเภทกับ ObjC
  • ขอแนะนำให้แยกวิเคราะห์ลงไปที่Bookวัตถุก่อนส่งคืนผลลัพธ์เป็นไฟล์Future. มีหลายวิธีที่ระบบนี้อาจล้มเหลวและจะสะดวกกว่ามากหากคุณตรวจสอบสิ่งเหล่านั้นทั้งหมดก่อนที่จะรวมเป็นไฟล์Future. การเดินทางไปยัง[Book]ส่วนที่เหลือของ Swift code จะดีกว่าการส่งNSArrayไฟล์.

4
Swiftz Futureการสนับสนุนอีกต่อไป แต่ลองดูที่github.com/mxcl/PromiseKitมันใช้งานได้ดีกับ Swiftz!
badeleux

ฉันใช้เวลาสองสามวินาทีกว่าจะรู้ว่าคุณไม่ได้เขียน Swift และเขียน Swift z
Honey

4
ดูเหมือนว่า "Swiftz" เป็นไลบรารีการทำงานของบุคคลที่สามสำหรับ Swift เนื่องจากคำตอบของคุณดูเหมือนจะเป็นไปตามไลบรารีนั้นคุณควรระบุอย่างชัดเจน (เช่น "มีไลบรารีของบุคคลที่สามที่เรียกว่า" Swiftz "ที่รองรับโครงสร้างที่ใช้งานได้เช่น Futures และควรใช้เป็นจุดเริ่มต้นที่ดีหากคุณต้องการใช้ Promises") มิฉะนั้นผู้อ่านของคุณจะสงสัยว่าทำไมคุณจึงสะกดผิด " รวดเร็ว ".
Duncan C

3
โปรดทราบว่าgithub.com/maxpow4h/swiftz/blob/master/swiftz/Future.swiftไม่ทำงานอีกต่อไป
Ahmad F

1
@Rob getคำนำหน้าระบุการส่งคืนโดยการอ้างอิงใน ObjC (เช่นใน-[UIColor getRed:green:blue:alpha:]) เมื่อฉันเขียนสิ่งนี้ฉันกังวลว่าผู้นำเข้าจะใช้ประโยชน์จากข้อเท็จจริงนั้น (เช่นส่งคืนทูเปิลโดยอัตโนมัติ) ปรากฎว่าพวกเขาไม่ได้ทำ เมื่อฉันเขียนสิ่งนี้ฉันอาจจะลืมไปแล้วว่า KVC รองรับคำนำหน้า "get" สำหรับ accessors ตกลงกันแล้ว; ฉันไม่เคยเจอกรณีใด ๆ ที่ผู้นำgetทำลายสิ่งต่างๆ เป็นเพียงการเข้าใจผิดสำหรับผู้ที่รู้ความหมายของ ObjC "get"
Rob Napier

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

หมายเหตุด้านบนฉันกำลังส่งตัวจัดการความสมบูรณ์กลับไปที่คิวหลักเพื่อลดความซับซ้อนของโมเดลและการอัปเดต UI นักพัฒนาบางรายใช้ข้อยกเว้นในการปฏิบัตินี้และใช้คิวอะไรก็ได้ที่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 ที่จะเข้ามาในรายละเอียดที่นี่ดังนั้นฉันจึงละเว้นเพื่อหลีกเลี่ยงไม่ให้ปัญหาหลักขุ่นมัวการใช้การปิดเป็นตัวจัดการที่สมบูรณ์


คุณสามารถใช้Resultใน Swift 4 และต่ำกว่าได้เช่นกัน แต่คุณต้องประกาศ enum ด้วยตัวเอง ฉันใช้รูปแบบนี้มาหลายปีแล้ว
Vadian

ใช่แน่นอนเช่นเดียวกับฉัน แต่ดูเหมือนว่า Apple จะได้รับการยอมรับจาก Apple ด้วยการเปิดตัว Swift 5 พวกเขาเพิ่งไปงานปาร์ตี้
Rob

7

Swift 4.0

สำหรับ async Request-Response คุณสามารถใช้ complete handler ดูด้านล่างฉันได้แก้ไขโซลูชันด้วยกระบวนทัศน์การจัดการที่สมบูรณ์

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

คำตอบของ @Alexey Globchastyy เวอร์ชัน 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

ฉันหวังว่าคุณจะยังไม่ติดปัญหานี้ แต่คำตอบสั้น ๆ คือคุณไม่สามารถทำสิ่งนี้ใน Swift ได้

อีกทางเลือกหนึ่งคือส่งคืนการเรียกกลับซึ่งจะให้ข้อมูลที่คุณต้องการทันทีที่พร้อม


1
เขาสามารถทำสัญญาได้อย่างรวดเร็วเช่นกัน แต่ aproceh ที่แนะนำในปัจจุบันของ apple ใช้callbackกับclosures ตามที่คุณชี้หรือใช้delegationเช่น API ของโกโก้รุ่นเก่า
Mojtaba Hosseini

คุณพูดถูกเกี่ยวกับสัญญา แต่ Swift ไม่มี API ดั้งเดิมสำหรับสิ่งนี้เขาจึงต้องใช้ PromiseKit หรือทางเลือกอื่น
LironXYZ

1

การสร้างฟังก์ชันเรียกกลับมี 3 วิธี ได้แก่ 1. ตัวจัดการการเสร็จสิ้น 2. การแจ้งเตือน 3. ผู้แทน

ตัวจัดการความสมบูรณ์ ภายในชุดบล็อกถูกเรียกใช้งานและส่งคืนเมื่อแหล่งที่มาพร้อมใช้งานตัวจัดการจะรอจนกว่าจะมีการตอบกลับเพื่อให้สามารถอัปเดต UI ได้ในภายหลัง

การแจ้งเตือน กลุ่มข้อมูลจะถูกเรียกใช้ในทุกแอพ Listner สามารถดึง n ใช้ประโยชน์จากข้อมูลนั้นได้ วิธี Async ในการรับข้อมูลผ่านโครงการ

Delegates ชุดของวิธีการจะถูกทริกเกอร์เมื่อมีการเรียกผู้รับมอบสิทธิ์แหล่งที่มาจะต้องถูกจัดเตรียมผ่านวิธีการเอง


-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

ส่วนใหญ่มี 3 วิธีในการโทรกลับอย่างรวดเร็ว

  1. ตัวจัดการการปิด / เสร็จสิ้น

  2. ผู้รับมอบสิทธิ์

  3. การแจ้งเตือน

นอกจากนี้ยังสามารถใช้ผู้สังเกตการณ์เพื่อรับการแจ้งเตือนเมื่องาน async เสร็จสมบูรณ์


-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)")
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.