A native macOS image slideshow app built with SwiftUI and Swift Package Manager.
| Tool | Version |
|---|---|
| macOS | 13.0 Ventura or later |
| Swift | 5.9 or later |
| Xcode | 15+ (optional — SPM works from the terminal) |
# 1. Clone / navigate to the project
cd /path/to/slideshow
# 2. Build a release .app bundle and launch it
./build-app.sh release
open Slideshow.appopen Package.swift # Xcode opens the SPM package as a full projectPress ⌘R to build and run.
./build-app.sh release installAfter installation, press ⌘Space and type Slideshow — the app will appear in Spotlight results.
First-launch security prompt: macOS Gatekeeper will warn that the app is from an unidentified developer (ad-hoc signed, no Apple Developer account required). Right-click the app → Open → Open to approve it permanently.
- Click Open Folder (⌘O) in the toolbar to pick a folder of images
- Drag and drop a folder or an individual image file onto the window — a directory loads all images in it; an image file loads its parent folder and jumps to that image
| Action | Shortcut |
|---|---|
| Play / Pause | Space |
| Next image | → |
| Previous image | ← |
| Toggle fullscreen | F |
| Open folder | ⌘O |
| Settings | ⌘, |
| Debug log | ⌘D |
Open Settings (⌘,) to choose a transition style (None / Fade / Slide / Zoom) and adjust the slide duration.
Press ⌘D or click the ladybug button in the toolbar to open the debug log overlay. It shows file metadata, video player events, and any errors — useful for diagnosing why a file isn't loading. Press Copy to paste the full log to the clipboard.
./build-app.sh [config] [install]| Argument | Default | Description |
|---|---|---|
config |
release |
release or debug |
install |
(omitted) | Pass install to copy to /Applications |
Examples:
./build-app.sh # release build, Slideshow.app in project root
./build-app.sh debug # debug build
./build-app.sh release install # release build + install to /ApplicationsThe script:
- Runs
swift build -c [config] - Assembles
Slideshow.app/Contents/with the binary andInfo.plist - Ad-hoc signs with
codesign --sign - - Optionally copies to
/Applications
swift test35 unit tests cover the pure-logic core (navigation, file loading, transition types). All tests run in < 100 ms with no UI setup.
Test Suite 'SlideshowTests' — 35 tests
SlideshowControllerTests (18) — navigation, wrap-around, index clamping
ImageFileLoaderTests (11) — format filtering, sorting, hidden files, video extensions
TransitionTypeTests (6) — codable, identifiable, equatable
slideshow/
├── Package.swift # SPM manifest
├── Resources/
│ └── Info.plist # macOS app bundle metadata
├── build-app.sh # builds + assembles Slideshow.app
│
├── Sources/
│ ├── SlideshowCore/ # ← pure logic, no SwiftUI, fully testable
│ │ ├── TransitionType.swift # enum: None / Fade / Slide / Zoom
│ │ ├── SlideshowController.swift # value-type state machine (struct)
│ │ └── ImageFileLoader.swift # scans a directory for image/video URLs
│ │
│ └── Slideshow/ # ← SwiftUI executable
│ ├── SlideshowApp.swift # @main entry point
│ ├── AppState.swift # shared singleton for menu commands
│ ├── SlideshowViewModel.swift # @MainActor ObservableObject, timer, file loading
│ ├── TransitionType+SwiftUI.swift # SwiftUI animations/transitions on core enum
│ ├── ContentView.swift # root: welcome screen ↔ slideshow, drag-and-drop
│ ├── SlideshowView.swift # image/video display + controls overlay
│ ├── AsyncImageView.swift # NSImageView wrapper (GIF-aware, WebP-aware)
│ ├── VideoPlayerView.swift # WKWebView wrapper for WebM video playback
│ ├── DebugLog.swift # in-app debug log singleton + overlay
│ ├── ControlsView.swift # transport bar with keyboard shortcuts
│ ├── SettingsView.swift # transition style + slide duration sheet
│ └── CreditsView.swift # credits sheet
│
└── Tests/
└── SlideshowTests/
├── SlideshowControllerTests.swift
├── ImageFileLoaderTests.swift
└── TransitionTypeTests.swift
SlideshowCore is a pure Swift library — no SwiftUI, no AppKit, no timers. All navigation logic lives in SlideshowController, a plain struct with mutating func methods. This makes it trivially testable without mocks.
Slideshow is the SwiftUI executable. SlideshowViewModel wraps @Published var controller = SlideshowController(). SwiftUI can synthesize Binding values through struct keypaths, so $viewModel.controller.transitionType "just works" as a two-way binding in SettingsView.
SwiftUI-specific behavior (animation curves, AnyTransition) is added via extension TransitionType in TransitionType+SwiftUI.swift — the core enum stays framework-free.
- Add a case to
TransitionTypeinSources/SlideshowCore/TransitionType.swift - Add the
swiftUITransitionandanimationcases inSources/Slideshow/TransitionType+SwiftUI.swift - Add a test case in
Tests/SlideshowTests/TransitionTypeTests.swift
- Add the lowercase extension to
ImageFileLoader.supportedExtensionsinSources/SlideshowCore/ImageFileLoader.swift - Add an assertion to
ImageFileLoaderTests.testSupportedExtensionsContainsCommonFormats - Update the format list in this README
- Add the lowercase extension to
ImageFileLoader.videoExtensions— it is automatically included insupportedExtensionsviaSet.union - Confirm WebKit/HTML5 video can decode the codec; add a
<source type="…">inVideoPlayerViewif a different MIME type is required - Add assertions in
ImageFileLoaderTestsand update this README
Note: AVFoundation does not support VP8/VP9.
VideoPlayerViewusesWKWebViewwithloadFileURL(_:allowingReadAccessTo:)— do not use AVKit for VP8/VP9 content.loadHTMLStringcannot be used either as it sandboxesfile://access.
./build-app.sh release install # rebuild + update /Applicationsjpg · jpeg · png · gif (animated) · bmp · tiff · tif · heic · heif · webp
Animated GIFs loop continuously until the slideshow advances to the next image.
webm (VP8 / VP9)
WebM files play via WKWebView's HTML5 video stack. When the slideshow is in play mode, the next slide loads automatically when the video finishes (the video's natural duration overrides the slide-delay timer for that slide).