program story

Swift 함수의 비동기 호출에서 데이터 반환

inputbox 2020. 10. 8. 07:59
반응형

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]>> {

메모

  • getSwift에서 함수 접두사를 사용하지 않는 것이 좋습니다 . 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 가지가 있습니다.

  1. 폐쇄 / 완료 핸들러

  2. 대표자

  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

반응형