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


93

모든 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...
    }
}

5
이 실수는 Stack Overflow에서 너무나
matt

답변:


97

콜백을 전달하고 비동기 호출 내에서 콜백을 호출 할 수 있습니다.

같은 것 :

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

감사합니다. 내 마지막 질문은 내 뷰 컨트롤러에서이 클래스 메서드를 호출하는 방법입니다. 코드는 현재 다음과 같습니다.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) }
Mark Tyers 2014-08-08

13

Swiftz는 이미 Promise의 기본 구성 요소 인 Future를 제공합니다. 퓨처는 실패 할 수없는 약속입니다 (여기서 모든 용어는 스칼라 해석을 기반으로 하며 약속은 모나드입니다 ).

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

결국 완전한 스칼라 스타일의 약속으로 확장되기를 바랍니다.

귀하의 특별한 경우에는 아마도 Result<[Book]>( Alexandros Salazar의 버전을Result 기반으로) 만들 것입니다 . 그러면 메서드 서명은 다음과 같습니다.

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

메모

  • getSwift에서 함수 접두사를 사용하지 않는 것이 좋습니다 . ObjC와의 특정 종류의 상호 운용성을 깨뜨릴 것입니다.
  • Book결과를 .txt 파일로 반환하기 전에 객체 까지 파싱하는 것이 좋습니다 Future. 이 시스템이 실패 할 수있는 몇 가지 방법이 있으며, .NET Framework로 정리하기 전에 이러한 모든 사항을 확인하는 것이 훨씬 더 편리합니다 Future. 를 [Book]처리하는 것보다 Swift 코드의 나머지 부분에서 NSArray.

4
Swiftz는 더 이상 Future. 하지만 github.com/mxcl/PromiseKit 에서 Swiftz 와 잘 작동합니다!
badeleux

당신은 스위프트를 작성하지 않은 실현하기 위해 나에게 몇 초를 가져다 스위프트 쓴 Z

4
"Swiftz"는 Swift 용 타사 기능 라이브러리 인 것처럼 들립니다. 귀하의 답변은 해당 라이브러리를 기반으로하는 것 같으므로 명시 적으로 명시해야합니다. (예 : "Futures와 같은 기능적 구성을 지원하는 'Swiftz'라는 타사 라이브러리가 있으며 Promise를 구현하려는 경우 좋은 시작점 역할을해야합니다.") 그렇지 않으면 독자들은 왜 철자가 틀렸는 지 궁금 할 것입니다. " 빠른".
Duncan C

3
있습니다 github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift가 더 이상 작동하지 않습니다.
Ahmad F

1
@Rob get접두사는 ObjC의 참조에 의한 반환을 나타냅니다 (예 : in -[UIColor getRed:green:blue:alpha:]). 내가 이것을 썼을 때 나는 수입업자가 (예를 들어 자동으로 튜플을 반환하기 위해) 그 사실을 활용할 것이라고 걱정했습니다. 그들은 그렇지 않은 것으로 밝혀졌습니다. 이 글을 썼을 때 나는 아마도 KVC가 접근 자에 대한 "get"접두사를 지원한다는 사실을 잊었을 것입니다. (이것은 제가 여러 번 배우고 잊어 버린 것입니다). 그래서 동의했습니다. 나는 선두 get가 일을 깨뜨리는 경우를 겪지 않았습니다 . ObjC "get"의 의미를 아는 사람들에게 오해의 소지가 있습니다.
Rob Napier

9

기본 패턴은 완료 핸들러 클로저를 사용하는 것입니다.

예를 들어, 곧 나올 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에 대한 정보가 충분하지 않았기 때문에 핵심 문제인 클로저를 완료 핸들러로 사용하는 것을 피하기 위해 생략했습니다.


ResultSwift 4 이하에서도 사용할 수 있지만 열거 형을 직접 선언해야합니다. 저는 이런 종류의 패턴을 수년간 사용하고 있습니다.
vadian

예, 물론 저도 그렇습니다.하지만 스위프트 5가 출시되면서 애플이 받아 들인 것처럼 보입니다. 파티에 늦었습니다.
Rob

7

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

2

@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()
}

2

나는 당신이 여전히 이것에 집착하지 않기를 바라지 만, 짧은 대답은 당신이 Swift에서 이것을 할 수 없다는 것입니다.

다른 방법은 필요한 데이터가 준비되는 즉시 제공하는 콜백을 반환하는 것입니다.


1
그는 또한 신속하게 약속을 할 수 있습니다. 그러나 Apple의 현재 권장되는 aproceh는 s callback와 함께 closure사용하거나 delegation이전 코코아 API처럼 사용하고 있습니다
Mojtaba Hosseini

약속에 대해 맞습니다. 그러나 Swift는이를위한 네이티브 API를 제공하지 않으므로 PromiseKit 또는 다른 대안을 사용해야합니다.
LironXYZ

1

콜백 함수를 생성하는 방법은 3 가지가 있습니다. 1. 완료 핸들러 2. 알림 3. 델리게이트

Completion Handler 내부 블록 집합이 실행되고 소스가 사용 가능할 때 반환되며, Handler는 응답이 올 때까지 대기하여 UI가 업데이트 될 수 있도록합니다.

알림 정보 묶음은 모든 앱에서 트리거되며 Listner는 해당 정보를 사용하여 검색 할 수 있습니다. 프로젝트를 통해 정보를 얻는 비동기 방식.

델리게이트 메소드 집합은 델리게이트가 호출 될 때 트리거되며 소스는 메소드 자체를 통해 제공되어야합니다.


-1
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

신속하게 콜백을 달성하는 방법은 크게 3 가지가 있습니다.

  1. 폐쇄 / 완료 핸들러

  2. 대표자

  3. 알림

비동기 작업이 완료되면 관찰자를 사용하여 알림을받을 수도 있습니다.


-2

모든 좋은 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)
        }
    }

}

-2

이것은 도움이 될 수있는 작은 사용 사례입니다.

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)")
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.