Updating the Data Source

One problem with the app at the moment is that fetchInterestingPhotos(completion:) only returns the newly inserted photos. Now that the application supports saving, it should return all the photos – the previously saved photos as well as the newly inserted ones. You need to ask Core Data for all the Photo entities, and you will accomplish this using a fetch request.

Fetch requests and predicates

To get objects back from the NSManagedObjectContext, you must prepare and execute an NSFetchRequest. After a fetch request is executed, you will get an array of all the objects that match the parameters of that request.

A fetch request needs an entity description that defines which entity you want to get objects from. To fetch Photo instances, you specify the Photo entity. You can also set the request’s sort descriptors to specify the order of the objects in the array. A sort descriptor has a key that maps to an attribute of the entity and a Bool that indicates whether the order should be ascending or descending.

The sortDescriptors property on NSFetchRequest is an array of NSSortDescriptor instances. Why an array? The array is useful if you think there might be collisions when sorting. For example, say you are sorting an array of people by their last names. It is entirely possible that multiple people have the same last name, so you can specify that people with the same last name should be sorted by their first names. This would be implemented by an array of two NSSortDescriptor instances. The first sort descriptor would have a key that maps to the person’s last name, and the second sort descriptor would have a key that maps to the person’s first name.

A predicate is represented by the NSPredicate class and contains a condition that can be true or false. If you wanted to find all photos with a given identifier, you would create a predicate and add it to the fetch request like this:

    let predicate = NSPredicate(format: "(#keyPath(Photo.photoID)) == (identifier)")
    request.predicate = predicate

The format string for a predicate can be very long and complex. Apple’s Predicate Programming Guide is a complete discussion of what is possible.

You want to sort the returned instances of Photo by dateTaken in descending order. To do this, you will instantiate an NSFetchRequest for requesting Photo entities. Then you will give the fetch request an array of NSSortDescriptor instances. For Photorama, this array will contain a single sort descriptor that sorts photos by their dateTaken properties. Finally, you will ask the managed object context to execute this fetch request.

In PhotoStore.swift, implement a method that will fetch the Photo instances from the view context.

Listing 22.6  Implementing a method to fetch all photos from disk (PhotoStore.swift)

func fetchAllPhotos(completion: @escaping (Result<[Photo], Error>) -> Void) {
    let fetchRequest: NSFetchRequest<Photo> = Photo.fetchRequest()
    let sortByDateTaken = NSSortDescriptor(key: #keyPath(Photo.dateTaken),
                                           ascending: true)
    fetchRequest.sortDescriptors = [sortByDateTaken]

    let viewContext = persistentContainer.viewContext
    viewContext.perform {
        do {
            let allPhotos = try viewContext.fetch(fetchRequest)
            completion(.success(allPhotos))
        } catch {
            completion(.failure(error))
        }
    }
}

Next, open PhotosViewController.swift and add a new method that will update the data source with all the photos.

Listing 22.7  Implementing a method to update the data source (PhotosViewController.swift)

private func updateDataSource() {
    store.fetchAllPhotos {
        (photosResult) in

        switch photosResult {
        case let .success(photos):
            self.photoDataSource.photos = photos
        case .failure:
            self.photoDataSource.photos.removeAll()
        }
        self.collectionView.reloadSections(IndexSet(integer: 0))
    }
}

Now update viewDidLoad() to call this method to fetch and display all the photos saved to Core Data.

Listing 22.8  Updating the data source after a web service fetch (PhotosViewController.swift)

override func viewDidLoad()
    super.viewDidLoad()

    collectionView.dataSource = photoDataSource
    collectionView.delegate = self

    store.fetchInterestingPhotos {
        (photosResult) -> Void in

        switch photosResult {
        case let .success(photos):
            print("Successfully found (photos.count) photos.")
            self.photoDataSource.photos = photos
        case let .failure(error):
            print("Error fetching interesting photos: (error)")
            self.photoDataSource.photos.removeAll()
        }
        self.collectionView.reloadSections(IndexSet(integer: 0))

        self.updateDataSource()
    }
}

Previously saved photos will now be returned when the web service request finishes. But there is still one problem: If the application is run multiple times and the same photo is returned from the web service request, it will be inserted into the context multiple times. This is not good – you do not want duplicate photos.

Luckily there is a unique identifier for each photo. When the interesting photos web service request finishes, the identifier for each photo in the incoming JSON data can be compared to the photos stored in Core Data. If one is found with the same identifier, that photo will be returned. Otherwise, a new photo will be inserted into the context.

To do this, you need a way to tell the fetch request that it should not return all photos but instead only the photos that match some specific criteria. In this case, the specific criteria is only photos that have this specific identifier, of which there should either be zero or one photo. In Core Data, this is done with a predicate.

In PhotoStore.swift, update processPhotosRequest(data:error:) to check whether there is an existing photo with a given ID before inserting a new one.

Listing 22.9  Fetching previously saved photos (PhotoStore.swift)

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

    let context = persistentContainer.viewContext

    switch FlickrAPI.photos(fromJSON: jsonData) {
    case let .success(flickrPhotos):
        let photos = flickrPhotos.map { flickrPhoto -> Photo in
            let fetchRequest: NSFetchRequest<Photo> = Photo.fetchRequest()
            let predicate = NSPredicate(
                format: "(#keyPath(Photo.photoID)) == (flickrPhoto.photoID)"
            )
            fetchRequest.predicate = predicate
            var fetchedPhotos: [Photo]?
            context.performAndWait {
                fetchedPhotos = try? fetchRequest.execute()
            }
            if let existingPhoto = fetchedPhotos?.first {
                return existingPhoto
            }

            var photo: Photo!
            context.performAndWait {
                photo = Photo(context: context)
                photo.title = flickrPhoto.title
                photo.photoID = flickrPhoto.photoID
                photo.remoteURL = flickrPhoto.remoteURL
                photo.dateTaken = flickrPhoto.dateTaken
            }
            return photo
        }
        return .success(photos)
    case let .failure(error):
        return .failure(error)
    }
}

Duplicate photos will no longer be inserted into Core Data.

Build and run the application. The photos will appear just as they did before introducing Core Data. Kill the application and launch it again; you will see the photos that Core Data saved in the collection view.

There is one last small problem to address: The user will not see any photos appear in the collection view unless the web service request completes. If the user has slow network access, it might take up to 60 seconds (which is the default timeout interval for the request) to see any photos. It would be best to see the previously saved photos immediately on launch and then refresh the collection view once new photos are fetched from Flickr.

Go ahead and do this. In PhotosViewController.swift, update the data source as soon as the view is loaded.

Listing 22.10  Updating the data source on load (PhotosViewController.swift)

override func viewDidLoad()
    super.viewDidLoad()

    collectionView.dataSource = photoDataSource
    collectionView.delegate = self

    updateDataSource()

    store.fetchInterestingPhotos {
        (photosResult) -> Void in

        self.updateDataSource()
    }
}

The Photorama application is now persisting its data between runs. The photo metadata is being persisted using Core Data, and the image data is being persisted directly to the filesystem. As you have seen, there is no one-size-fits-all approach to data persistence. Instead, each persistence mechanism has its own set of benefits and drawbacks. In this chapter, you have explored one approach, Core Data, but you have only seen the tip of the iceberg. In Chapter 23, you will explore the Core Data framework further to learn about relationships and performance.

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

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