swifturlsession

Set URLSession Delegate to another Swift class


I am attempting to call an API to log in to a web site. I currently have all my API calls in a Swift class called APICalls. My view controller I'm using to log in with is called CreateAccountViewController.

In my API call to log in I create a URL session and set the delegate like this:

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

Then in my VC class I have this function

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

This function is being called when the API is done, but I feel like I'm causing a memory leak or something by using .init in the delegate declaration when creating my URL session. Is there a better way to do this?

Also, how do I access the data from the API call? In completion handlers there's a data response I can get at, but not in this delegate call.


Solution

  • Yes, you technically can have a separate object be the delegate for the session. But it doesn’t make much sense to instantiate a view controller for this, for a few reasons:

    1. Your code is creating a view controller instance as the delegate object, but you’re handing this off to the URLSession without keeping a reference to it. Thus, there’s no way to add this to the view controller hierarchy (e.g. to present it, to push to it, perform a segue to it, whatever).

      Sure, you might be presenting another instance of this view controller elsewhere, but that will be a completely separate instance, with no connection to the one you just created here. You’d end up with two separate CreateAccountViewController objects.

    2. From an architectural perspective, many would argue that network delegate code doesn’t really belong in view controllers, anyway. View controllers are for populating views and responding to user events, not for network code.

    So, in short, while you technically can have your API manager class use a separate object for the delegate calls, that’s a bit unusual. And if you did do that, you certainly wouldn’t create a UIViewController subclass for that.

    A more common pattern (if you use the delegate pattern at all) might be to make the API manager, itself, the delegate for its URLSession. (Adding a separate dedicate delegate object in the mix probably only complicates the situation.) But by keeping all of this network-specific code out of the view controllers, you abstract your view controllers away from the gory details of parsing network responses, handling all of the various delegate methods, etc.


    All of this begs the question: Do you really need to use the delegate-based API? It’s critical in those rare cases where you need the rich delegate API (handling custom challenge responses, etc.), but in most cases, the simple completion handler rendition of dataTask is much easier.

    Give your API method a completion handler closure, so that the caller can specify what should happen if the network request succeeds. You can do this with delegate based sessions, but it’s a lot more complicated and we’d generally only go down that rabbit hole if absolutely necessary, which is not the case here.

    So a common pattern would be to give your API manager (which I’ll assume is a singleton) a login method, like so:

    /// 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
    }
    

    Where you might have a custom error class like so:

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

    And you’d call it like so:

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

    Below is a more complete example, where I’ve broken up the network code down a bit (one to perform network requests, one generic method for parsing JSON, one specific method to parse the JSON associated with login), but the idea is still the same. When you perform an asynchronous method, give the method an @escaping completion handler closure which is called when the asynchronous task is done.

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