Sample Code

Creating a Slideshow Project Extension for Photos

Augment the macOS Photos app with extensions that support project creation.

Download

Overview

Starting in macOS 10.13, you can create Photos project extensions. This sample app shows you how to implement a slideshow extension that transitions between photos by zooming in to the region of interest (ROI) that’s algorithmically deemed most important. It demonstrates the computation of saliency based on an ROI’s weight and quality, and the process of subscribing to change notifications so your extension can respond to asset modifications.

Set Up the Project to Run Inside the Photos App

The sample app builds and runs in Xcode, but you open the macOS Photos app to access functionality. In the extension’s Info.plist file, designate the extension type by entering slideshow in the field at NSExtension > NSExtensionAttributes > PHProjectCategory. You can add more categories to the information property list if you want your extension to appear in more categories in the Create menu.

From within the Photos app, access the Create categories by choosing File > Create or right-clicking any group of assets. Under the Slideshow category, you’ll see the app extension and can create a project to run in it.

Because the project extension runs inside the Photos.app, the sample emulates the grid layout of the user’s photo assets. Pressing the play button in the upper-right corner of the extension starts the slideshow.

Customize the Focus Rectangle of the Zoom Transition

The sample code project contains custom Animator and AssetModel classes.

The Animator class handles transitions between photos in the slideshow. This sample’s Animator asks an AssetModel object for a rectangle to zoom in to. Photos identifies each human face it finds as a possible ROI, and the sample uses the bounding box of the most salient one as the preferred zoom rectangle. The code defines saliency of a PHProjectRegionOfInterest as the sum of its weight and quality values, then sorts the array of the photo’s regions by that value.

let sortedRois = assetProjectElement.regionsOfInterest.sorted { (roi1, roi2) -> Bool in
    return roi1.weight + roi1.quality < roi2.weight + roi2.quality
}
return sortedRois.last?.rect

The weight of an ROI represents the pervasiveness of the face in the project as a whole. The quality score represents the quality of the ROI in the individual asset, based on factors such as sharpness, visibility, and prominence in the photo. Adding these two values is a heuristic for determining the face’s relative importance throughout a photo project. Objects that aren’t faces don’t qualify as ROI.

Respond to Asset Changes in the Project

Your app extension should monitor change notifications and respond to asset changes in the Photos library, like photos being added or removed.

Register for change observation as soon as the project begins or resumes. In the PHProjectExtensionController protocol, the beginProject(with:projectInfo:completion:) and resumeProject(with:completion:) methods provide points for your extension to begin monitoring changes.

self.projectAssets = PHAsset.fetchAssets(in: extensionContext.project, options: nil)
extensionContext.photoLibrary.register(self)

When the project is complete, use the finishProject(completionHandler:) protocol method to unregister from change observation.

library.unregisterChangeObserver(self)

Whenever something changes in the Photos library, the photoLibraryDidChange(_:) method is called. When implementing this method, ask the PHChange instance for details about changes to the object you’re interested in. When assets are added or removed, the sample project calls updatedProjectInfo(from:completion:) to get an updated PHProjectInfo instance, which you can use to refresh your UI.

func photoLibraryDidChange(_ changeInstance: PHChange) {
    guard let fetchResult = projectAssets as? PHFetchResult<AnyObject>,
        let changeDetails = changeInstance.changeDetails(for: fetchResult) as? PHFetchResultChangeDetails<PHAsset>
        else { return }
    projectAssets = changeDetails.fetchResultAfterChanges

    guard let projectExtensionContext = projectExtensionContext else { return }
    projectExtensionContext.updatedProjectInfo(from: projectModel?.projectInfo) { (updatedProjectInfo) in
        guard let projectInfo = updatedProjectInfo else { return }
        DispatchQueue.main.async {
            self.setupProjectModel(with: projectInfo, extensionContext: projectExtensionContext)
        }
    }
}

Support Copy and Paste

If your extension handles the paste action, implement the validateMenuItem(_:) delegate method to handle pasteboard contents.

func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
    var canHandlePaste = false
    if menuItem.action == #selector(paste(_:)) {
        canHandlePaste = canHandleCurrentPasteboardContent()
    }
    return canHandlePaste
}

See Also

macOS Photos Project Extensions

class PHProject

A representation of a Photos app project extension.

class PHProjectInfo

Information about the project extension.

class PHProjectExtensionContext

An object that provides Photos project extensions with access to the underlying project, as well as to the user's photo library for editing.

class PHProjectElement

The superclass for all element objects.

class PHProjectSection

A collection of content representing curated asset and text elements.

class PHProjectRegionOfInterest

A representation of a region of interest in a photo asset.

class PHProjectChangeRequest

A request to change asset data in a Photos project extension.

protocol PHProjectExtensionController

A protocol defining the life cycle and supported types of project extensions.

class PHCloudIdentifier

A cloud identifier for a Photos project extension.

struct PHProjectCategory

A representation of Photos project extension categories.