Augment the macOS Photos app with extensions that support project creation.
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.
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.
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?.rectView in Source
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.
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 and resumeProject methods provide points for your extension to begin monitoring changes.
self.projectAssets = PHAsset.fetchAssets(in: extensionContext.project, options: nil)
extensionContext.photoLibrary.register(self)View in Source
When the project is complete, use the finishProject protocol method to unregister from change observation.
library.unregisterChangeObserver(self)View in Source
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)
}
}
}View in Source
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
}