Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
90
90%
Does it follow best practices?
Impact
—
Average score across 248 eval scenarios
Advisory
Suggest reviewing before use
High-level media playback UI built on AVFoundation. Provides system-standard video players, Picture-in-Picture, AirPlay routing, transport controls, and subtitle/caption display. Targets Swift 6.3 / iOS 26+.
Playback apps need an audio session category and the matching background mode when they support background audio, AirPlay, or PiP.
audio value in UIBackgroundModes).playbacksetActive(true) until playback begins so you do not interrupt other
audio prematurelyimport AVFoundation
func configureAudioSessionForPlayback() {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playback, mode: .moviePlayback)
} catch {
print("Audio session category failed: \(error)")
}
}
func activateAudioSessionWhenPlaybackBegins() {
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Audio session activation failed: \(error)")
}
}import AVKit // AVPlayerViewController, VideoPlayer, PiP
import AVFoundation // AVPlayer, AVPlayerItem, AVAssetAVPlayerViewController is the standard UIKit player. It provides system
playback controls, PiP, AirPlay, subtitles, and frame analysis out of the box.
Do not subclass it.
import AVKit
func presentPlayer(from viewController: UIViewController, url: URL) {
let player = AVPlayer(url: url)
let playerVC = AVPlayerViewController()
playerVC.player = player
viewController.present(playerVC, animated: true) {
player.play()
}
}Add AVPlayerViewController as a child view controller for inline playback.
Call addChild, add the view with constraints, then call didMove(toParent:).
func embedPlayer(in parent: UIViewController, container: UIView, url: URL) {
let playerVC = AVPlayerViewController()
playerVC.player = AVPlayer(url: url)
parent.addChild(playerVC)
container.addSubview(playerVC.view)
playerVC.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
playerVC.view.leadingAnchor.constraint(equalTo: container.leadingAnchor),
playerVC.view.trailingAnchor.constraint(equalTo: container.trailingAnchor),
playerVC.view.topAnchor.constraint(equalTo: container.topAnchor),
playerVC.view.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])
playerVC.didMove(toParent: parent)
}playerVC.showsPlaybackControls = true // Show/hide system controls
playerVC.videoGravity = .resizeAspect // .resizeAspectFill to crop
playerVC.entersFullScreenWhenPlaybackBegins = false
playerVC.exitsFullScreenWhenPlaybackEnds = true
playerVC.updatesNowPlayingInfoCenter = true // Auto-updates MPNowPlayingInfoCenterUse contentOverlayView to add non-interactive views (watermarks, logos)
between the video and transport controls.
Adopt AVPlayerViewControllerDelegate to respond to full-screen transitions,
PiP lifecycle events, interstitial playback, and media selection changes.
Use the transition coordinator's animate(alongsideTransition:completion:) to
synchronize your UI with full-screen animations.
Observe isReadyForDisplay before showing the player to avoid a black flash:
let observation = playerVC.observe(\.isReadyForDisplay) { observed, _ in
if observed.isReadyForDisplay {
// Safe to show the player view
}
}The VideoPlayer SwiftUI view wraps AVKit's playback UI.
import SwiftUI
import AVKit
struct PlayerView: View {
@State private var player: AVPlayer?
var body: some View {
Group {
if let player {
VideoPlayer(player: player)
.frame(height: 300)
} else {
ProgressView()
}
}
.task {
let url = URL(string: "https://example.com/video.m3u8")!
player = AVPlayer(url: url)
}
}
}Add a SwiftUI overlay above the video content and below the system playback controls. The overlay can be interactive, but it only receives events the system controls do not handle.
VideoPlayer(player: player) {
VStack {
Spacer()
HStack {
Image("logo")
.resizable()
.frame(width: 40, height: 40)
.padding()
Spacer()
}
}
}VideoPlayer does not expose all AVPlayerViewController properties. For PiP
configuration, delegate callbacks, or playback speed control, wrap
AVPlayerViewController in a UIViewControllerRepresentable. See the full
pattern in references/avkit-patterns.md.
PiP lets users watch video in a floating window while using other apps.
AVPlayerViewController supports PiP automatically once the app is configured,
the device supports PiP, and the current AVPlayerItem is playable video
content in an AVPlayer-compatible format. Audio-only items, unsupported
containers/codecs, or items that are not ready to display video can make PiP
unavailable even when app and device setup are correct. For custom player UIs,
use AVPictureInPictureController directly.
.playback (see Setup)AVPlayerItem with playable video media, not audio-only contentisPictureInPicturePossiblePiP is enabled by default on AVPlayerViewController. Control automatic
activation and inline-to-PiP transitions:
let playerVC = AVPlayerViewController()
playerVC.player = player
// PiP enabled by default; set false to disable
playerVC.allowsPictureInPicturePlayback = true
// Auto-start PiP when app backgrounds (for inline/non-fullscreen players)
playerVC.canStartPictureInPictureAutomaticallyFromInline = trueWhen the user taps the restore button in PiP, implement the delegate method to
re-present your player. Call the completion handler with true to signal the
system to finish the restore animation.
func playerViewController(
_ playerViewController: AVPlayerViewController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
// Re-present or re-embed the player view controller
present(playerViewController, animated: false) {
completionHandler(true)
}
}For custom player UIs, use AVPictureInPictureController with an AVPlayerLayer
or sample buffer content source. Check device support before creating PiP UI,
then check the controller's isPictureInPicturePossible before starting PiP in
the current playback context. See references/avkit-patterns.md
for full custom player and sample buffer PiP patterns.
guard AVPictureInPictureController.isPictureInPictureSupported() else { return }
let pipController = AVPictureInPictureController(playerLayer: playerLayer)
pipController.delegate = self
pipController.canStartPictureInPictureAutomaticallyFromInline = true
// Call this from the user's PiP button action, never automatically.
if pipController.isPictureInPicturePossible {
pipController.startPictureInPicture()
}Interstitial breaks can come from the media stream/manifest, which AVFoundation
exposes through AVPlayerItem.interstitialTimeRanges, or from an app-owned
AVPlayerInterstitialEventController schedule. Do not assign
interstitialTimeRanges directly on iOS. Use requiresLinearPlayback only to
prevent seeking during required ad or legal segments:
// During an ad
playerVC.requiresLinearPlayback = true
// After the ad completes
playerVC.requiresLinearPlayback = falseAVPlayerViewController supports AirPlay automatically when app configuration,
media, routes, and device support allow external playback. No additional code is
required when using the standard player. The system displays the AirPlay button
in the transport controls when AirPlay-capable devices are available.
Add a standalone AirPlay route picker button outside the player UI:
import AVKit
func addRoutePicker(to containerView: UIView) {
let routePicker = AVRoutePickerView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
routePicker.activeTintColor = .systemBlue
routePicker.prioritizesVideoDevices = true // Show video-capable routes first
containerView.addSubview(routePicker)
}AVPlayer allows external playback by default. Leave it enabled for AirPlay,
or set it explicitly when code elsewhere may disable it:
player.allowsExternalPlayback = trueSet usesExternalPlaybackWhileExternalScreenIsActive only when you want the
player to automatically switch to external playback while an external screen
mode is active.
Provide user-selectable playback speeds in the player UI:
let playerVC = AVPlayerViewController()
playerVC.speeds = [
AVPlaybackSpeed(rate: 0.5, localizedName: "Half Speed"),
AVPlaybackSpeed(rate: 1.0, localizedName: "Normal"),
AVPlaybackSpeed(rate: 1.5, localizedName: "1.5x"),
AVPlaybackSpeed(rate: 2.0, localizedName: "Double Speed")
]Use AVPlaybackSpeed.systemDefaultSpeeds to restore the default speed options.
On iOS, use the standard transport controls and AVPlayer.seek(...) for custom
app controls. AVPlayerViewController skipping behavior APIs such as
isSkipForwardEnabled, isSkipBackwardEnabled, and skippingBehavior are
tvOS-focused; keep them out of iOS player implementations.
AVPlayerViewController updates MPNowPlayingInfoCenter automatically by
default. Disable this if you manage Now Playing info manually:
playerVC.updatesNowPlayingInfoCenter = falseAVKit handles subtitle and closed caption display automatically when the media contains appropriate text tracks. Users control subtitle preferences in Settings > Accessibility > Subtitles & Captioning.
let asset = player.currentItem?.asset
if let group = try await asset?.loadMediaSelectionGroup(for: .legible),
let english = group.options.first(where: { option in
option.locale?.language.languageCode?.identifier == "en"
}) {
player.currentItem?.select(english, in: group)
}allowedSubtitleOptionLanguages, requiresFullSubtitles, and the
AVPlayerViewControllerDelegate media-selection callback are tvOS-only. For iOS,
load the asset's .legible media selection group and select an option on the
AVPlayerItem when the app needs a default.
Subtitles and closed captions are embedded in HLS manifests. AVKit reads them
from AVMediaSelectionGroup on the AVAsset. For local files, use media that
already includes legible subtitle or closed-caption tracks, or author those
tracks into the playable asset before presenting it with AVKit.
Apple explicitly states this is unsupported. It may cause undefined behavior or crash on future OS versions.
// WRONG
class MyPlayerVC: AVPlayerViewController { } // Unsupported
// CORRECT: Use composition with delegation
let playerVC = AVPlayerViewController()
playerVC.delegate = coordinatorPiP and background playback depend on the playback audio session category and the Audio, AirPlay, and Picture in Picture background mode.
// WRONG: Default audio session
let playerVC = AVPlayerViewController()
playerVC.player = player // PiP won't work
// CORRECT: Configure the category, then activate when playback starts
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
try AVAudioSession.sharedInstance().setActive(true)
let playerVC = AVPlayerViewController()
playerVC.player = playerWithout restoreUserInterfaceForPictureInPictureStopWithCompletionHandler, the
system cannot return the user to your player. Failing to call
completionHandler(true) leaves the system in an inconsistent state.
// WRONG: No delegate method or missing completionHandler call
// User taps restore in PiP -> nothing happens or animation hangs
// CORRECT
func playerViewController(
_ playerViewController: AVPlayerViewController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
present(playerViewController, animated: false) {
completionHandler(true)
}
}Creating the player eagerly causes performance issues. SwiftUI may recreate the view multiple times.
// WRONG: Created on every view init
struct PlayerView: View {
let player = AVPlayer(url: videoURL) // Re-created on every view evaluation
var body: some View { VideoPlayer(player: player) }
}
// CORRECT: Use @State and defer creation
struct PlayerView: View {
@State private var player: AVPlayer?
var body: some View {
VideoPlayer(player: player)
.task { player = AVPlayer(url: videoURL) }
}
}.playback with mode: .moviePlaybackUIBackgroundModesAVPlayerViewController is not subclassedcompletionHandler(true)isPictureInPicturePossibleAVPlayer deferred to .task in SwiftUI (not created eagerly)canStartPictureInPictureAutomaticallyFromInline set for inline playersrequiresLinearPlayback toggled only during required ad/legal segments.resizeAspect vs .resizeAspectFill)isReadyForDisplay observed before showing the player view.tessl-plugin
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
references
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references