Background Tasks

You learned in Chapter 20 that blocking the main queue can lead to an unresponsive application, so it is often a good idea to do expensive operations using a background task. The viewContext of NSPersistentContainer is associated with the main queue, so long-running operations using the viewContext – like the insertion of photos from the web service – are good candidates for a background task.

You are going to update processPhotosRequest(data:error:) to use a background task. Background tasks are an asynchronous operation, so instead of returning a Result synchronously from your method, you will pass a completion handler to be called asynchronously.

Open PhotoStore.swift and update processPhotosRequest(data:error:) to take in a completion handler. You will have some errors in the code due to the signature change; you will fix these shortly.

Listing 23.13  Making the processing asynchronous (PhotoStore.swift)

private func processPhotosRequest(data: Data?,
                                  error: Error?) -> Result<[Photo], Error> {
private func processPhotosRequest(data: Data?,
                          error: Error?,
                          completion: @escaping (Result<[Photo], Error>) -> Void) {
    guard let jsonData = data else {
        return .failure(error!)
    }
    ...

With the completion parameter in place, you will need to replace the return statements with a call to the completion handler instead.

Listing 23.14  Calling the completion handler (PhotoStore.swift)

private func processPhotosRequest(data: Data?,
                          error: Error?,
                          completion: @escaping (Result<[Photo], Error>) -> Void) {
    guard let jsonData = data else {
        return .failure(error!)
        completion(.failure(error!))
        return
    }

    let context = persistentContainer.viewContext

    switch FlickrAPI.photos(fromJSON: jsonData) {
    case let .success(flickrPhotos):
        let photos = flickrPhotos.map { flickrPhoto -> Photo in
            ...
        }
        return .success(photos)
        completion(.success(photos))
    case let .failure(error):
        return .failure(error)
        completion(.failure(error))
    }
}

Notice the use of return within the guard statement. Recall that with a guard statement, you must exit scope. The scope of the guard statement is the function itself, so you must exit the scope of the function somehow. This is a fantastic benefit to using a guard statement. The compiler will enforce this requirement, so you can be certain that no code below the guard statement will be executed.

Now you can add in the code for the background task. NSPersistentContainer has a method to perform a background task. This method takes in a closure to call, and the closure vends a new NSManagedObjectContext to use.

Update processPhotosRequest(data:error:completion:) to kick off a new background task. (Do not neglect to add the new closing brace.)

Listing 23.15  Starting a new background task (PhotoStore.swift)

private func processPhotosRequest(data: Data?,
                          error: Error?,
                          completion: @escaping (Result<[Photo], Error>) -> Void) {
    guard let jsonData = data else {
        completion(.failure(error!))
        return
    }

    let context = persistentContainer.viewContext

    persistentContainer.performBackgroundTask {
        (context) in

        switch FlickrAPI.photos(fromJSON: jsonData) {
        case let .success(flickrPhotos):
            let photos = flickrPhotos.map { flickrPhoto -> Photo in
                ...
            }
            completion(.success(photos))
        case let .failure(error):
            completion(.failure(error))
        }
    }
}

For the insertions to persist and be available to other managed object contexts, you need to save the changes to the background context.

Update the background task in processPhotosRequest(data:error:completion:) to do this.

Listing 23.16  Importing photos in a background task (PhotoStore.swift)

persistentContainer.performBackgroundTask {
    (context) in

    switch FlickrAPI.photos(fromJSON: jsonData) {
    case let .success(flickrPhotos):
        let photos = flickrPhotos.map { flickrPhoto -> Photo in
            ...
        }
        do {
            try context.save()
        } catch {
            print("Error saving to Core Data: (error).")
            completion(.failure(error))
            return
        }
        completion(.success(photos))
    case let .failure(error):
        completion(.failure(error))
    }
}

Here is where things change a bit. An NSManagedObject should only be accessed from the context that it is associated with. After the expensive operation of inserting the Photo instances and saving the context, you will want to fetch the same photos – but only those that are associated with the viewContext (that is, the photos associated with the main queue).

Each NSManagedObject has an objectID that is the same across different contexts. You will use this objectID to fetch the corresponding Photo instances associated with the viewContext.

Update processPhotosRequest(data:error:completion:) to get the Photo instances associated with the viewContext and pass them back to the caller via the completion handler.

Listing 23.17  Fetching the main queue photos (PhotoStore.swift)

persistentContainer.performBackgroundTask {
    (context) in

    switch FlickrAPI.photos(fromJSON: jsonData) {
    case let .success(flickrPhotos):
        let photos = flickrPhotos.map { flickrPhoto -> Photo in
            ...
        }
        do {
            try context.save()
        } catch {
            print("Error saving to Core Data: (error).")
            completion(.failure(error))
            return
        }
        completion(.success(photos))

        let photoIDs = photos.map { $0.objectID }
        let viewContext = self.persistentContainer.viewContext
        let viewContextPhotos =
            photoIDs.map { viewContext.object(with: $0) } as! [Photo]
        completion(.success(viewContextPhotos))
    case let .failure(error):
        completion(.failure(error))
    }
}

First, you get an array of all the objectIDs associated with the Photo instances. This will be an array of NSManagedObjectID instances. Within the closure, $0 is of type Photo.

Then you create a local variable to reference the viewContext. Next, you map over the photoIDs. Within the closure, $0 is of type NSManagedObjectID. You use this managed object ID to ask the viewContext for the object associated with a specific object identifier.

The method object(with:) returns an NSManagedObject, so the result of the entire map operation will be an array of NSManagedObject instances. You know that the instances being returned will be of type Photo, so you downcast the array of NSManagedObject instances into an array of Photo instances.

The map method is a useful tool for the common operation of converting one array into another array.

The final change you need to make is to update fetchInterestingPhotos(completion:) to use the updated processPhotosRequest(data:error:completion:) method.

Listing 23.18  Using the asynchronous process method (PhotoStore.swift)

func fetchInterestingPhotos(completion: @escaping (Result<[Photo], Error>) -> Void) {

    let url = FlickrAPI.interestingPhotosURL
    let request = URLRequest(url: url)
    let task = session.dataTask(with: request) {
        (data, response, error) in

        var result = self.processPhotosRequest(data: data, error: error)

        if case .success = result {
            do {
                try self.persistentContainer.viewContext.save()
            } catch let error {
                result = .failure(error)
            }
        }

        OperationQueue.main.addOperation {
            completion(result)
        }

        self.processPhotosRequest(data: data, error: error) {
            (result) in

            OperationQueue.main.addOperation {
                completion(result)
            }
        }
    }
    task.resume()
}

Build and run the application. Although the behavior has not changed, the application is no longer in danger of becoming unresponsive while new photos are being added. As the scale of your applications increases, handling Core Data entities somewhere other than the main queue, as you have done here, can result in huge performance wins.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.28.197