将 URLSession 委托设置为另一个 Swift Class

Set URLSession Delegate To Another Swift Class

我正在尝试调用 API 来登录网站。我目前的所有 API 通话都在一个名为 API 的 swift class 通话中。我用来登录的视图控制器称为 CreateAccountViewController

在我的 API 登录调用中,我创建了一个 URL 会话并像这样设置委托:

let task = URLSession.init(configuration: URLSessionConfiguration.default, delegate: CreateAccountViewController.init(), delegateQueue: nil)

task.dataTask(with: request).resume()

然后在我的VCclass我有这个功能

func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError: Error?) {
        //Check the data returned from API call, ensure user is logged in
    }

此函数在 API 完成时被调用,但我觉得我在创建 URL 会话时在委托声明中使用 .init 导致内存泄漏或其他问题.有一个更好的方法吗?

此外,如何访问 API 调用中的数据?在完成处理程序中有一个我可以获得的数据响应,但在这个委托调用中没有。

最好不要通过在 URL 会话案例中委派其他 class 来完成此操作... return 完成处理程序中的数据并在您的 [=17= 中访问它]

对于数据你可以使用其他方法

func urlSession(_ session: URLSession, 
                dataTask: URLSessionDataTask, 
              didReceive data: Data)

在这里你会得到接收到的数据

是的,从技术上讲,您可以让一个单独的对象作为会话的委托。但是为此实例化视图控制器没有多大意义,原因如下:

  1. 您的代码正在创建一个视图控制器实例作为委托对象,但是您将其交给了 URLSession 而没有保留对它的引用。因此,没有办法将其添加到视图控制器层次结构中(例如,呈现它、推送它、对其执行 segue 等)。

    当然,您可能会在其他地方展示此视图控制器的另一个实例,但那将是一个完全独立的实例,与您刚刚在此处创建的实例没有任何联系。您最终会得到两个单独的 CreateAccountViewController 对象。

  2. 从架构的角度来看,许多人会争辩说网络委托代码并不真正属于视图控制器。视图控制器用于填充视图和响应用户事件,而不是用于网络代码。

因此,简而言之,虽然您在技术上可以让您的 API 经理 class 使用单独的对象进行委托调用,但这有点不寻常。如果你这样做了,你肯定不会为此创建一个 UIViewController subclass。

一个更常见的模式(如果您完全使用委托模式)可能是让 API 经理本身成为其 URLSession 的委托。 (在混合中添加一个单独的专用委托对象可能只会使情况复杂化。)但是通过将所有这些特定于网络的代码保留在视图控制器之外,您将视图控制器从解析网络响应的血淋淋的细节中抽象出来,处理所有各种委托方法等


所有这些都引出了一个问题:您真的需要使用基于委托的 API 吗?在极少数情况下,您需要丰富的委托 API(处理自定义质询响应等),但在大多数情况下,dataTask 的简单完成处理程序再现要容易得多。

为您的 API 方法提供一个完成处理程序闭包,以便调用者可以指定如果网络请求成功应该发生什么。您可以使用基于委托的会话来做到这一点,但它要复杂得多,我们通常只会在绝对必要时才去那个兔子洞,这里不是这种情况。

所以一个常见的模式是给你的 API 经理(我假设他是一个单身人士)一个 login 方法,就像这样:

/// Perform login request
///
/// - Parameters:
///   - userid: Userid string.
///   - password: Password string
///   - completion: Calls with `.success(true)` if successful. Calls `.failure(Error)` on error.
///
/// - Returns: The `URLSessionTask` of the network request (in case caller wants to cancel request).

@discardableResult
func login(userid: String, password: String, completion: @escaping (Result<Bool, Error>) -> Void) -> URLSessionTask {
    let request = ... // Build your `URLRequest` here

    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard
            error == nil,
            let responseData = data,
            let httpResponse = response as? HTTPURLResponse,
            200 ..< 300 ~= httpResponse.statusCode
        else {
            DispatchQueue.main.async { completion(.failure(error ?? APIManagerError.invalidResponse(data, response))) }
            return
        }

        // parse `responseData` here

        let success = true

        DispatchQueue.main.async {
            if success {
                completion(.success(true))
            } else {
                completion(.failure(error))
            }
        }
    }
    task.resume()
    return task
}

您可能会遇到自定义错误 class,如下所示:

enum APIManagerError: Error {
    case invalidResponse(Data?, URLResponse?)
    case loginFailed(String)
}

你会这样称呼它:

APIManager.shared.login(userid: userid, password: password) { result in
    switch result {
    case .failure(let error):
        // update UI to reflect error
        print(error)

    case .success:
        // do whatever you want if the login was successful
    }
}

下面是一个更完整的示例,其中我将网络代码分解了一点(一个用于执行网络请求,一个用于解析 JSON 的通用方法,一个用于解析 JSON与登录关联),但思路还是一样的。当您执行异步方法时,为该方法提供一个 @escaping 完成处理程序闭包,该闭包将在异步任务完成时调用。

final class APIManager {
    static let shared = APIManager()

    private var session: URLSession

    private init() {
        session = .shared
    }

    let baseURLString = "https://example.com"

    enum APIManagerError: Error {
        case invalidResponse(Data?, URLResponse?)
        case loginFailed(String)
    }

    /// Perform network request with `Data` response.
    ///
    /// - Parameters:
    ///   - request: The `URLRequest` to perform.
    ///   - completion: Calls with `.success(Data)` if successful. Calls `.failure(Error)` on error.
    ///
    /// - Returns: The `URLSessionTask` of the network request (in case caller wants to cancel request).

    @discardableResult
    func perform(_ request: URLRequest, completion: @escaping (Result<Data, Error>) -> Void) -> URLSessionTask {
        let task = session.dataTask(with: request) { data, response, error in
            guard
                error == nil,
                let responseData = data,
                let httpResponse = response as? HTTPURLResponse,
                200 ..< 300 ~= httpResponse.statusCode
            else {
                completion(.failure(error ?? APIManagerError.invalidResponse(data, response)))
                return
            }

            completion(.success(responseData))
        }

        task.resume()
        return task
    }

    /// Perform network request with JSON response.
    ///
    /// - Parameters:
    ///   - request: The `URLRequest` to perform.
    ///   - completion: Calls with `.success(Data)` if successful. Calls `.failure(Error)` on error.
    ///
    /// - Returns: The `URLSessionTask` of the network request (in case caller wants to cancel request).

    @discardableResult
    func performJSON<T: Decodable>(_ request: URLRequest, of type: T.Type, completion: @escaping (Result<T, Error>) -> Void) -> URLSessionTask {
        return perform(request) { result in
            switch result {
            case .failure(let error):
                completion(.failure(error))

            case .success(let data):
                do {
                    let responseObject = try JSONDecoder().decode(T.self, from: data)
                    completion(.success(responseObject))
                } catch let parseError {
                    completion(.failure(parseError))
                }
            }
        }
    }

    /// Perform login request
    ///
    /// - Parameters:
    ///   - userid: Userid string.
    ///   - password: Password string
    ///   - completion: Calls with `.success()` if successful. Calls `.failure(Error)` on error.
    ///
    /// - Returns: The `URLSessionTask` of the network request (in case caller wants to cancel request).

    @discardableResult
    func login(userid: String, password: String, completion: @escaping (Result<Bool, Error>) -> Void) -> URLSessionTask {
        struct ResponseObject: Decodable {
            let success: Bool
            let message: String?
        }

        let request = prepareLoginRequest(userid: userid, password: password)

        return performJSON(request, of: ResponseObject.self) { result in
            switch result {
            case .failure(let error):
                completion(.failure(error))

            case .success(let responseObject):
                if responseObject.success {
                    completion(.success(true))
                } else {
                    completion(.failure(APIManagerError.loginFailed(responseObject.message ?? "Unknown error")))
                }
                print(responseObject)
            }
        }
    }

    private func prepareLoginRequest(userid: String, password: String) -> URLRequest {
        var components = URLComponents(string: baseURLString)!
        components.query = "login"
        components.queryItems = [
            URLQueryItem(name: "userid", value: userid),
            URLQueryItem(name: "password", value: password)
        ]

        var request = URLRequest(url: components.url!)
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")

        return request
    }
}