A comprehensive SwiftUI package for displaying beautiful media galleries with advanced features including zoom, pan, slideshow, grid view with multi-select, video playback, and more.
- Slideshow View: Fullscreen media viewer with swipe navigation
- Grid View: Browsing interface with thumbnails and filtering
- Responsive Design: Adapts to screen size (3 wide on iPhone portrait, 4 on landscape)
- β Double-tap to zoom (1x to 4x with smooth animations)
- β Pinch-to-zoom gesture support
- β Pan gesture when zoomed in
- β Swipe navigation between media items
- β Caption support with toggle visibility
- β Share functionality (preserves original file formats)
- β Built-in iCloud download support
- Configurable duration (default 5 seconds)
- Automatic playback through images and videos
- Smart pause when zoomed in
- Automatic resume when zoomed out
- Duration detection for animated images
- Auto-disable idle timer (iOS): Prevents device from sleeping during slideshow playback
- Static Images: JPEG, PNG, HEIC, RAW (DNG, CR2, NEF, ARW), etc.
- Animated Images: GIF, APNG, HEIF sequences, WebP
- Videos: MP4, MOV, M4V, WebM with playback controls
- Audio: MP3, AAC, M4A, FLAC, WAV with artwork and controls
- Duration Display: Shows video/audio length and animated image duration
- Multi-Select Mode: Tap to select multiple items with visual indicators
- Filtering: Built-in filter UI (All, Images, Videos, Audio, Animated)
- Custom Filters: Apply your own filtering logic
- Custom Sorting: Define custom sort order
- Batch Operations: Share, delete, or perform custom actions on selected items
- Platform-specific share sheets (iOS UIActivityViewController, macOS NSSharingServicePicker)
- Custom action buttons API
- Multi-select with custom bulk actions
- Drag & drop support (macOS)
- Cross-platform support (iOS & macOS)
- LRU Thumbnail Cache: Automatic eviction of least-recently-used thumbnails with configurable memory limit (default 100MB)
- Visibility-based Loading: Only loads thumbnails for items currently visible on screen
- ImageIO Downsampling: Uses efficient CGImageSource for thumbnails without loading full images into memory
- Memory Pressure Handling: Automatically evicts cache entries when iOS sends memory warnings
- Lazy Gallery Rendering: Only renders current and adjacent items in slideshow view (not all 600+ items)
- WKWebView Video Player: Memory-efficient HTML5 video playback supporting WebM, MP4, and more
- Native Animated Images: CGImageSource + display link rendering with LRU frame cache
- sourceURL Property: Direct URL loading for animated images without intermediate decoding
- Improved Gesture Support: Full zoom/pan support for animated images on macOS and iOS
- Simplified Audio Controls: Mute/unmute toggle with persistent state between videos
- Audio Media Type: New
MediaType.audiofor audio file support - Audio Player Controls: Full-featured playback with:
- Play/pause button with elegant circular design
- Scrubber slider for seeking with time display
- Volume slider with expand/collapse animation
- Mute/unmute toggle with persistent state
- Progress tracking and duration display
- Album Artwork Display: Shows embedded artwork or custom placeholder
- Audio Placeholder Thumbnails: Gradient background with music note icon when no artwork exists
- Audio Metadata: Title, artist, album, track number, and year support
- Slideshow Integration: Audio files work seamlessly in slideshow with auto-advance
- Local Media Caching: Download media files locally for offline/background playback
MediaDownloadManager: Singleton for managing downloads and cacheMediaDownloadButton: UI component with download/progress/cached states- Files stored in
~/Library/Caches/MediaStream/DownloadedMedia/(unencrypted for AVPlayer)
- Background Audio/Video Playback: Continue playback when app is backgrounded (cached media only)
- Lock Screen & Control Center Integration:
- Play/pause, next/previous track controls
- Seek bar with accurate position tracking
- Album artwork and metadata display (title, artist, album)
- Playback position updates in real-time
- Picture-in-Picture (PiP): Manual PiP toggle for cached videos
- Smart Playback Behavior:
- Short-form content (< 7 min): Starts from beginning (music behavior)
- Long-form content (β₯ 7 min): Resumes from last position (podcast/movie behavior)
- Cache Management:
- Individual item download/clear in slideshow view
- Bulk download/clear in grid view
- Integrates with "Clear Cache" to remove downloaded media
- Native CGImageSource Rendering: Replaced WKWebView-based animated image display with native frame-by-frame rendering via
CGImageSource+ display link (CADisplayLinkon iOS,Timeron macOS) - Animated WebP Support: Full frame duration extraction via
kCGImagePropertyWebPDictionaryacross all animated image helpers - LRU Frame Cache: 4-frame sliding window cache for memory-efficient playback
- Accurate Frame Timing:
CACurrentMediaTime()for smooth animation without dropped or doubled frames - Improved macOS Gesture Support: Native
NSViewrendering eliminates WKWebView scroll event conflicts β zoom/pan gestures work correctly - Thumbnail Load Cancellation: Grid thumbnails cancel in-flight downloads when views disappear (e.g., gallery dismiss)
- Media Type Re-filtering: Grid automatically re-checks filter chips when WebP/HEIC items resolve their actual animation state after download
- Video Metadata Auth Headers:
getVideoDurationWebViewandhasAudioTrackWebViewnow pass auth headers through to the WebView fallback
- 360/180 Spherical Video: Renders equirectangular video on an interactive SceneKit sphere with gyroscope and touch/drag controls
- Stereoscopic Formats: Side-by-Side (SBS/HSBS) and Top-Bottom (TB/HTB) projection modes for 3D content
- Fisheye Projection: Metal shader-based equidistant fisheye UV remapping for fisheye-encoded content (mono, SBS, TB)
- 2D Flat Crop Mode: View stereoscopic content as flat 2D by cropping to one eye (left for SBS, top for TB) β works in both slideshow and grid views
- Automatic Detection: Filename-based VR projection detection (e.g.,
_180_sbs,_360,_fisheye) viaVRFilenameDetector - Manual Override: Per-item projection picker lets users manually set or change the VR projection type
- Smart Thumbnail Cropping: Grid thumbnails automatically show only one eye for SBS/TB content
- tvOS Support: Full VR projection controls, scrub bar, and slideshow overlay on Apple TV
- Apple TV Media Browser: Full-screen media viewer with native tvOS navigation and focus system
- Slideshow Controls: Double-tap Play/Pause to access slideshow overlay with navigation, loop, shuffle, and interval controls
- VR Projection Controls: SceneKit sphere rendering and flat crop modes on tvOS with confirmation dialog picker
- Recently Played: Thumbnail cache with SBS/TB-aware cropping for recently played media
- Native Video Controls: AVPlayerViewController integration with subtitle and audio track selection
- Native RAW Support: Leverages iOS/macOS ImageIO for RAW image formats
- Supported Formats: DNG, CR2, CR3, NEF, ARW, ORF, RW2, and other camera RAW formats
- Efficient Thumbnails: Uses CGImageSource for memory-efficient RAW thumbnail generation
- Full Resolution Display: RAW images display at full quality in slideshow view
Add the package to your Xcode project:
- In Xcode, go to File > Add Package Dependencies
- Enter the repository URL:
https://github.com/blaineam/MediaStream.git - Select your desired version or branch
- Click Add Package
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/blaineam/MediaStream.git", from: "1.9.0")
]import SwiftUI
import MediaStream
struct ContentView: View {
let mediaItems: [any MediaItem] = [
// Your media items
]
@State private var showGallery = false
var body: some View {
Button("Show Gallery") {
showGallery = true
}
.sheet(isPresented: $showGallery) {
MediaGalleryView(
mediaItems: mediaItems,
initialIndex: 0,
onDismiss: {
showGallery = false
}
)
}
}
}import SwiftUI
import MediaStream
struct GalleryBrowserView: View {
let mediaItems: [any MediaItem]
@State private var showGallery = false
var body: some View {
MediaGalleryGridView(
mediaItems: mediaItems,
multiSelectActions: [
MediaGalleryMultiSelectAction(
title: "Delete",
icon: "trash"
) { selectedItems in
// Handle deletion
deleteItems(selectedItems)
}
],
includeBuiltInShareAction: true,
onSelect: { index in
// Open slideshow at selected index
showGallery = true
},
onDismiss: {
// Handle dismiss
}
)
}
}The MediaItem protocol is the foundation of the package. Here's a complete implementation:
import Foundation
import MediaStream
#if canImport(UIKit)
import UIKit
typealias PlatformImage = UIImage
#elseif canImport(AppKit)
import AppKit
typealias PlatformImage = NSImage
#endif
struct PhotoMediaItem: MediaItem {
let id: UUID
let type: MediaType
private let imageURL: URL
private let caption: String?
init(id: UUID = UUID(), imageURL: URL, caption: String? = nil, isAnimated: Bool = false) {
self.id = id
self.imageURL = imageURL
self.caption = caption
self.type = isAnimated ? .animatedImage : .image
}
// Load the image from disk or network
func loadImage() async -> PlatformImage? {
do {
let data = try Data(contentsOf: imageURL)
#if canImport(UIKit)
return UIImage(data: data)
#elseif canImport(AppKit)
return NSImage(data: data)
#endif
} catch {
print("Failed to load image: \(error)")
return nil
}
}
// Not used for images
func loadVideoURL() async -> URL? {
return nil
}
// Return duration for animated images
func getAnimatedImageDuration() async -> TimeInterval? {
guard type == .animatedImage else { return nil }
return await AnimatedImageHelper.getAnimatedImageDuration(from: imageURL)
}
// Not used for images
func getVideoDuration() async -> TimeInterval? {
return nil
}
// Return the item to share (preserves original format)
func getShareableItem() async -> Any? {
return imageURL
}
// Return optional caption text
func getCaption() async -> String? {
return caption
}
// Videos only
func hasAudioTrack() async -> Bool {
return false
}
}struct VideoMediaItem: MediaItem {
let id: UUID
let type: MediaType = .video
private let videoURL: URL
private let thumbnailURL: URL?
init(id: UUID = UUID(), videoURL: URL, thumbnailURL: URL? = nil) {
self.id = id
self.videoURL = videoURL
self.thumbnailURL = thumbnailURL
}
// Load thumbnail image for grid view
func loadImage() async -> PlatformImage? {
guard let thumbnailURL = thumbnailURL else { return nil }
do {
let data = try Data(contentsOf: thumbnailURL)
#if canImport(UIKit)
return UIImage(data: data)
#elseif canImport(AppKit)
return NSImage(data: data)
#endif
} catch {
return nil
}
}
// Return video URL for playback
func loadVideoURL() async -> URL? {
return videoURL
}
func getAnimatedImageDuration() async -> TimeInterval? {
return nil
}
// Return video duration
func getVideoDuration() async -> TimeInterval? {
let asset = AVAsset(url: videoURL)
return try? await asset.load(.duration).seconds
}
func getShareableItem() async -> Any? {
return videoURL
}
func getCaption() async -> String? {
return nil
}
// Check if video has audio track
func hasAudioTrack() async -> Bool {
let asset = AVAsset(url: videoURL)
let tracks = try? await asset.loadTracks(withMediaType: .audio)
return !(tracks?.isEmpty ?? true)
}
}struct AudioFileMediaItem: MediaItem {
let id: UUID
let type: MediaType = .audio
private let audioURL: URL
private let artworkURL: URL?
private let metadata: AudioMetadata?
init(id: UUID = UUID(), audioURL: URL, artworkURL: URL? = nil, metadata: AudioMetadata? = nil) {
self.id = id
self.audioURL = audioURL
self.artworkURL = artworkURL
self.metadata = metadata
}
// Load album artwork for grid view (returns placeholder if nil)
func loadImage() async -> PlatformImage? {
if let artworkURL = artworkURL {
do {
let data = try Data(contentsOf: artworkURL)
#if canImport(UIKit)
return UIImage(data: data)
#elseif canImport(AppKit)
return NSImage(data: data)
#endif
} catch {
return nil
}
}
// AudioMediaItem automatically returns audio placeholder when artwork is nil
return nil
}
// Return audio URL for playback
func loadAudioURL() async -> URL? {
return audioURL
}
// Return audio duration
func getAudioDuration() async -> TimeInterval? {
let asset = AVAsset(url: audioURL)
return try? await asset.load(.duration).seconds
}
// Return audio metadata for caption display
func getAudioMetadata() async -> AudioMetadata? {
return metadata
}
func loadVideoURL() async -> URL? { nil }
func getAnimatedImageDuration() async -> TimeInterval? { nil }
func getVideoDuration() async -> TimeInterval? { nil }
func getShareableItem() async -> Any? { audioURL }
func getCaption() async -> String? {
guard let metadata = metadata else { return nil }
var parts: [String] = []
if let title = metadata.title { parts.append(title) }
if let artist = metadata.artist { parts.append(artist) }
if let album = metadata.album { parts.append(album) }
return parts.isEmpty ? nil : parts.joined(separator: "\n")
}
func hasAudioTrack() async -> Bool { true }
}You can also use the built-in AudioMediaItem for simple audio playback:
let audioItem = AudioMediaItem(
audioURLLoader: { return URL(fileURLWithPath: "/path/to/song.mp3") },
artworkLoader: {
// Load album artwork from ID3 tags or external source
return await extractAlbumArt(from: audioURL)
},
durationLoader: { return 180.0 },
metadataLoader: {
return AudioMetadata(
title: "Song Title",
artist: "Artist Name",
album: "Album Name",
trackNumber: 1,
year: 2024
)
}
)import SwiftUI
import MediaStream
struct BackgroundPlaybackExample: View {
let mediaItems: [any MediaItem]
@ObservedObject private var downloadManager = MediaDownloadManager.shared
var body: some View {
VStack {
// Download button for caching media locally
MediaDownloadButton(
mediaItems: mediaItems,
headerProvider: { url in
// Return auth headers if needed for your media URLs
return ["Authorization": "Bearer \(token)"]
}
)
// Check cache status
if downloadManager.allCached(mediaItems) {
Text("All media cached - background playback enabled!")
}
// Open gallery (background playback works automatically for cached items)
MediaGalleryView(
mediaItems: mediaItems,
initialIndex: 0,
onDismiss: { }
)
}
}
}Important Notes:
- Background playback only works for cached/downloaded media items
- Non-cached media will pause when the app enters background
- The
diskCacheKeyproperty on MediaItem is required for caching - Lock screen controls (play/pause, next/prev, seek) work automatically
- Album artwork and metadata display in Control Center when available
let config = MediaGalleryConfiguration(
slideshowDuration: 5.0, // Seconds per slide
showControls: true, // Show play/pause, share buttons
backgroundColor: .black, // Background color
customActions: [ // Custom action buttons
MediaGalleryAction(icon: "heart.fill") { index in
print("Favorited item at index \(index)")
},
MediaGalleryAction(icon: "square.and.arrow.down") { index in
print("Downloaded item at index \(index)")
}
]
)
MediaGalleryView(
mediaItems: mediaItems,
initialIndex: 0,
configuration: config,
onDismiss: { }
)let filterConfig = MediaGalleryFilterConfig(
customFilter: { item in
// Only show images
return item.type == .image
},
customSort: { item1, item2 in
// Sort by type (images first, then videos)
if item1.type == .image && item2.type != .image {
return true
}
return false
}
)
MediaGalleryGridView(
mediaItems: mediaItems,
filterConfig: filterConfig,
onSelect: { index in },
onDismiss: { }
)let multiSelectActions = [
MediaGalleryMultiSelectAction(
title: "Export",
icon: "square.and.arrow.down"
) { selectedItems in
Task {
for item in selectedItems {
if let shareableItem = await item.getShareableItem() {
// Export the item
exportToFiles(shareableItem)
}
}
}
},
MediaGalleryMultiSelectAction(
title: "Delete",
icon: "trash"
) { selectedItems in
// Show confirmation
showDeleteConfirmation(for: selectedItems)
},
MediaGalleryMultiSelectAction(
title: "Add to Album",
icon: "folder.badge.plus"
) { selectedItems in
// Show album picker
showAlbumPicker(for: selectedItems)
}
]
MediaGalleryGridView(
mediaItems: mediaItems,
multiSelectActions: multiSelectActions,
includeBuiltInShareAction: true, // Adds built-in share button
onSelect: { index in },
onDismiss: { }
)- Navigation: Swipe left/right to navigate between items
- Zoom: Double-tap to zoom in/out, pinch to zoom
- Controls: Play/pause slideshow, share button, caption toggle
- Caption: Collapsible caption overlay at bottom
- Progress: Page indicator showing current position
- Responsive Layout: 3 columns (portrait) or 4 columns (landscape) on iOS
- Filter Bar: Buttons to filter by media type
- Multi-Select: Tap "Select" to enter multi-select mode
- Selection Indicator: Blue checkmarks on selected items
- Toolbar: Action buttons appear when items are selected
struct EncryptedMediaItem: MediaItem {
let id: UUID
let type: MediaType
private let encryptedURL: URL
private let decryptionKey: Data
func loadImage() async -> PlatformImage? {
do {
// Load encrypted data
let encryptedData = try Data(contentsOf: encryptedURL)
// Decrypt (using your encryption manager)
let decryptedData = try decrypt(encryptedData, key: decryptionKey)
#if canImport(UIKit)
return UIImage(data: decryptedData)
#elseif canImport(AppKit)
return NSImage(data: decryptedData)
#endif
} catch {
print("Failed to decrypt image: \(error)")
return nil
}
}
func getShareableItem() async -> Any? {
// Create temporary decrypted file for sharing
let tempURL = FileManager.default.temporaryDirectory
.appendingPathComponent("\(UUID().uuidString).png")
do {
let encryptedData = try Data(contentsOf: encryptedURL)
let decryptedData = try decrypt(encryptedData, key: decryptionKey)
try decryptedData.write(to: tempURL)
return tempURL
} catch {
return nil
}
}
// ... other required methods
}The package includes built-in iCloud download support. When a file is not available locally, it will automatically attempt to download it from iCloud:
func loadImage() async -> PlatformImage? {
let url = imageURL
// Check if file exists, attempt iCloud download if needed
if !FileManager.default.fileExists(atPath: url.path) {
do {
try FileManager.default.startDownloadingUbiquitousItem(at: url)
// Wait for download (up to 5 seconds)
for _ in 1...10 {
if FileManager.default.fileExists(atPath: url.path) {
break
}
try await Task.sleep(nanoseconds: 500_000_000)
}
} catch {
print("iCloud download failed: \(error)")
return nil
}
}
// Load the image
// ...
}The package is designed with a protocol-oriented architecture:
MediaStream (Package)
βββ MediaItem (Protocol)
β βββ Defines interface for media items
β βββ Async methods for loading content
β βββ loadThumbnail for efficient thumbnail loading (v1.1.0)
β βββ vrProjection for VR/3D content detection (v2.0.0)
βββ MediaGalleryView
β βββ Main slideshow view
β βββ Zoom & pan support
β βββ Slideshow controls
β βββ 3D/2D projection toggle
β βββ Lazy rendering (only current + adjacent items)
βββ MediaGalleryGridView
β βββ Grid browsing interface
β βββ Multi-select mode
β βββ Filtering UI
β βββ SBS/TB thumbnail cropping
β βββ LazyThumbnailView for visibility-based loading (v1.1.0)
βββ VRVideoPlayerView (v2.0.0)
β βββ SceneKit sphere rendering for 360/180 video
β βββ Gyroscope + drag navigation
β βββ Metal fisheye shader
β βββ Projection picker overlay
βββ ThumbnailCache (v1.1.0)
β βββ LRU cache with memory limit
β βββ Memory pressure handling
β βββ ImageIO-based downsampling
βββ ZoomableMediaView
β βββ Individual media display
β βββ Gesture handling
β βββ Video playback
βββ ShareSheet
β βββ iOS: UIActivityViewController
β βββ macOS: NSSharingServicePicker
βββ AnimatedImageHelper
βββ Format detection
βββ Duration calculation
public protocol MediaItem: Identifiable, Sendable {
var id: UUID { get }
var type: MediaType { get }
var diskCacheKey: String? { get } // Optional disk caching
var sourceURL: URL? { get } // For animated image streaming
func loadImage() async -> PlatformImage?
func loadThumbnail(targetSize: CGFloat) async -> PlatformImage?
func loadVideoURL() async -> URL?
func loadAudioURL() async -> URL? // v1.6.0
func getAnimatedImageDuration() async -> TimeInterval?
func getVideoDuration() async -> TimeInterval?
func getAudioDuration() async -> TimeInterval? // v1.6.0
func getAudioMetadata() async -> AudioMetadata? // v1.6.0
func getShareableItem() async -> Any?
func getCaption() async -> String?
func hasAudioTrack() async -> Bool
}public struct AudioMetadata: Sendable {
public let title: String?
public let artist: String?
public let album: String?
public let trackNumber: Int?
public let year: Int?
public init(
title: String? = nil,
artist: String? = nil,
album: String? = nil,
trackNumber: Int? = nil,
year: Int? = nil
)
}public final class ThumbnailCache {
public static let shared: ThumbnailCache
public static let thumbnailSize: CGFloat = 200
public init(maxMemoryMB: Int = 100)
public func get(_ id: UUID) -> PlatformImage?
public func set(_ id: UUID, image: PlatformImage)
public func contains(_ id: UUID) -> Bool
public func clear()
public func handleMemoryPressure()
public var stats: (count: Int, memoryMB: Double)
// Efficient thumbnail generation using ImageIO
public static func createThumbnail(from image: PlatformImage, targetSize: CGFloat) -> PlatformImage
public static func createThumbnail(from data: Data, targetSize: CGFloat) -> PlatformImage?
public static func createThumbnail(from url: URL, targetSize: CGFloat) -> PlatformImage?
}@MainActor
public final class MediaDownloadManager: ObservableObject {
public static let shared: MediaDownloadManager
// Published state
@Published public private(set) var downloadState: DownloadState
@Published public private(set) var progress: DownloadProgress?
// Check cache status
public func isCached(mediaItem: any MediaItem) -> Bool
public func allCached(_ items: [any MediaItem]) -> Bool
public func anyCached(_ items: [any MediaItem]) -> Bool
public func cachedCount(of items: [any MediaItem]) -> Int
public func canCache(_ mediaItem: any MediaItem) -> Bool
// Get local file URL for cached media
public func localURL(for mediaItem: any MediaItem) -> URL?
// Download operations
public func downloadAll(
_ items: [any MediaItem],
headerProvider: @escaping @Sendable (URL) async -> [String: String]?
) async
public func cancelDownload()
// Clear cache
public func clearAllDownloads()
public func clearDownloads(for items: [any MediaItem])
// Cache statistics
public var stats: (fileCount: Int, diskMB: Double)
}
public enum DownloadState: Equatable, Sendable {
case idle
case downloading(completed: Int, total: Int)
case completed
case cancelled
case failed(String)
}
public struct DownloadProgress: Sendable {
public let completed: Int
public let total: Int
public let currentItemName: String?
public let bytesDownloaded: Int64
public let totalBytes: Int64
public var fractionCompleted: Double
public var currentItemProgress: Double
}
/// A button that manages downloading and clearing cached media files.
/// Shows three states: not cached, downloading, cached.
public struct MediaDownloadButton: View {
public init(
mediaItems: [any MediaItem],
headerProvider: @escaping @Sendable (URL) async -> [String: String]?
)
}
// States:
// - Not cached: Download icon (arrow.down.circle)
// - Partially cached: Dotted download icon (arrow.down.circle.dotted)
// - Downloading: Progress ring with stop button
// - Cached: Green checkmark (checkmark.circle.fill)@MainActor
public final class MediaPlaybackService: NSObject, ObservableObject {
public static let shared: MediaPlaybackService
// Notifications for external player integration
public static let shouldPauseForBackgroundNotification: Notification.Name
public static let externalPlayNotification: Notification.Name
public static let externalPauseNotification: Notification.Name
public static let externalSeekNotification: Notification.Name
public static let externalTrackChangedNotification: Notification.Name
// External playback mode (when views own the player)
public var externalPlaybackMode: Bool
// Playlist management
public func setPlaylist(_ mediaItems: [any MediaItem], startIndex: Int = 0)
public var currentIndex: Int
public var loopMode: PlaybackLoopMode
// Now Playing info for Control Center/Lock Screen
public func updateNowPlayingForCurrentItem() async
public func updateNowPlayingForExternalPlayer(
mediaItem: any MediaItem,
title: String?,
artist: String?,
album: String?,
artwork: PlatformImage?,
duration: TimeInterval,
isVideo: Bool
)
public func updateExternalPlaybackPosition(
currentTime: TimeInterval,
duration: TimeInterval,
isPlaying: Bool
)
// Picture-in-Picture (iOS only)
public func setupPiP(with playerLayer: AVPlayerLayer)
public func startPiP()
public func stopPiP()
public func togglePiP()
public var isPiPActive: Bool
public var isPiPPossible: Bool
}
public enum PlaybackLoopMode {
case off // Stop at end
case all // Loop entire playlist
case one // Repeat current track
}
public enum MediaType {
case image
case video
case animatedImage
case audio
}public struct MediaGalleryConfiguration {
public var slideshowDuration: TimeInterval = 5.0
public var showControls: Bool = true
public var backgroundColor: Color = .black
public var customActions: [MediaGalleryAction] = []
}public struct MediaGalleryAction: Identifiable {
public let id: UUID
public let icon: String
public let action: (Int) -> Void
public init(id: UUID = UUID(), icon: String, action: @escaping (Int) -> Void)
}public struct MediaGalleryMultiSelectAction: Identifiable {
public let id: UUID
public let title: String
public let icon: String
public let action: ([any MediaItem]) -> Void
public init(
id: UUID = UUID(),
title: String,
icon: String,
action: @escaping ([any MediaItem]) -> Void
)
}public struct MediaGalleryFilterConfig {
public var customFilter: ((any MediaItem) -> Bool)?
public var customSort: ((any MediaItem, any MediaItem) -> Bool)?
public init(
customFilter: ((any MediaItem) -> Bool)? = nil,
customSort: ((any MediaItem, any MediaItem) -> Bool)? = nil
)
}The package automatically detects and handles animated images:
- GIF: Graphics Interchange Format
- APNG: Animated PNG
- HEIF: High Efficiency Image Format sequences
- WebP: WebP animated images
Duration detection ensures animations play completely before advancing in slideshow mode.
- iOS: 17.0+
- macOS: 14.0+
- tvOS: 17.0+
- Swift: 5.9+
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
Blaine Miller - @blaineam
Project Link: https://github.com/blaineam/MediaStream
Made with β€οΈ and SwiftUI