すべてのSwift要求および応答を処理するRESTプロジェクトにユーティリティクラスを作成しました。コードをテストできるように、単純なREST APIを作成しました。 NSArrayを返す必要があるクラスメソッドを作成しましたが、API呼び出しが非同期であるため、非同期呼び出し内のメソッドから返す必要があります。問題は、非同期がvoidを返すことです。 Nodeでこれを行っていた場合、JS Promiseを使用しますが、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...
}
}
コールバックを渡し、非同期呼び出し内でコールバックを呼び出すことができます
何かのようなもの:
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)")
}
}
SwiftzはすでにFutureを提供しています。これはPromiseの基本的な構成要素です。未来は失敗することのない約束です(ここのすべての用語はScalaの解釈に基づいています ここで約束はモナドです )。
https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.Swift
最終的には完全なScalaスタイルのPromiseに拡張されることを願っています(ある時点で自分で書くかもしれません。他のPRも歓迎されると確信しています。Futureがすでに用意されているのでそれほど難しくはありません)。
あなたの特定のケースでは、おそらくResult<[Book]>
を作成します( Alexandros SalazarのResult
のバージョン に基づいています)。次に、メソッドのシグネチャは次のようになります。
class func fetchGenres() -> Future<Result<[Book]>> {
ノート
get
を付けることはお勧めしません。 ObjCとの特定の種類の相互運用性が破壊されます。Book
として返す前に、Future
オブジェクトに至るまで解析することをお勧めします。このシステムに障害が発生する可能性のある方法はいくつかありますが、それらをすべてFuture
にまとめる前にそれらすべてをチェックする方がはるかに便利です。 [Book]
に到達することは、Swiftコードの残りの部分でNSArray
を処理するよりもはるかに優れています。Swift 4.
非同期リクエスト-レスポンスの場合、完了ハンドラを使用できます。以下を参照してください。完了ハンドルパラダイムでソリューションを変更しました。
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
}
基本的なパターンは、完了ハンドラーのクロージャーを使用することです。
たとえば、今後の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についての十分な情報がなかったため、コアの問題、完了ハンドラーとしてのクロージャーの使用を曇らせないようにすることを省略しました。
@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()
}
まだこれにこだわっていないことを願っていますが、簡単な答えは、Swiftではこれができないということです。
別のアプローチは、準備ができ次第、必要なデータを提供するコールバックを返すことです。
コールバック関数を作成するには、次の3つの方法があります。1.完了ハンドラー2.通知3.デリゲート
完了ハンドラーブロックのセットが実行され、ソースが使用可能になったときに返されると、ハンドラーは応答が来るまで待機し、UIが更新されるようにします。
通知すべてのアプリで情報の束がトリガーされ、Listnerはその情報を利用してnを取得できます。プロジェクト全体で情報を取得する非同期の方法。
デリゲートデリゲートが呼び出されたときにメソッドのセットがトリガーされます。ソースはメソッド自体を介して提供する必要があります
これは役に立つかもしれない小さなユースケースです:-
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)")
}
クロージャー/完了ハンドラー
デリゲート
通知
オブザーバーを使用して、非同期タスクが完了すると通知を受け取ることもできます。
すべての優れた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)
}
}
}
完了ブロックを使用して、メインスレッドでアクティブにします。
メインスレッドはUIスレッドです。非同期タスクを作成し、UIを更新する場合は、UIスレッドですべてのUI変更を行う必要があります
例:
func asycFunc(completion: () -> Void) {
URLSession.shared.dataTask(with: request) { data, _, error in
// This is an async task...!!
if let error = error {
}
DispatchQueue.main.async(execute: { () -> Void in
//When the async taks will be finished this part of code will run on the main thread
completion()
})
}
}