โถ๏ธ AVKit
โญ Difficulty: โญโญ
โฑ๏ธ Est. Time: 1h
๐ Graphics & Media
An advanced framework for video player UI and controls
iOS 8+SwiftUI Supported
โจ AVKit is?
AVKit is a framework that provides a complete user interface for video playback. Built on top of AVFoundation, it makes it easy to implement professional video player features like playback controls, Picture-in-Picture, AirPlay, subtitles, and chapter navigation. It supports both UIKit's AVPlayerViewController and SwiftUI's VideoPlayer.
๐ก Key Features: Playback Controls ยท Picture-in-Picture ยท AirPlay ยท Subtitles ยท Chapters ยท Speed Control ยท Full Screen ยท Gestures ยท System Integration
๐ฌ 1. ๊ธฐ๋ณธ ๋น๋์ค ํ๋ ์ด์ด (SwiftUI)
Play video easily with SwiftUI's VideoPlayer.
VideoPlayerView.swift โ SwiftUI Video Player
import SwiftUI import AVKit struct SimpleVideoPlayer: View { let url: URL var body: some View { VideoPlayer(player: AVPlayer(url: url)) .frame(height: 300) } } // ์ฌ์ฉ ์์ struct ContentView: View { var body: some View { VStack { if let url = URL(string: "https://example.com/video.mp4") { SimpleVideoPlayer(url: url) } } } } // ์ปค์คํ ์ปจํธ๋กค์ด ์๋ VideoPlayer struct VideoPlayerWithOverlay: View { @State private var player: AVPlayer @State private var isPlaying = false init(url: URL) { _player = State(initialValue: AVPlayer(url: url)) } var body: some View { VideoPlayer(player: player) { // ์ปค์คํ ์ค๋ฒ๋ ์ด VStack { Text("๋๋ง์ ๋น๋์ค") .font(.title) .foregroundStyle(.white) .padding() Spacer() HStack { Button { if isPlaying { player.pause() } else { player.play() } isPlaying.toggle() } label: { Image(systemName: isPlaying ? "pause.fill" : "play.fill") .font(.largeTitle) .foregroundStyle(.white) } } .padding() } } .frame(height: 300) } }
๐ฑ 2. AVPlayerViewController (UIKit)
Build a full-featured player using AVPlayerViewController in UIKit.
PlayerViewController.swift โ UIKit Player
import SwiftUI import AVKit struct PlayerView: UIViewControllerRepresentable { let player: AVPlayer func makeUIViewController(context: Context) -> AVPlayerViewController { let controller = AVPlayerViewController() controller.player = player // ์ฌ์ ์ปจํธ๋กค ์ค์ controller.showsPlaybackControls = true // Picture-in-Picture ํ์ฉ controller.allowsPictureInPicturePlayback = true // ๋น๋์ค ์ค๋ ฅ ์ค์ controller.videoGravity = .resizeAspect // ์๋ง ํ์ controller.canStartPictureInPictureAutomaticallyFromInline = true return controller } func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {} } // ์ฌ์ฉ ์์ struct VideoPlayerScreen: View { @State private var player: AVPlayer? var body: some View { VStack { if let player = player { PlayerView(player: player) .frame(height: 300) } else { ProgressView() } } .onAppear { loadVideo() } } func loadVideo() { guard let url = URL(string: "https://example.com/video.mp4") else { return } player = AVPlayer(url: url) } }
๐บ 3. Picture-in-Picture
Implement Picture-in-Picture to play video in a floating window.
PiPPlayerView.swift โ Picture-in-Picture
import SwiftUI import AVKit @Observable class PiPManager: NSObject { var pipController: AVPictureInPictureController? var isPiPActive = false var isPiPSupported = false func setupPiP(with playerLayer: AVPlayerLayer) { // PiP ์ง์ ํ์ธ isPiPSupported = AVPictureInPictureController.isPictureInPictureSupported() guard isPiPSupported else { return } // PiP ์ปจํธ๋กค๋ฌ ์์ฑ pipController = AVPictureInPictureController(playerLayer: playerLayer) pipController?.delegate = self // ์ค๋์ค ์ธ์ ์ค์ try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback) try? AVAudioSession.sharedInstance().setActive(true) } func startPiP() { pipController?.startPictureInPicture() } func stopPiP() { pipController?.stopPictureInPicture() } } extension PiPManager: AVPictureInPictureControllerDelegate { func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { isPiPActive = true } func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { isPiPActive = false } func pictureInPictureController( _ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error ) { print("PiP ์์ ์คํจ: \(error.localizedDescription)") } } struct PiPPlayerView: View { @State private var player: AVPlayer @State private var pipManager = PiPManager() init(url: URL) { _player = State(initialValue: AVPlayer(url: url)) } var body: some View { VStack { PiPVideoView(player: player, pipManager: pipManager) .frame(height: 300) Button { if pipManager.isPiPActive { pipManager.stopPiP() } else { pipManager.startPiP() } } label: { Label( pipManager.isPiPActive ? "PiP ์ข ๋ฃ" : "PiP ์์", systemImage: "pip" ) } .disabled(!pipManager.isPiPSupported) .buttonStyle(.borderedProminent) .padding() } } } struct PiPVideoView: UIViewRepresentable { let player: AVPlayer let pipManager: PiPManager func makeUIView(context: Context) -> UIView { let view = UIView() let playerLayer = AVPlayerLayer(player: player) playerLayer.videoGravity = .resizeAspect view.layer.addSublayer(playerLayer) // PiP ์ค์ pipManager.setupPiP(with: playerLayer) DispatchQueue.main.async { playerLayer.frame = view.bounds } return view } func updateUIView(_ uiView: UIView, context: Context) { if let layer = uiView.layer.sublayers?.first as? AVPlayerLayer { layer.frame = uiView.bounds } } }
๐ก 4. AirPlay
Stream video to other devices via AirPlay.
AirPlayView.swift โ AirPlay Supported
import SwiftUI import AVKit struct AirPlayButton: UIViewRepresentable { func makeUIView(context: Context) -> AVRoutePickerView { let routePickerView = AVRoutePickerView() routePickerView.tintColor = .UIColor.systemBlue routePickerView.activeTintColor = .UIColor.systemGreen routePickerView.prioritizesVideoDevices = true return routePickerView } func updateUIView(_ uiView: AVRoutePickerView, context: Context) {} } struct VideoPlayerWithAirPlay: View { let url: URL @State private var player: AVPlayer? var body: some View { VStack { if let player = player { VideoPlayer(player: player) .frame(height: 300) } HStack { Text("AirPlay๋ก ์ฌ์") .font(.headline) Spacer() AirPlayButton() .frame(width: 40, height: 40) } .padding() } .onAppear { setupPlayer() } } func setupPlayer() { player = AVPlayer(url: url) // AirPlay๋ฅผ ์ํ ์ค๋์ค ์ธ์ let audioSession = AVAudioSession.sharedInstance() try? audioSession.setCategory(.playback, mode: .moviePlayback) try? audioSession.setActive(true) // ์ธ๋ถ ์ฌ์ ํ์ฉ player?.allowsExternalPlayback = true player?.usesExternalPlaybackWhileExternalScreenIsActive = true } }
โ๏ธ 5. ์ปค์คํ ์ปจํธ๋กค
์์ ํ ์ปค์คํฐ๋ง์ด์ฆ๋ ์ฌ์ ์ปจํธ๋กค์ ๋ง๋ญ.
CustomPlayerView.swift โ Custom Controls
import SwiftUI import AVKit import Combine @Observable class CustomPlayerManager { var player: AVPlayer var isPlaying = false var currentTime: Double = 0 var duration: Double = 0 var playbackRate: Float = 1.0 var volume: Float = 1.0 private var timeObserver: Any? init(url: URL) { self.player = AVPlayer(url: url) setupObservers() } func setupObservers() { // ์ฌ์ ์๊ฐ ๊ด์ฐฐ timeObserver = player.addPeriodicTimeObserver( forInterval: CMTime(seconds: 0.5, preferredTimescale: 600), queue: .main ) { [weak self] time in self?.currentTime = time.seconds } // ์ฌ์ ์๋ฃ ๊ฐ์ง NotificationCenter.default.addObserver( forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main ) { [weak self] _ in self?.isPlaying = false self?.player.seek(to: .zero) } // ์์ ๊ธธ์ด ๋ก๋ Task { if let asset = player.currentItem?.asset, let duration = try? await asset.load(.duration) { self.duration = duration.seconds } } } func togglePlayPause() { if isPlaying { player.pause() } else { player.play() } isPlaying.toggle() } func seek(to time: Double) { let cmTime = CMTime(seconds: time, preferredTimescale: 600) player.seek(to: cmTime) } func skipForward(_ seconds: Double = 10) { seek(to: currentTime + seconds) } func skipBackward(_ seconds: Double = 10) { seek(to: max(0, currentTime - seconds)) } func changePlaybackRate(to rate: Float) { playbackRate = rate player.rate = rate } func changeVolume(to volume: Float) { self.volume = volume player.volume = volume } } struct CustomPlayerView: View { @State private var playerManager: CustomPlayerManager @State private var showControls = true init(url: URL) { _playerManager = State(initialValue: CustomPlayerManager(url: url)) } var body: some View { ZStack { // ๋น๋์ค ๋ ์ด์ด VideoLayer(player: playerManager.player) .onTapGesture { withAnimation { showControls.toggle() } } // ์ปค์คํ ์ปจํธ๋กค if showControls { VStack { Spacer() // ์งํ ๋ฐ VStack(spacing: 8) { Slider( value: Binding( get: { playerManager.currentTime }, set: { playerManager.seek(to: $0) } ), in: 0...max(playerManager.duration, 1) ) .tint(.white) HStack { Text(formatTime(playerManager.currentTime)) Spacer() Text(formatTime(playerManager.duration)) } .font(.caption) .foregroundStyle(.white) } .padding(.horizontal) // ์ฌ์ ์ปจํธ๋กค HStack(spacing: 40) { Button { playerManager.skipBackward() } label: { Image(systemName: "gobackward.10") .font(.title) } Button { playerManager.togglePlayPause() } label: { Image(systemName: playerManager.isPlaying ? "pause.fill" : "play.fill") .font(.title) } Button { playerManager.skipForward() } label: { Image(systemName: "goforward.10") .font(.title) } Menu { Button("0.5x") { playerManager.changePlaybackRate(to: 0.5) } Button("1.0x") { playerManager.changePlaybackRate(to: 1.0) } Button("1.5x") { playerManager.changePlaybackRate(to: 1.5) } Button("2.0x") { playerManager.changePlaybackRate(to: 2.0) } } label: { Image(systemName: "gauge") .font(.title) } } .foregroundStyle(.white) .padding() } .background( LinearGradient( colors: [.Color.clear, .Color.black.opacity(0.7)], startPoint: .top, endPoint: .bottom ) ) .transition(.opacity) } } .frame(height: 300) .background(Color.black) } func formatTime(_ time: Double) -> String { let minutes = Int(time) / 60 let seconds = Int(time) % 60 return String(format: "%d:%02d", minutes, seconds) } } struct VideoLayer: UIViewRepresentable { let player: AVPlayer func makeUIView(context: Context) -> UIView { let view = UIView() let playerLayer = AVPlayerLayer(player: player) playerLayer.videoGravity = .resizeAspect view.layer.addSublayer(playerLayer) DispatchQueue.main.async { playerLayer.frame = view.bounds } return view } func updateUIView(_ uiView: UIView, context: Context) { if let layer = uiView.layer.sublayers?.first as? AVPlayerLayer { layer.frame = uiView.bounds } } }
๐ฑ Complete Example
VideoApp.swift โ App
import SwiftUI import AVKit struct VideoApp: View { @State private var selectedVideo: VideoItem? let videos = [ VideoItem(title: "์ํ ๋น๋์ค 1", url: URL(string: "https://example.com/video1.mp4")!), VideoItem(title: "์ํ ๋น๋์ค 2", url: URL(string: "https://example.com/video2.mp4")!), ] var body: some View { NavigationStack { List(videos) { video in Button { selectedVideo = video } label: { HStack { Image(systemName: "play.rectangle.fill") .font(.largeTitle) .foregroundStyle(.blue) Text(video.title) .font(.headline) } } } .navigationTitle("๋น๋์ค") .sheet(item: $selectedVideo) { video in VideoPlayerSheet(video: video) } } } } struct VideoPlayerSheet: View { let video: VideoItem @Environment(\.dismiss) var dismiss var body: some View { NavigationStack { CustomPlayerView(url: video.url) .navigationTitle(video.title) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("๋ซ๊ธฐ") { dismiss() } } } } } } struct VideoItem: Identifiable { let id = UUID() let title: String let url: URL }
๐ก HIG Guidelines
- ๋ค์ดํฐ๋ธ ์ปจํธ๋กค: Use AVPlayerViewController's default controls when possible.
- ์ ์ค์ฒ: ํญ์ผ๋ก ์ปจํธ๋กค ํ์/์จ๊น, ์ค์์ดํ๋ก ํ์
- Picture-in-Picture: ๋ฉํฐํ์คํน์ ์ํด PiP Supported
- AirPlay: ์ธ๋ถ ๋์คํ๋ ์ด ์ฌ์ ํ์ฉ
- Background: In Info.plist, ๋ฐฑ๊ทธ๋ผ์ด๋ ๋ชจ๋ ์ถ๊ฐ
- ์ ๊ทผ์ฑ: ์๋ง ๋ฐ ์ค๋์ค ์ค๋ช Supported
๐ฏ Practical Usage
- Streaming Apps: Netflix, YouTube ์คํ์ผ ํ๋ ์ด์ด
- ๊ต์ก ํ๋ซํผ: ๊ฐ์ ๋น๋์ค ์ฌ์
- Social Media: ์ธํผ๋ ๋น๋์ค ์ฌ์
- ๋ผ์ด๋ธ ๋ฐฉ์ก: ์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ ์์ฒญ
- Video Editing: ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ๋ ์ด์ด
๐ Learn More
โก๏ธ Performance Tips: Using AVPlayerViewController lets the system automatically provide an optimized playback experience. When building custom controls,
addPeriodicTimeObserver โ don't set the interval too short. About 0.5 seconds is appropriate.