2

I have an application that requires downloading large amount of data when the user logs in. I wanted to move the download portion of it to a background thread so the user can navigate the app without having to wait for the download to complete. I have tried the following methods but some of them still locks the app so user cant click on anything,

dispatch_async(dispatch_get_main_queue(), ^{

});

Have also tried

[self performSelectorInBackground:@selector(loadDataThatToBeFetchedInThread:) 
                      withObject:objectArrayThatNeedToFetchData];

this one seems to just stop if I move between activity. Have tried moving it to the AppDelegate method but when I try to save to SQlite DB i get some error. Am i doing something wrong? Can some one please help.

Thanks in advance

dogwasstar
  • 852
  • 3
  • 16
  • 31

3 Answers3

2

Well, dispatch_get_main_queue() is going to give you the main thread, so that's probably not what you want.

Instead, you should obtain a background queue using:

dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ ... });

And then, it's customary to either send out some notification, or even call back to the main thread directly to (in the UI) report success:

dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    // Do the download...
    // Download finishes...
    dispatch_async(dispatch_get_main_queue(), ^{
        // Call a UI-updating method, or similar
    });
});
Craig Otis
  • 31,257
  • 32
  • 136
  • 234
2

Look up NSURLSession and NSURLSessionDownloadTask. This is the latest and greatest from Apple.

Watch the Core Networking videos (What's New in Core Networking) from the 2015 WWDC videos and 2014 WWDC videos.

URL Session Programming Guide is also a good resource.

NSURLSession is asynchronous out of the box — which is what you're looking for.

As a bonus NSURLSessionDownloadTask makes it easy to continue the download when you app changes to background state (which is much different than a background thread). It also allows you to easily cancel and/or resume a download.

rosem
  • 1,311
  • 14
  • 19
  • +1 for mentioning the `NSURLSessionDownloadTask` class. While a different strategy than what OP is trying, it's almost certainly the better solution. – Craig Otis Oct 07 '15 at 01:32
1

I'd recommend using NSOperation and NSOperationQueue to keep it nice and clean.

Read & Watch more:

Here's a basic setup that you can customise to fit your needs

Disclaimer: although it seems like a lot, it makes up for a nicer API.

First, let's define an interface to handle our API endpoints:

// Endpoints.swift

let api_base = "https://myserver.com/"
let api_path = "api/"

protocol EndpointGenerator {
    func URL() -> NSURL
}

extension EndpointGenerator {
    func URL() -> NSURL {
        return NSURL(string: api_base)!
    }
}

// Represents a null endpoint. It will fail.
struct NullEndpoint: EndpointGenerator { }

enum Endpoint: String, EndpointGenerator {
    case Login = "login"
    case SignUp = "signup"

    func URL() -> NSURL {
        return NSURL(string: api_base + api_path + self.rawValue)!
    }
}

Next, let's build our custom NSOperation:

// Operation.swift
public class Operation: NSOperation {
    public typealias Completion = Operation -> ()
    public typealias Error = NSError -> ()

    var endpoint: EndpointGenerator {
        return NullEndpoint()
    }

    var headerParams: [String:String]? {
        return nil
    }

    var requestBody: [String:AnyObject]? {
        return nil
    }

    var method: HTTPMethod {
        return .GET
    }

    var networkTask: NSURLSessionTask?

    var completion: Completion?
    var error: Error?
    public var parsedObject = [String:AnyObject]()

    override public init() { }

    public init(completion: Completion, error: Error) {
        self.completion = completion
        self.error = error
    }

    override public func start() {
        NSURLSessionImplementaion.execute(self)
    }

    override public func cancel() {
        networkTask?.cancel()
        networkTask = nil
    }
}

To be almost done, let's handle the actual queue:

// OperationQueue.swift
public class OperationQueue: NSOperationQueue {
        public static let internalQueue = OperationQueue()

        public static func addOperation(operation: NSOperation) {
            internalQueue.addOperation(operation)
        }

        public static func addOperations(operations: NSOperation...) {
            for operation in operations {
                addOperation(operation)
            }
        }

        public static func cancellAllOperations() {
            internalQueue.cancelAllOperations()
        }
}

Finally, the download part:

// NSURLSessionImplementation.swift
enum HTTPMethod: String {
    case POST = "POST"
    case GET = "GET"
    case PATCH = "PATCH"
}

public let OSNetworkingErrorDomain = "com.swanros.errordomain"

class NSURLSessionImplementaion {
    class func execute(operation: Operation) {
        let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
        let request = NSMutableURLRequest(URL: operation.endpoint.URL())
        if let headerParams = operation.headerParams {
            for element in headerParams {
                request.setValue(element.1, forHTTPHeaderField: element.0)
            }
        }

        if let body = operation.requestBody {
            do {
                request.HTTPBody = try NSJSONSerialization.dataWithJSONObject(body, options: .PrettyPrinted)
            } catch {
                return
            }
        }

        request.HTTPMethod = operation.method.rawValue

        let task = session.dataTaskWithRequest(request) { data, response, error in
            if let e = error {
                operation.error?(e)
                return
            }

            guard let d = data else {
                operation.error?(errorWithDescription("No data"))
                return
            }

            do {
                let json = try NSJSONSerialization.JSONObjectWithData(d, options: .MutableLeaves) as? [String:AnyObject]
                guard let j = json else {
                    operation.error?(errorWithDescription("Error parsing JSON."))
                    return
                }

                if let errorMessage = string(j, key: "error") {
                    operation.error?(errorWithDescription(errorMessage))
                    return
                }

                operation.parsedObject = j
                operation.completion?(operation)
            } catch let jsonError as NSError {
                operation.error?(jsonError)
            }
        }

        operation.networkTask = task
        task.resume()
    }
}

func errorWithDescription(desc: String) -> NSError {
    return NSError(domain: OSNetworkingErrorDomain, code: 0, userInfo: [NSLocalizedDescriptionKey:desc])
}

How do you implement this? Say you want to hit the /login endpoint. Subclass Operation as follows:

// LogInOperation.swift
public class LogInOperation: Operation {
    override var endpoint: EndpointGenerator {
        // A nice way to represent endpoints: use enums and protocols!
        return Endpoint.Login
    }

    // The headers for this particular request. Maybe you need a token here!
    override var headerParams: [String:String]? {
        return [
            "Content-Type": "application/json",
            "Application-Id": "bAAvLosWNeSTHrlYilysdeEYoJHUXs88"
        ]
    }

    // The HTTP request body!
    override var requestBody: [String:AnyObject]? {
        return [
            "mail": mail,
            "password": password
        ]
    }

    // .GET is default
    override var method: HTTPMethod {
        return .POST
    }

    private var mail: String
    private var password: String

    public init(mail m: String, password p: String, completion: Completion, error: Error) {
        mail = m
        password = p

        super.init(completion: completion, error: error)
    }
}

And you use it like this:

// ViewController.swift

let loginOperation = LogInOperation(
                         mail: "mail@example.com", 
                         password: "123123", 
                         completion: { op in
                             // parsedObject would be the user's info 
                             print(op.parsedObject?)
                         }, error: { error in 
                             print(error.localizedDescription)
                         }
                     )
OperationQueue.addOperation(loginOperation)
Oscar Swanros
  • 19,767
  • 5
  • 32
  • 48