Skip to content

SnipSnaps for macOS — native Mac app (Photos + Files)#2

Open
NickReisenauer wants to merge 10 commits into
mainfrom
macos-port-phase-0-1
Open

SnipSnaps for macOS — native Mac app (Photos + Files)#2
NickReisenauer wants to merge 10 commits into
mainfrom
macos-port-phase-0-1

Conversation

@NickReisenauer

Copy link
Copy Markdown
Contributor

SnipSnaps for macOS — native Mac app (Photos + Files)

Brings SnipSnaps to macOS as a native SwiftUI destination (not Catalyst), shipping two cleanup surfaces — the existing Photos review/delete flow and a new macOS-only Files on-disk cleanup surface — and then makes the Mac build feel genuinely native. Distribution target: Mac App Store + App Sandbox. iOS is untouched throughout (all macOS-only code is #if os(macOS)-gated; iOS deployment target stays 18.5).

Port foundation (earlier phases on this branch)

  • Phase 0–1 — native Mac target (macosx in SUPPORTED_PLATFORMS), sandbox + photos/files entitlements, fixed unguarded UIKit and iOS-only SwiftUI modifiers. Fixed a launch crash (Form columnar style → AppKit constraint-cycle abort) with .formStyle(.grouped).
  • Phase 2 — Similar/duplicate photo scan restored on macOS via a shared CGImage dHash + Vision core.
  • Phase 4 / 4b — net-new Files surface: user grants folders via NSOpenPanel → app-scoped security-scoped bookmarks; categories Everything / Large / Old / Screenshots / Duplicates; swipe + keyboard review; recoverable trashItem deletes; shared review-size/lifetime stats and "Remember Reviewed" with Photos.

Native UI/UX & desktop pass (this session)

Driven by a 70-finding audit, adversarially verified, then re-reviewed (3 issues caught + fixed). Bumps MACOSX_DEPLOYMENT_TARGET 14.0 → 26.0 so Liquid Glass APIs are used directly on Mac.

  • Shell: NavigationSplitView sidebar replaces the bottom tab bar on macOS; Settings → Settings{} scene (⌘,); window default/min size + resizability; persisted sidebar selection; .accentColor.tint.
  • Keyboard + menu bar: real menu bar (Edit ▸ Undo ⌘Z, File ▸ Add Folder ⇧⌘O, Review menu, Help → real URLs) bridged via focusedSceneValue; ←/→/Delete/Space/Esc/Return shortcuts across all three review surfaces.
  • Finder integration (Files): Reveal in Finder, Quick Look (Space), Open (double-click/⌘O), right-click context menus, drag-a-folder-to-add, sort menu, Show-in-Trash.
  • Liquid Glass: glass buttons + info-chips, hover lift + pointer cursor, adaptive card edges, monospaced hero digits; fixes a Light-mode bug where window/card colors collapsed to the same gray (cards near-invisible).
  • Bug fixes: skip iCloud .icloud placeholders (no multi-GB download storms) + on-disk allocated size; report the 50k-file scan cap instead of silent-empty; cancellable scans; hardened security-scoped bookmarks (preserve offline grants, remove dead ones, re-add recovery, inaccessible warning); HomeView count-cache race; trash/hash failure surfacing; duplicate bucketing by logical size.
  • Accessibility & icon: Reduce Motion, VoiceOver, Dynamic Type growth, localized screenshot detection; a proper macOS app icon (squircle from the iOS artwork).

Verification status

  • macOS build green and launches at every phase.
  • ⚠️ Not runtime-verified on this machine (empty Photos library, iOS platform not installed): destructive Files trash flow, security-scoped-bookmark relaunch round-trips, Similar-matching accuracy, and an iOS regression build. Recommend building iOS + exercising the destructive Files flows on a populated Mac before merge.

Deferred (follow-ups)

AppKit video/Live-Photo playback on macOS; Photos-side Quick Look + VoiceOver swipe actions; String Catalog pluralization.

🤖 Generated with Claude Code

NickReisenauer and others added 10 commits June 22, 2026 13:43
Phased plan for bringing SnipSnaps to macOS (Mac App Store, sandboxed):
Photos surface port + new on-disk file-cleanup surface. Verification-grounded
against current Apple docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nly code

Phase 0 (project config):
- Add macosx to SUPPORTED_PLATFORMS and MACOSX_DEPLOYMENT_TARGET=14.0 on the
  app target's two configs only (test targets untouched). Native Mac
  destination on the existing SwiftUI target (not Catalyst).
- Add SnipSnaps-macOS.entitlements (app-sandbox + photos-library) wired via
  CODE_SIGN_ENTITLEMENTS[sdk=macosx*]; iOS keeps the existing entitlements file.

Phase 1 (make the Photos build compile + run on macOS):
- AppColors: cross-platform semantic colors; AppKit (NSColor) branch for the
  background/card/chip/fill/separator families that have no UIKit equivalent.
- Guard UIKit-only haptics (ContentView) behind #if canImport(UIKit).
- SettingsView: UIApplication.shared.open -> @Environment(.openURL).
- Replace Color(UIColor.tertiaryLabel) / Color(.tertiarySystemGroupedBackground)
  etc. with .tertiary / AppColor tokens.
- Guard iOS-only SwiftUI modifiers (navigationBarTitleDisplayMode, .tabBar,
  statusBarHidden, presentationDetents) with #if os(iOS); swap topBar*
  toolbar placements for cross-platform cancellation/confirmation/primaryAction;
  fullScreenCover -> sheet on macOS.

Verified: macOS build SUCCEEDED (xcodebuild, signing disabled). Similar/
duplicate scan remains a no-op on macOS pending the Phase 2 CGImage refactor.
iOS build not run here (iOS 26.5 platform not installed on this machine);
iOS code paths are unchanged or behind #if guards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SettingsView's Form used the macOS-default columnar form style, whose
label-column layout failed to converge and tripped AppKit's
'Update Constraints in Window pass' cycle (NSGenericException / SIGABRT)
at launch. SwiftUI's TabView eagerly lays out all tabs on macOS, so the
whole app aborted even though Home was frontmost.

.formStyle(.grouped) lays rows edge-to-edge (no column solve) and matches
the grouped look the app already uses on iOS (where .grouped is the Form
default, so iOS is visually unchanged). Verified: app builds, launches,
and renders Home + Settings on macOS without crashing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e core)

The entire similar-photo pipeline (fetchSimilarPhotoGroups, thumbnailImage,
featurePrint, differenceHash) was wrapped in #if canImport(UIKit) with the
AppKit branch returning [], so Similar mode was a silent no-op on macOS.

Refactor so the comparison core is platform-shared:
- thumbnailImage(->UIImage) becomes thumbnailCGImage(->CGImage); the only
  platform-conditional step is PlatformImage->CGImage (UIImage.cgImage vs
  NSImage.cgImage(forProposedRect:context:hints:)).
- differenceHash now takes a CGImage; featurePrint requests a CGImage directly.
- Lift fetchSimilarPhotoGroups + the helpers out of the UIKit gate (Vision,
  CoreGraphics, PhotoKit are all available on macOS).

iOS behavior is unchanged by construction: the same CGImage is extracted from
the same PHImageManager UIImage and fed to the same dHash/Vision math, so the
matching thresholds are identical. Verified: macOS build SUCCEEDED and the app
launches/runs with the scan compiled in.

NOT yet verified: real duplicate matching on macOS — this Mac's Photos library
is empty (0 assets), so thumbnailCGImage/dHash/Vision never receive real input.
Needs a populated macOS Photos library to confirm matching accuracy + any
NSImage->CGImage color-space threshold drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New macOS-only 'Files' tab that reviews loose files in user-granted folders
(Downloads/Desktop/Documents/any) and moves confirmed files to the Trash.
All new code is #if os(macOS) so iOS is untouched.

- FileFolderStore: user grants folders via NSOpenPanel; access persisted as
  app-scoped security-scoped bookmarks, reopened each launch. No silent
  full-disk scan (sandbox: user picks each folder).
- FileLibrary: enumerates granted folders into FileItems; categories
  Everything / Large (>=50MB) / Old (>180d) / Screenshots (image + name
  heuristic); single-pass counts; deletion via FileManager.trashItem
  (recoverable) — never removeItem. Skips dirs/symlinks/packages; capped scan.
- FilesView: onboarding + folder management + category cards with live counts.
- FileReviewSessionView: swipe + ←/→ keyboard + ⌘Z undo review flow, async
  QuickLook thumbnails, deferred 'Move N to Trash' summary.
- Entitlements: files.user-selected.read-write (build setting flipped to
  readwrite) + files.bookmarks.app-scope; Info.plist Desktop/Documents/
  Downloads usage strings.

Verified live on macOS: granted the real Downloads folder, scan produced real
counts (4715 everything / 3229 old / 6 large / 5 screenshots), categories
render. Review-card + trash UI built; trash action not exercised on real files
by me (destructive). MVP scope — content-hash dedup and persistent file review
memory are deferred (Phase 4b).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Files review now uses the shared @AppStorage("reviewLimit") "Review Size"
  setting instead of a separate hardcoded limit.
- Files deletions accumulate into the shared totalDeletedCount/totalDeletedBytes
  lifetime stats; Settings 'Lifetime Stats' relabeled 'Deleted photos' ->
  'Deleted items' since it now covers both surfaces.
- FilesView re-tallies category counts when a review session closes (deletions
  now reflected), mirroring HomeView's post-review refresh.
- Files summary gains the same Lifetime card + kept/trashed tally as Photos.

Still Phase 4b: 'Remember Reviewed' skip-memory for files (needs persistent
file identity) — Files currently re-shows every file each scan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the last cohesion gaps between Files and Photos:
- FileReviewHistory: on-disk analog of PhotoReviewHistory, keyed on file path,
  honoring the same ReviewMemoryOption windows. Files review marks items
  reviewed on keep/delete and unmarks on undo; scans exclude reviewed paths.
- FilesView cards now show 'X not reviewed - Y total' and respect the shared
  'Remember Reviewed' setting (counts refresh when it changes); FileLibrary.counts
  returns total + notReviewed per category (FileCounts), mirroring Photos.
- New Duplicates category: exact-content dupes via size-bucket + streamed
  SHA-256 (CryptoKit), surfacing redundant copies (keeps the oldest). Shown as
  a scan-on-demand 'Scan' card like Photos' Similar.
- Reset Local Settings also clears file review memory; app launch compacts it.

Builds on macOS. Deferred: near-duplicate image matching (could reuse the
Phase 2 dHash+Vision core fed CGImageSource from disk).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records what shipped (Phases 0-2, 4, 4b + cohesion), deviations (skipped
Phase 3, Form crash fix, per-SDK entitlements), verification reality (macOS
green; iOS unbuildable here; Photos lib empty; destructive paths not driven),
and what remains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Glass, Finder, bug fixes)

Make the Mac build feel like a real native app rather than a ported phone app.
Bumps MACOSX_DEPLOYMENT_TARGET 14.0 -> 26.0 (iOS target stays 18.5 - they are
independent settings) so Liquid Glass APIs are used directly inside #if os(macOS)
with no availability gating; iOS is untouched.

Shell & window:
- Replace the iOS bottom TabView with a native NavigationSplitView sidebar on
  macOS (Photos / Files); iOS keeps its tabs.
- Move Settings into a macOS Settings{} scene (Cmd-,); add default/min window
  size + resizability; Mac title-bar Refresh; persist sidebar selection.
- Replace deprecated .accentColor with .tint.

Keyboard & menu bar:
- New Commands/AppCommands.swift: Edit > Undo (Cmd-Z), File > Add Folder
  (Shift-Cmd-O), a Review menu, and a Help menu wired to the real support/legal
  URLs, all routed to the active review via focusedSceneValue.
- Keyboard shortcuts on all three review surfaces (left/right/Delete/Space/Esc/
  Return) on hidden buttons; Cmd-keys owned by the menu.

Finder integration (Files surface):
- Reveal in Finder, Quick Look (Space), Open (double-click / Cmd-O), right-click
  context menus, drag-a-folder-to-add, a sort menu, and Show-in-Trash.

Liquid Glass & visual polish:
- Design/GlassStyle.swift helpers (glass buttons, glass info-chips, hover lift +
  pointer cursor); adaptive AppColor.cardEdge; monospaced hero digits.
- Fix a Light-mode bug where the window and card colors collapsed to the same
  gray (cards were near-invisible) - now a proper recessed/raised hierarchy.

Bug fixes (Files engine + counts):
- Skip iCloud (.icloud) placeholder files so a scan can't trigger multi-GB
  downloads; use on-disk allocated size for "space freed".
- Report the 50k-file scan cap via ScanResult.truncated instead of silently
  returning an empty result.
- Make scans cancellable; harden security-scoped bookmarks (preserve transiently
  offline grants, allow removing dead ones, recover an inaccessible folder via
  re-add, surface an inaccessible warning).
- Fix a HomeView count-cache generation race (cross-platform).
- Surface trash/hash failures; snapshot the Remember-Reviewed option per session;
  bucket duplicates by logical (not allocated) size.

Accessibility & icon:
- Honor Reduce Motion; VoiceOver (hide the decorative count, merge cards, add
  Files swipe actions); Dynamic Type growth; localized screenshot-name detection.
- Add a macOS app icon (squircle generated from the iOS artwork).

macOS build verified green and launches. Not yet runtime-tested on a populated
Photos library or a real iOS build (neither available on this machine); shared-
file changes are #if os(macOS)-gated or cross-platform-safe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a native macOS build of SnipSnaps (not Catalyst) with a Mac-first shell, menu/keyboard support, and a new sandboxed “Files” cleanup surface that complements the existing Photos review flow, while keeping iOS behavior gated/unchanged where needed.

Changes:

  • Introduces macOS-only Files cleanup: folder grants via security-scoped bookmarks, file scanning/categories, and a review+trash workflow.
  • Updates the macOS app shell/UI: NavigationSplitView sidebar, native Settings {} window, Commands-based menu bar actions, and Liquid Glass-style helpers.
  • Refactors shared utilities for cross-platform correctness (e.g., Photos “Similar” scan CGImage path, semantic colors, openURL).

Reviewed changes

Copilot reviewed 22 out of 29 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
SnipSnaps/Views/Tabs/SettingsView.swift macOS Settings window rendering + cross-platform URL opening + reset integration with Files history.
SnipSnaps/Views/Tabs/HomeView.swift macOS title/toolbar tweaks, hover styling, and count-cache race fix.
SnipSnaps/Views/Review/ReviewSessionView.swift macOS keyboard/menu integration via focused values; Reduce Motion handling; styling consolidation.
SnipSnaps/Views/Marketing/ScreenshotDemoView.swift Updates demo UI to match new semantic colors/tertiary styling.
SnipSnaps/Views/Files/FilesView.swift New Files landing page: folder grants, category cards, navigation into file review.
SnipSnaps/Views/Files/FileReviewSessionView.swift New Files review flow (scan → swipe/keys → summary → trash) with Quick Look + Finder actions.
SnipSnaps/Utils/Photos.swift Makes Similar scan cross-platform by using CGImage thumbnails and shared hashing/Vision pipeline.
SnipSnaps/SnipSnapsApp.swift macOS window sizing, Commands hookup, native Settings scene; compacts file review history.
SnipSnaps/SnipSnaps-macOS.entitlements Adds macOS sandbox/Photos/bookmark/user-selected read-write entitlements.
SnipSnaps/Info.plist Adds Desktop/Documents/Downloads usage descriptions for macOS folder access prompts.
SnipSnaps/Files/FinderActions.swift NSWorkspace wrappers for Reveal/Open/Trash Finder integration (macOS-only).
SnipSnaps/Files/FileReviewHistory.swift Remembers reviewed file paths (session + persisted windows) mirroring Photos behavior.
SnipSnaps/Files/FileModels.swift Defines Files domain models (items, categories, sort options, counts).
SnipSnaps/Files/FileLibrary.swift Implements folder enumeration, categorization, duplicate hashing, and trashing.
SnipSnaps/Files/FileFolderStore.swift Persists and manages security-scoped folder bookmarks and access lifetimes.
SnipSnaps/Design/GlassStyle.swift Adds Liquid Glass/button/card hover helpers with iOS fallbacks.
SnipSnaps/Design/AppColors.swift Introduces cross-platform semantic color mapping (UIKit vs AppKit).
SnipSnaps/ContentView.swift Switches to macOS sidebar shell; keeps iOS TabView; (needs UIKit import fix).
SnipSnaps/Commands/AppCommands.swift Adds macOS menu bar commands wired via focused scene values.
SnipSnaps/Assets.xcassets/AppIcon.appiconset/Contents.json Wires macOS icon sizes into the shared app icon set.
SnipSnaps.xcodeproj/project.pbxproj Adds macOS destination, sets macOS deployment target, macOS entitlements, and readwrite user-selected files.
docs/macos-port-plan.md Documents the macOS port plan, phases, and verification notes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -8,20 +8,34 @@
import SwiftUI
Comment on lines +253 to +270
private func refreshCounts() {
let folders = store.folders.map(\.url)
guard !folders.isEmpty else {
counts = [:]
return
}
isCounting = true
let reviewed = FileReviewHistory.reviewedPaths(memoryOption: reviewMemoryOption)
Task {
let tally = await Task.detached(priority: .utility) {
FileLibrary.counts(folders: folders, reviewedPaths: reviewed)
}.value
await MainActor.run {
counts = tally
isCounting = false
}
}
}
Comment on lines +326 to +334
Button {
applyDecision(.delete)
} label: {
Label("Trash", systemImage: "xmark")
.frame(maxWidth: .infinity)
}
.tint(AppColor.delete)
.keyboardShortcut(.leftArrow, modifiers: [])

Comment on lines +343 to +350
Button {
applyDecision(.keep)
} label: {
Label("Keep", systemImage: "checkmark")
.frame(maxWidth: .infinity)
}
.tint(AppColor.success)
.keyboardShortcut(.rightArrow, modifiers: [])
Comment on lines +335 to +342
Button {
undo()
} label: {
Image(systemName: "arrow.uturn.backward")
}
.disabled(lastUndo == nil)
.help("Undo (⌘Z)")

} else if value.translation.width > threshold {
applyDecision(.keep)
} else {
withAnimation(.snappy(duration: 0.25)) { dragOffset = .zero }
Comment on lines +67 to +83
for folder in folders {
guard let enumerator = enumerator(for: folder) else { continue }
for case let url as URL in enumerator {
if examined >= maxFilesExamined { break }
guard let item = makeItem(url) else { continue }
examined += 1
let notReviewed = !reviewed.contains(item.url.path)
func bump(_ category: FileReviewCategory) {
total[category, default: 0] += 1
if notReviewed { fresh[category, default: 0] += 1 }
}
bump(.everything)
if item.size >= largeFileMinimumBytes { bump(.large) }
if item.modified < oldCutoff { bump(.old) }
if item.isScreenshot { bump(.screenshots) }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants