Swift 함수의 비동기 호출에서 데이터 반환
모든 REST 요청과 응답을 처리하는 Swift 프로젝트에 유틸리티 클래스를 만들었습니다. 코드를 테스트 할 수 있도록 간단한 REST API를 구축했습니다. NSArray를 반환해야하는 클래스 메서드를 만들었지 만 API 호출이 비동기이기 때문에 비동기 호출 내부의 메서드에서 반환해야합니다. 문제는 비동기가 무효를 반환한다는 것입니다. 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는 이미 Promise의 기본 구성 요소 인 Future를 제공합니다. 퓨처는 실패 할 수없는 약속입니다 (여기서 모든 용어는 스칼라 해석을 기반으로 하며 약속은 모나드입니다 ).
https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift
결국 완전한 스칼라 스타일의 약속으로 확장되기를 바랍니다 (언젠가는 직접 작성할 수 있습니다. 다른 PR도 환영받을 것입니다. Future가 이미 자리를 잡은 상태에서는 그렇게 어렵지 않습니다).
귀하의 특정 경우에는 아마도 Result<[Book]>
( Alexandros Salazar의 버전을Result
기반으로) 만들 것입니다 . 그러면 메서드 서명은 다음과 같습니다.
class func fetchGenres() -> Future<Result<[Book]>> {
메모
get
Swift에서 함수 접두사를 사용하지 않는 것이 좋습니다 . ObjC와의 특정 종류의 상호 운용성을 깨뜨릴 것입니다.Book
결과를 .txt 파일로 반환하기 전에 객체 까지 파싱하는 것이 좋습니다Future
. 이 시스템이 실패 할 수있는 방법은 여러 가지가 있으며 .NET Framework로 정리하기 전에 이러한 모든 사항을 확인하는 것이 훨씬 더 편리합니다Future
. 를 처리[Book]
하는 것보다 나머지 Swift 코드에서NSArray
.
스위프트 4.0
비동기 요청-응답의 경우 완료 처리기를 사용할 수 있습니다. 아래에서 완료 핸들 패러다임을 사용하여 솔루션을 수정했습니다.
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
}
@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 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에 대한 정보가 충분하지 않았기 때문에 핵심 문제인 클로저를 완료 핸들러로 사용하는 것을 피하기 위해 생략했습니다.
나는 당신이 여전히 이것에 집착하지 않기를 바랍니다. 그러나 짧은 대답은 당신이 Swift에서 이것을 할 수 없다는 것입니다.
다른 방법은 필요한 데이터가 준비되는 즉시 제공하는 콜백을 반환하는 것입니다.
콜백 함수를 생성하는 방법은 3 가지가 있습니다. 1. 완료 핸들러 2. 알림 3. 델리게이트
Completion Handler 내부 블록 집합이 실행되어 소스가 사용 가능할 때 반환되며, Handler는 응답이 올 때까지 대기하여 UI가 업데이트 될 수 있도록합니다.
알림 정보 묶음은 모든 앱에서 트리거되며 Listner는 해당 정보를 사용하여 검색 할 수 있습니다. 프로젝트를 통해 정보를 얻는 비동기 방식.
델리게이트 메소드 집합은 델리게이트가 호출 될 때 트리거되며 소스는 메소드 자체를 통해 제공되어야합니다.
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()
신속하게 콜백을 달성하는 방법은 크게 3 가지가 있습니다.
폐쇄 / 완료 핸들러
대표자
알림
비동기 작업이 완료되면 관찰자를 사용하여 알림을받을 수도 있습니다.
완료 블록을 사용하고 메인 스레드에서 활성화하십시오.
주 스레드는 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()
})
}
}
모든 좋은 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)
}
}
}
이것은 도움이 될 수있는 작은 사용 사례입니다.
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)")
}
참고 URL : https://stackoverflow.com/questions/25203556/returning-data-from-async-call-in-swift-function
'program story' 카테고리의 다른 글
Subversion : 체크 아웃하고 수정 한 다음 브랜치로 만들 수 있습니까? (0) | 2020.10.08 |
---|---|
Bootstrap 프로젝트에서 normalize.css를 사용해야합니까? (0) | 2020.10.08 |
인스턴스 변수에 직접 액세스 vs. 접근 자 메서드 사용 (0) | 2020.10.08 |
깨끗한 풀 요청을 할 수 있도록 Github 포크를 어떻게 정리합니까? (0) | 2020.10.08 |
음수를 처리하는 C / C ++ / Obj-C에서 모듈로 (%) 연산자를 코딩하는 방법 (0) | 2020.10.08 |