Skip to main content

FocusedImage: fix layout flashes when the underlying data changes

ID
584f492
date
2026-06-29 08:00:39+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
e154ddb
message
FocusedImage: fix layout flashes when the underlying data changes
changed files
2 files, 34 additions, 8 deletions

Changed files

Blink/Views/FocusedImage/FocusedImage.swift (959) → Blink/Views/FocusedImage/FocusedImage.swift (2076)

diff --git a/Blink/Views/FocusedImage/FocusedImage.swift b/Blink/Views/FocusedImage/FocusedImage.swift
index ada00c7..1e250f2 100644
--- a/Blink/Views/FocusedImage/FocusedImage.swift
+++ b/Blink/Views/FocusedImage/FocusedImage.swift
@@ -1,26 +1,52 @@
 import SwiftUI
 import Photos
 
-/// Render the big image that gets shown in the main view.
+/// FocusedImage is the primary container view for the photo currently under review.
+///
+/// It handles the presentation and layout of the focused asset, including displaying
+/// the image itself, adding metadata overlays, and attaching interactive modifiers.
 struct FocusedImage: View, Identifiable {
+    let asset: PHAsset
+    @ObservedObject var photosLibrary: PhotosLibrary
+    
     var id: String {
         asset.localIdentifier
     }
     
-    var asset: PHAsset
-    @ObservedObject var focusedAssetImage: PHAssetImage
+    var body: some View {
+        FocusedImageContent(
+            asset: asset,
+            assetImage: photosLibrary.getFullSizedImage(for: asset)
+        )
+        // Tell SwiftUI that this instance of the view corresponds to this asset.
+        //
+        // This means that when the underlying data updates in response to changes
+        // in the Photos app, SwiftUI will update the existing view instead of
+        // destroying and recreating it. This prevents layout flashes.
+        .id(asset.localIdentifier)
+    }
+}
+
+/// FocusedImageContent renders the content of a single photo.
+///
+/// This view isolates and observes a `PHAssetImage` to gracefully handle the
+/// two-step progressive loading from the Photos framework (instantly displaying
+/// a low-resolution thumbnail, followed by the high-resolution image).
+private struct FocusedImageContent: View {
+    let asset: PHAsset
+    @ObservedObject var assetImage: PHAssetImage
     
     var body: some View {
-        Image(nsImage: focusedAssetImage.image)
+        Image(nsImage: assetImage.image)
             .resizable()
-            .draggable(Image(nsImage: focusedAssetImage.image))
+            .draggable(Image(nsImage: assetImage.image))
             .aspectRatio(contentMode: .fit)
             .albumInfo(for: asset)
-            .loadingIndicator(isLoading: focusedAssetImage.isDegraded)
+            .loadingIndicator(isLoading: assetImage.isDegraded)
             .contextMenu {
                 Button {
                     NSPasteboard.general.clearContents()
-                    NSPasteboard.general.writeObjects([focusedAssetImage.image])
+                    NSPasteboard.general.writeObjects([assetImage.image])
                 } label: {
                     Label("Copy", systemImage: "doc.on.doc")
                         .labelStyle(.titleAndIcon)

Blink/Views/PhotoReviewer.swift (14287) → Blink/Views/PhotoReviewer.swift (14247)

diff --git a/Blink/Views/PhotoReviewer.swift b/Blink/Views/PhotoReviewer.swift
index 314936a..77bf5bc 100644
--- a/Blink/Views/PhotoReviewer.swift
+++ b/Blink/Views/PhotoReviewer.swift
@@ -61,7 +61,7 @@ struct PhotoReviewer: View {
                     
                     FocusedImage(
                         asset: focusedAsset,
-                        focusedAssetImage: photosLibrary.getFullSizedImage(for: focusedAsset)
+                        photosLibrary: photosLibrary,
                     )
                     
                     Spacer()