ivan-magda/swiftui-background-video
Seamless looping background videos for SwiftUI-because `VideoPlayer` can't loop and nobody wants 80 lines of AVPlayerLooper boilerplate.
Why SwiftUIBackgroundVideo?
SwiftUI's native VideoPlayer can't loop videos or hide controls-it's designed for interactive playback, not backgrounds. The standard fix requires wrapping AVQueuePlayer + AVPlayerLooper in UIViewRepresentable, handling app lifecycle (background/foreground), audio interruptions, and memory management.
This package does all of that in 3 lines:
BackgroundVideoView(resourceName: "background", resourceType: "mp4")Alternatives:
| Package | Verdict | | ---------------------------- | ------------------------------------------------------------------------------------------------ | | swiftui-loop-videoPlayer | Feature-heavy (subtitles, Metal shaders, PiP). Good if you need those; overkill for backgrounds. | | SwiftVideoBackground | UIKit-only, last updated 2019, no SPM support. | | DIY | 80+ lines of boilerplate you'll copy from Stack Overflow anyway. |
Features
- 3-line integration - Drop
BackgroundVideoViewinto any SwiftUI view - Truly seamless loops - Uses
AVPlayerLooper, not notification-based seeking (no 100ms gaps) - Asset caching -
NSCache-backed, max 3 assets, auto-clears on memory warning - Lifecycle-aware - Auto-pauses on background, resumes on foreground
- Audio interruption handling - Phone calls won't break your player
- UIKit support - Use
BackgroundVideoUIViewdirectly if needed - iOS 13+ - Works on 99%+ of devices in the wild
Requirements
- iOS 13.0+
- Swift 6.0+
- Xcode 16+
Installation
Swift Package Manager
Add SwiftUIBackgroundVideo to your project by adding it as a dependency in your Package.swift file:
dependencies: [
.package(url: "https://github.com/ivan-magda/swiftui-background-video.git", from: "1.3.0")
]Or add it directly through Xcode:
- Go to File → Add Packages...
- Enter package URL:
https://github.com/ivan-magda/swiftui-background-video.git - Click Add Package
Usage
SwiftUI
import SwiftUI
import SwiftUIBackgroundVideo
struct ContentView: View {
var body: some View {
ZStack {
BackgroundVideoView(
resourceName: "background_video",
resourceType: "mp4"
)
.ignoresSafeArea()
Text("Hello, World!")
.foregroundStyle(.white)
.font(.largeTitle)
.padding()
.background(Color.black.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}Handling State Changes
Monitor loading, playback, and errors:
import SwiftUI
import SwiftUIBackgroundVideo
struct ContentView: View {
@State private var isLoading = true
@State private var errorMessage: String?
var body: some View {
ZStack {
BackgroundVideoView(
resourceName: "background_video",
resourceType: "mp4"
) { state in
switch state {
case .idle:
break
case .loading:
isLoading = true
case .playing:
isLoading = false
case .paused:
break
case .failed(let error):
isLoading = false
errorMessage = error.localizedDescription
}
}
.ignoresSafeArea()
if isLoading {
ProgressView()
.scaleEffect(1.5)
}
if let error = errorMessage {
Text("Error: \(error)")
.foregroundStyle(.red)
}
}
}
}UIKit
import UIKit
import SwiftUIBackgroundVideo
class ViewController: UIViewController {
private var videoView: BackgroundVideoUIView?
override func viewDidLoad() {
super.viewDidLoad()
// Create and add the video view
videoView = BackgroundVideoUIView(
frame: view.bounds,
resourceName: "background_video",
resourceType: "mp4"
)
videoView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
if let videoView = videoView {
view.addSubview(videoView)
}
// Monitor state changes
videoView?.stateDidChange = { state in
switch state {
case .loading:
print("Loading video...")
case .playing:
print("Video playing")
case .paused:
print("Video paused")
case .failed(let error):
print("Error: \(error.localizedDescription)")
default:
break
}
}
// Add content on top
let label = UILabel()
label.text = "Hello, World!"
label.textColor = .white
label.font = .preferredFont(forTextStyle: .largeTitle)
label.textAlignment = .center
label.backgroundColor = UIColor.black.withAlphaComponent(0.5)
label.layer.cornerRadius = 10
label.clipsToBounds = true
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
label.widthAnchor.constraint(lessThanOrEqualTo: view.widthAnchor, constant: -40)
])
}
}API Reference
BackgroundVideoView (SwiftUI)
| Parameter | Type | Description | | ---------------- | ------------------------------- | ---------------------------------------------- | | resourceName | String | Video filename without extension | | resourceType | String | File extension (e.g., "mp4", "mov") | | bundle | Bundle | Bundle containing the video (default: .main) | | onStateChanged | ((VideoPlayerState) -> Void)? | Optional state change callback |
BackgroundVideoUIView (UIKit)
| Property/Method | Type | Description | | ------------------------------------------ | ------------------------------- | ----------------------------------- | | stateDidChange | ((VideoPlayerState) -> Void)? | State change callback | | playerState | VideoPlayerState | Current playback state (read-only) | | prepareAndPlayVideo(with:ofType:bundle:) | Method | Load and play a different video | | cleanupPlayer() | Method | Stop playback and release resources |
VideoPlayerState
| Case | Description | | ---------------- | ----------------------------------------- | | .idle | Player initialized, no video loaded | | .loading | Video asset loading asynchronously | | .playing | Video actively playing | | .paused | Playback paused (background/interruption) | | .failed(Error) | Loading or playback failed |
VideoPlayerError
| Case | Description | | ------------------- | ------------------------------- | | .resourceNotFound | Video file not in app bundle | | .invalidResource | File exists but can't be played | | .playbackFailed | Runtime playback error |
How It Works
Under the hood, SwiftUIBackgroundVideo uses Apple's recommended approach for seamless video looping:
- AVQueuePlayer + AVPlayerLooper - The "treadmill pattern" from WWDC 2016 that cycles player items without gaps
- AVPlayerLayer - Hardware-accelerated video rendering with aspect-fill scaling
- NSCache - Lightweight asset caching (max 3 videos) with automatic memory warning cleanup
- NotificationCenter - Observes
willEnterForeground,didEnterBackground, andinterruptionNotificationfor proper lifecycle handling
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Package Metadata
Repository: ivan-magda/swiftui-background-video
Default branch: main
README: README.md