Swift関数の非同期呼び出しからデータを返す


93

すべてのREST要求と応答を処理するユーティリティクラスをSwiftプロジェクトに作成しました。コードをテストできるように、簡単なREST APIを作成しました。NSArrayを返す必要があるクラスメソッドを作成しましたが、API呼び出しは非同期なので、非同期呼び出し内のメソッドから返す必要があります。問題は、非同期が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

回答:


96

コールバックを渡し、非同期呼び出し内でコールバックを呼び出すことができます

何かのようなもの:

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) }
マーク・Tyers

13

SwiftzはすでにPromiseの基本的なビルディングブロックであるFutureを提供しています。未来は失敗することのない約束です(ここでのすべての用語はScalaの解釈に基づいており、約束はモナドです)。

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

うまくいけば、最終的には完全なScalaスタイルのPromiseに拡張されます(いつかは自分で書くかもしれません。他のPRも歓迎します。Futureがすでに導入されているのでそれほど難しくありません)。

あなたの特定のケースでは、おそらくResult<[Book]>アレクサンドロスサラザールのバージョンのにResult基づいて)を作成します。次に、メソッドの署名は次のようになります。

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

ノート

  • getSwiftで関数に接頭辞を付けることはお勧めしません。これは、ObjCとの特定の種類の相互運用性を破壊します。
  • Book結果をとして返す前に、オブジェクトまで完全に解析することをお勧めしますFuture。このシステムが失敗する原因はいくつかありますFuture。それらをすべてにまとめる前にそれらすべてをチェックすると、はるかに便利です。[Book]他のSwiftコードでは、を手に入れるよりもにアクセスする方がはるかに優れていますNSArray

4
SwiftzはサポートしなくなりましたFuture。しかし、github.com / mxcl / PromiseKitをご覧ください。Swiftでうまく動作します!
badeleux 2015

あなたがSwiftを書かなかったことに気づくのに数秒かかって、Swift z
Honey

4
"Swiftz"はSwiftのサードパーティの機能ライブラリのようです。あなたの答えはそのライブラリに基づいているようですので、それを明確に述べる必要があります。(たとえば、「Futuresのような機能的な構成をサポートするサードパーティのライブラリがあり、Promiseを実装したい場合は、良い出発点として機能するはずです。」)そうでないと、読者はつづりを間違えたのかと疑問に思うでしょう。迅速"。
ダンカンC

3
github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swiftが機能しなくなったことに注意してください。
Ahmad F

1
@Rob get接頭辞は、ObjCの参照による戻り値を示します(など-[UIColor getRed:green:blue:alpha:])。私がこれを書いたとき、インポーターがその事実を利用するのではないかと心配していました(たとえば、タプルを自動的に返すため)。彼らはそうではないことが判明しました。私がこれを書いたとき、KVCがアクセサの "get"プレフィックスをサポートしていることも忘れていたはずです(これは私が何度か学んで忘れてきたものです)。そう同意した。私はリーディングがget物事を壊すようなケースに遭遇したことはありません。これは、ObjCの「取得」の意味を知っている人を誤解させるだけです。
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に関する十分な情報がなかったため、ここで詳細を説明することはできませんでした。そのため、コアクラウドの問題である完了ハンドラーとしてのクロージャーの使用を回避するために省略しました。


ResultSwift 4以下でも使用できますが、列挙型を自分で宣言する必要があります。この種のパターンを何年も使用しています。
バディアン

はい、もちろん、私もそうです。しかし、Swift 5のリリースでAppleに採用されたようです。彼らはパーティーに遅れをとっています。
ロブ

7

Swift 4.0

非同期の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

@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
彼はまた、迅速に約束をすることができます。しかし、Appleの現在の推奨aprocehを使用しているcallbackとのclosureご指摘の通り、Sまたは使用delegationの古いココアAPIのような
モタバ・ホセイーニ

あなたは約束について正しい。しかし、SwiftはこのためのネイティブAPIを提供していないため、PromiseKitまたはその他の代替手段を使用する必要があります。
LironXYZ

1

つまり、コールバック関数を作成する方法は3つあります。1.完了ハンドラ2.通知3.デリゲート

完了ハンドラー ブロックのセットが実行され、ソースが利用可能になると返されます。ハンドラーは、応答が来るまで待機して、UIを後で更新できるようにします。

通知の 一連の情報はすべてのアプリでトリガーされ、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()

-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)")
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.