itsource

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

mycopycode 2023. 10. 9. 22:33
반응형

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

저는 스위프트 프로젝트에서 REST 요청과 응답을 모두 처리하는 유틸리티 클래스를 만들었습니다.간단한 REST API를 구축하여 코드를 테스트할 수 있습니다.NSray를 반환해야 하는 클래스 메소드를 만들었지만 API 호출이 비동기이기 때문에 비동기 호출 내부 메소드에서 반환해야 합니다.문제는 비동기가 공백을 반환한다는 것입니다.노드에서 이 작업을 수행하는 경우 JS 약속을 사용할 것이지만 스위프트에서 작동하는 솔루션을 찾을 수 없습니다.

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)")     
    }
}

에 Swift 5.5(iOS 15, macOS 12)를입니다.async-await패턴:

func fetchGenres() async throws -> [Genre] {
    …
    let (data, _) = try await URLSession.shared.dataTask(for: request)
    return try JSONDecoder().decode([Genre].self, from: data)
}

우리는 이것을 이렇게 부릅니다.

let genres = try await fetchGenres()

async-await구문은 아래의 제 원래 답변에서 설명된 전통적인 완성 처리기 패턴보다 훨씬 간결하고 자연스럽습니다.

자세한 내용은 Swift에서 비동기/대기 충족을 참조하십시오.


역사적 패턴은 완료 핸들러 클로징을 사용하는 것입니다.

예를 들어, 우리는 종종 다음을 사용합니다.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 .failure(let error):
        print(error.localizedDescription)

    case .success(let genres):
        // use `genres` here, e.g. update model and UI            
    }
}

// but don’t try to use `genres` here, as the above runs asynchronously

참고로 위에서 모델과 UI 업데이트를 단순화하기 위해 완료 핸들러를 메인 큐로 다시 보냅니다.은 이 합니다 합니다.URLSession사용되거나 자체 큐를 사용합니다(결과를 수동으로 동기화하려면 호출자를 requiring하십시오).

하지만 그건 중요한 게 아닙니다.핵심 문제는 완료 핸들러를 사용하여 비동기 요청이 수행될 때 실행할 코드 블록을 지정하는 것입니다.


에서 저는 했습니다, 로의 했습니다.NSArray(이러한 브리지형 Objective-C 유형은 더 이상 사용하지 않습니다.)제 생각엔 우리가 한 일이Genre한 것으로 됩니다.JSONDecoder에,JSONSerialization그러나 은 기본 에 대한 하지 않아하는 것을 하기 위해 그러나 이 질문은 기본 JSON에 대한 정보가 충분하지 않아서 핵심 문제인 완료 처리기로 폐쇄를 사용하는 것을 피하기 위해 생략했습니다.

스위프트즈는 이미 약속의 기본 구성 요소인 미래를 제공하고 있습니다.미래는 실패할 수 없는 약속입니다(여기서 모든 용어는 스칼라 해석에 기반을 두고 있으며, 여기서 약속은 모나드입니다).

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

궁극적으로 완전한 Scala 스타일의 Promise로 확장되기를 바랍니다. (어느 시점에서는 제가 직접 작성할 수도 있고, 다른 PR들도 환영할 것입니다. Future가 이미 구축되어 있기 때문에 그렇게 어렵지 않습니다.)

아마 .Result<[Book]>(Alexandros Salazar 버전을 기준으로 함.그러면 메소드 서명은 다음과 같습니다.

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

메모들

  • 다음으로 함수를 접두사로 지정하는 것은 권장하지 않습니다.get스위프트에서특정한 것입니다.그것은 ObjC와의 특정한 종류의 상호운용성을 깨뜨릴 것입니다.
  • 는 까지 파싱하는 을 추천합니다.Book에 를 선택합니다.Future이 은 몇 할 수 , 에 이 합니다.Future 중입니다.[Book]다를 의 나머지 이 훨씬 .NSArray.

스위프트 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
}

스위프트 3 버전 @Alexey Globchasty 답변:

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.5, 비동기식/대기 기반 솔루션

원래 포스터에서 제공한 원래 테스트 URL이 더 이상 작동하지 않아 변경해야 했습니다.이 솔루션은 제가 발견한 농담 API를 기반으로 합니다.당 API하지만 String합니다 ([String].), 합니다.


class Bookshop {
    class func getGenres() async -> [String] {
        print("Hello inside getGenres")
        let urlPath = "https://geek-jokes.sameerkumar.website/api?format=json"
        print(urlPath)
        let url = URL(string: urlPath)!
        let session = URLSession.shared
        
        typealias Continuation = CheckedContinuation<[String], Never>
        let genres = await withCheckedContinuation { (continuation: Continuation) in
            let task = session.dataTask(with: url) { data, response, error in
                print("Task completed")
                
                var result: [String] = []
                defer {
                    continuation.resume(returning: result)
                }
                
                if let error = error {
                    print(error.localizedDescription)
                    return
                }
                guard let data = data else { 
                    return
                }
                
                do {
                    let jsonResult = try JSONSerialization.jsonObject(with: data, options: [.mutableContainers])
                    print("jsonResult is \(jsonResult)")
                    if let joke = (jsonResult as? [String: String])?["joke"] {
                        result = [joke]
                    }
                } catch {
                    print("JSON Error \(error.localizedDescription)")
                    print("data was \(String(describing: String(data: data, encoding: .utf8)))")
                    return
                }
            }
            task.resume()
        }
        
        return genres
    }
}

async {
    let final = await Bookshop.getGenres()
    print("Final is \(final)")
}

withCheckedContinuationasync함수는 실제로 별도의 작업/thread에서 실행됩니다.

아직도 이 일에 매달리지 않았으면 좋겠는데, 단답형은 스위프트에서는 이 일을 할 수 없다는 것입니다.

필요한 데이터가 준비되는 대로 콜백을 반환하는 방법도 있습니다.

콜백 기능을 만드는 방법에는 3가지가 있습니다. 즉, 1.완료 핸들러 2.알림 3.대의원

완료 핸들러 블록 내부 세트는 소스가 사용 가능할 때 실행되어 반환됩니다. 핸들러는 응답이 올 때까지 기다렸다가 UI를 업데이트할 수 있습니다.

알림 앱 전체에서 많은 정보가 트리거되며 리스트너는 해당 정보를 사용하여 n을 검색할 수 있습니다.프로젝트 전반에 걸쳐 정보를 얻는 비동기식 방법.

딜러 호출 시 딜러 메소드 세트가 트리거됩니다. 메소드 자체를 통해 소스를 제공해야 합니다.

스위프트 5.5:

TL;DR: Swift 5.5는 아직 출시되지 않았습니다(작성 당시).swift 5.5를 사용하려면 swift toolchain development snapshot을 여기서 다운로드하고 컴파일러 플래그를 추가하면 됩니다.-Xfrontend -enable-experimental-concurrency. 여기서 자세히 보기

는 를 할 수 .async/await특징.

로서의 .async다 에서 수술을 .withUnsafeThrowingContinuation다음과 같은 블록.

class Bookshop {
  class func getGenres() async throws -> NSArray {
    print("Hello inside getGenres")
    let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
    print(urlPath)
    let url = URL(string: urlPath)!
    let session = URLSession.shared
    return try await withUnsafeThrowingContinuation { continuation in
      let task = session.dataTask(with: url, completionHandler: {data, response, error -> Void in
        print("Task completed")
        if(error != nil) {
          print(error!.localizedDescription)
          continuation.resume(throwing: error!)
          return
        }
        do {
          let jsonResult = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any]
          let results: NSArray = jsonResult!["genres"] as! NSArray
          continuation.resume(returning: results)
        } catch {
          continuation.resume(throwing: error)
        }
      })
      task.resume()
    }
  }
}

그리고 이 기능을 이렇게 부를 수 있습니다.

@asyncHandler
func check() {
  do {
    let genres = try await Bookshop.getGenres()
    print("Result: \(genres)")
  } catch {
    print("Error: \(error)")
  }
}

전화를 걸 때는 이 사실을 명심해야 합니다.Bookshop.getGenres, method는 method, caller method는다이어야 asyncs로 됩니다.@asyncHandler

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. 폐쇄/완료 핸들러

  2. 대의원

  3. 알림

관찰자는 비동기 작업이 완료된 후 알림을 받는 데 사용할 수도 있습니다.

프로토콜 중심의 API 클라이언트를 구현하는 모든 우수한 API Manager가 충족하기를 원하는 몇 가지 매우 일반적인 요구 사항이 있습니다.

AP 클라이언트 초기 인터페이스

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

반응형