Saya telah membuat kelas utilitas dalam proyek Swift saya yang menangani semua permintaan dan tanggapan REST. Saya telah membangun REST API sederhana sehingga saya dapat menguji kode saya. Saya telah membuat metode kelas yang perlu mengembalikan NSArray tetapi karena panggilan API adalah asinkron, saya harus kembali dari metode di dalam panggilan async. Masalahnya adalah pengembalian asinkron batal. Jika saya melakukan ini di Node, saya akan menggunakan janji JS tetapi saya tidak dapat menemukan solusi yang berfungsi di 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...
}
}
ios
rest
asynchronous
swift
Mark Tyers
sumber
sumber
Jawaban:
Anda dapat meneruskan callback, dan memanggil callback di dalam panggilan async
sesuatu seperti:
class func getGenres(completionHandler: (genres: NSArray) -> ()) { ... let task = session.dataTaskWithURL(url) { data, response, error in ... resultsArray = results completionHandler(genres: resultsArray) } ... task.resume() }
lalu panggil metode ini:
override func viewDidLoad() { Bookshop.getGenres { genres in println("View Controller: \(genres)") } }
sumber
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) }
Swiftz sudah menawarkan Future, yang merupakan elemen dasar dari Promise. Masa Depan adalah Janji yang tidak bisa gagal (semua istilah di sini didasarkan pada interpretasi Scala, di mana Janji adalah Monad ).
https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift
Mudah-mudahan akan berkembang menjadi Janji bergaya Scala penuh pada akhirnya (saya mungkin menulisnya sendiri di beberapa titik; saya yakin PR lain akan diterima; tidak terlalu sulit dengan Future yang sudah ada).
Dalam kasus khusus Anda, saya mungkin akan membuat
Result<[Book]>
(berdasarkan versi Alexandros Salazar dariResult
). Maka tanda tangan metode Anda adalah:class func fetchGenres() -> Future<Result<[Book]>> {
Catatan
get
di Swift. Ini akan merusak jenis interoperabilitas tertentu dengan ObjC.Book
objek sebelum mengembalikan hasil Anda sebagai fileFuture
. Ada beberapa cara sistem ini dapat gagal, dan jauh lebih nyaman jika Anda memeriksa semua hal itu sebelum membungkusnya menjadi fileFuture
. Mendapatkan[Book]
jauh lebih baik untuk kode Swift Anda yang lain daripada menyerahkanNSArray
.sumber
Future
. Tapi lihat github.com/mxcl/PromiseKit, ini berfungsi baik dengan Swiftz!get
Awalan menunjukkan return-by-reference di ObjC (seperti di-[UIColor getRed:green:blue:alpha:]
). Ketika saya menulis ini, saya khawatir bahwa importir akan memanfaatkan fakta itu (untuk mengembalikan tupel secara otomatis misalnya). Ternyata mereka belum melakukannya. Ketika saya menulis ini, saya mungkin juga lupa bahwa KVC mendukung prefiks "get" untuk pengakses (ini adalah sesuatu yang telah saya pelajari dan lupa beberapa kali). Begitu setuju; Saya belum pernah mengalami kasus di mana pemimpinget
merusak sesuatu. Ini hanya menyesatkan bagi mereka yang tahu arti ObjC "get."Pola dasarnya adalah dengan menggunakan penutupan penangan penyelesaian.
Misalnya, di Swift 5 yang akan datang, Anda akan menggunakan
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() }
Dan Anda akan menyebutnya seperti ini:
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
Catatan, di atas saya mengirim penangan penyelesaian kembali ke antrean utama untuk menyederhanakan pembaruan model dan UI. Beberapa pengembang mengambil pengecualian untuk praktik ini dan menggunakan antrean apa pun yang
URLSession
digunakan atau menggunakan antrean mereka sendiri (mengharuskan pemanggil untuk menyinkronkan sendiri hasilnya secara manual).Tapi itu bukan materi di sini. Masalah utamanya adalah penggunaan penangan penyelesaian untuk menentukan blok kode yang akan dijalankan ketika permintaan asinkron selesai.
Pola yang lebih tua, Swift 4 adalah:
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() }
Dan Anda akan menyebutnya seperti ini:
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
Catatan, di atas saya menghentikan penggunaan
NSArray
(kami tidak menggunakan tipe Objective-C yang dijembatani itu lagi). Saya berasumsi bahwa kami memilikiGenre
tipe dan kami mungkin menggunakanJSONDecoder
, daripadaJSONSerialization
, untuk memecahkan kode itu. Tetapi pertanyaan ini tidak memiliki cukup informasi tentang JSON yang mendasari untuk masuk ke detailnya di sini, jadi saya mengabaikannya untuk menghindari mengaburkan masalah inti, penggunaan closure sebagai penangan penyelesaian.sumber
Result
di Swift 4 dan yang lebih rendah, tetapi Anda harus mendeklarasikan enum sendiri. Saya menggunakan pola semacam ini selama bertahun-tahun.Swift 4.0.0
Untuk Request-Response asinkron, Anda bisa menggunakan penangan penyelesaian. Lihat di bawah saya telah memodifikasi solusi dengan paradigma penyelesaian menangani.
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() }
Anda dapat memanggil fungsi ini seperti di bawah ini:
getGenres { (array) in // Do operation with array }
sumber
Jawaban Swift 3 versi @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() }
sumber
Saya harap Anda tidak terjebak dalam hal ini, tetapi jawaban singkatnya adalah Anda tidak dapat melakukan ini di Swift.
Pendekatan alternatifnya adalah mengembalikan callback yang akan menyediakan data yang Anda butuhkan segera setelah siap.
sumber
callback
denganclosure
s seperti yang Anda tunjukkan atau untuk digunakandelegation
seperti API kakao yang lebih lamaAda 3 cara untuk membuat fungsi panggilan kembali yaitu: 1. Penyelesaian handler 2. Pemberitahuan 3. Delegasi
Completion Handler Di dalam set blok dijalankan dan dikembalikan ketika sumber tersedia, Handler akan menunggu sampai respon datang sehingga UI dapat diperbarui setelahnya.
Pemberitahuan Sekumpulan informasi dipicu di seluruh aplikasi, Listner dapat mengambil dan menggunakan info itu. Cara asinkron untuk mendapatkan info selama proyek.
Delegasi Kumpulan metode akan dipicu saat delegasi dipanggil, Sumber harus disediakan melalui metode itu sendiri
sumber
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()
sumber
Terutama ada 3 cara untuk mendapatkan panggilan balik dengan cepat
Penutupan / Penyelesaian penangan
Delegasi
Notifikasi
Pengamat juga dapat digunakan untuk mendapatkan pemberitahuan setelah tugas asinkron selesai.
sumber
Ada beberapa persyaratan yang sangat umum yang ingin dipenuhi oleh setiap Manajer API yang baik: akan menerapkan Klien API berorientasi protokol.
Antarmuka Awal APIClient
protocol APIClient { func send(_ request: APIRequest, completion: @escaping (APIResponse?, Error?) -> Void) } protocol APIRequest: Encodable { var resourceName: String { get } } protocol APIResponse: Decodable { }
Sekarang Silakan periksa struktur api lengkap
// ******* 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) } } }
sumber
Ini adalah kasus penggunaan kecil yang mungkin bisa membantu: -
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() }
Saat memanggil fungsi: -
testUrlSession(urlStr: "YOUR-URL") { (value) in print("Your string value ::- \(value)") }
sumber