Skip to main content

Optimise the state toggling logic

ID
eaabfa8
date
2023-05-23 06:40:47+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
0d3fa13
message
Optimise the state toggling logic

Previously we were issuing three change requests for every review action,
e.g. running `toggle-approved` would:

1.  look at whether a photo was in the 'approved' album, and add/remove
    as appropriate
2.  remove a photo from the 'rejected' album
3.  remove a photo from the 'needs action' album

But in practice actions (2) and (3) are usually redundant; most of what
I'm reviewing is unreviewed and isn't in any of the three albums.
Since we're already getting a complete list of albums that contain this
photo, we can check which albums need to change and only issue the
appropriate change requests.

I haven't profiled extensively, but in informal testing this does seem
to make the action maybe ~25% faster?
changed files
1 file, 46 additions, 9 deletions

Changed files

actions/run_action.swift (4989) → actions/run_action.swift (6346)

diff --git a/actions/run_action.swift b/actions/run_action.swift
index 9f335b8..49c5cfe 100644
--- a/actions/run_action.swift
+++ b/actions/run_action.swift
@@ -69,6 +69,18 @@ func getPhoto(withLocalIdentifier localIdentifier: String) -> PHAsset {
 }
 
 extension PHAsset {
+  func albums() -> [PHAssetCollection] {
+    var result: [PHAssetCollection] = []
+
+    PHAssetCollection
+      .fetchAssetCollectionsContaining(self, with: .album, options: nil)
+      .enumerateObjects({ (collection, index, stop) in
+        result.append(collection)
+      })
+
+    return result
+  }
+
   /// Returns true if an asset is in the given album, false otherwise.
   func isInAlbum(_ album: PHAssetCollection) -> Bool {
     var result = false
@@ -95,6 +107,17 @@ extension PHAsset {
     changeAlbum.removeAssets([self] as NSFastEnumeration)
   }
 
+  /// Add a photo to an album.
+  ///
+  /// This expects to be run inside a performChangesAndWait change block;
+  /// see https://developer.apple.com/documentation/photokit/phphotolibrary/1620747-performchangesandwait.
+  func add(toAlbum album: PHAssetCollection) -> Void {
+    let changeAlbum =
+      PHAssetCollectionChangeRequest(for: album)!
+
+    changeAlbum.addAssets([self] as NSFastEnumeration)
+  }
+
   /// Toggle a photo's inclusion in an album.
   ///
   /// If the photo is already in the album, remove it.  If the photo isn't
@@ -138,22 +161,36 @@ try PHPhotoLibrary.shared().performChangesAndWait {
       let rejected = getAlbum(withName: "Rejected")
       let needsAction = getAlbum(withName: "Needs Action")
 
-      if action == "toggle-approved" {
-        photo.toggle(inAlbum: approved)
-      } else {
+      let albums = photo.albums()
+
+      let isApproved = albums.contains(approved)
+      let isRejected = albums.contains(rejected)
+      let isNeedsAction = albums.contains(needsAction)
+
+      // Strictly speaking, the first condition is a combination of two:
+      //
+      //   1. The action is `toggle-approved` and the photo is approved,
+      //      in which case toggling means un-approving it.
+      //   2. The action is anything else and the photo is approved, in
+      //      which case setting the new status means removing approved.
+      //
+      // Similar logic applies for all three conditions.
+      if isApproved {
         photo.remove(fromAlbum: approved)
+      } else if action == "toggle-approved" {
+        photo.add(toAlbum: approved)
       }
 
-      if action == "toggle-rejected" {
-        photo.toggle(inAlbum: rejected)
-      } else {
+      if isRejected {
         photo.remove(fromAlbum: rejected)
+      } else if action == "toggle-rejected" {
+        photo.add(toAlbum: rejected)
       }
 
-      if action == "toggle-needs-action" {
-        photo.toggle(inAlbum: needsAction)
-      } else {
+      if isNeedsAction {
         photo.remove(fromAlbum: needsAction)
+      } else if action == "toggle-needs-action" {
+        photo.add(toAlbum: needsAction)
       }
 
     case "toggle-cross-stitch":