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.
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.
Build and run the Photos Project Slideshow scheme in Xcode once to run the sample app, which installs the extension in the macOS Photos app. To use the extension, build and run the Slideshow Sample scheme in Xcode, which prompts you to open the macOS Photos app to use the extension.
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?.rect
View 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,
let changeDetails = changeInstance.changeDetails(for: fetchResult)
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
}