Captions and subtitles make video and audio content accessible to deaf and hard of hearing users, as well as non-native speakers and users in sound-sensitive environments. In this post, we'll explore how to implement caption support in your SwiftUI apps and respect user preferences for displaying captions.

What are Captions?

Captions (also called subtitles or closed captions) are text overlays that display spoken dialogue, sound effects, and other audio information in video content. They enable:

  • Deaf and hard of hearing users to access audio content

  • Non-native speakers to better understand dialogue

  • Users in quiet environments where audio can't be played

  • Users in noisy environments where audio is hard to hear

  • Language learners to improve comprehension

  • Users who prefer reading to listening

There are two types of captions:

  • Closed Captions (CC): Can be turned on/off by the user

  • Open Captions: Always visible, burned into the video

In accessible apps, you should always provide closed captions so users can choose whether to display them.

Detecting User Caption Preferences

iOS provides an accessibility setting that allows users to indicate they prefer captions. You can detect this preference using the accessibilityShowsClosedCaptions environment variable:

import SwiftUI

struct CaptionPreferenceView: View {
    @Environment(\.accessibilityShowsClosedCaptions) 
    var showCaptions

    var body: some View {
        VStack(spacing: 20) {
            Text("Caption Preference")
                .font(.headline)

            if showCaptions {
                Text("✓ User prefers captions enabled")
                    .foregroundColor(.green)
            } else {
                Text("User has not enabled caption preference")
                    .foregroundColor(.secondary)
            }
        }
        .padding()
    }
}

Adding Captions to Video in SwiftUI

SwiftUI uses AVFoundation for video playback, which has built-in support for captions through subtitle tracks. Here's how to implement it:

Basic Video Player with Caption Support

import SwiftUI
import AVKit

struct VideoPlayerWithCaptions: View {
    @Environment(\.accessibilityShowsClosedCaptions) 
    var showCaptions

    @State private var player: AVPlayer?

    var body: some View {
        VStack {
            if let player = player {
                VideoPlayer(player: player)
                    .frame(height: 300)
                    .onAppear {
                        configureCaptions(for: player)
                    }
            } else {
                ProgressView("Loading video...")
                    .frame(height: 300)
            }

            Text("Video with captions")
                .font(.headline)
                .padding()
        }
        .onAppear {
            setupPlayer()
        }
    }

    func setupPlayer() {
        // Replace with your video URL
        guard let videoURL = Bundle.main.url(
            forResource: "sample",
            withExtension: "mp4"
        ) else { return }

        player = AVPlayer(url: videoURL)
    }

    func configureCaptions(for player: AVPlayer) {
        // Enable captions based on user preference
        guard let currentItem = player.currentItem else { return }

        // Get available media selection options
        let legibleGroup = currentItem.asset.mediaSelectionGroup(
            forMediaCharacteristic: .legible
        )

        if let legibleGroup = legibleGroup {
            // Find the appropriate caption track
            let selectedOption: AVMediaSelectionOption?

            if showCaptions {
                // Select caption track if available
                selectedOption = legibleGroup.options.first
            } else {
                // No captions
                selectedOption = nil
            }

            currentItem.select(selectedOption, in: legibleGroup)
        }
    }
}

Advanced Video Player with Caption Controls

struct AdvancedVideoPlayer: View {
    @Environment(\.accessibilityShowsClosedCaptions) 
    var userPrefersClosedCaptions

    @State private var player: AVPlayer?
    @State private var showCaptions: Bool = false
    @State private var availableCaptionLanguages: [String] = []
    @State private var selectedCaptionLanguage: String?

    var body: some View {
        VStack(spacing: 0) {
            if let player = player {
                VideoPlayer(player: player)
                    .frame(height: 300)
            }

            // Caption controls
            VStack(alignment: .leading, spacing: 16) {
                Toggle("Show Captions", isOn: $showCaptions)
                    .onChange(of: showCaptions) { newValue in
                        updateCaptions()
                    }

                if showCaptions && !availableCaptionLanguages.isEmpty {
                    VStack(alignment: .leading, spacing: 8) {
                        Text("Caption Language")
                            .font(.subheadline)
                            .foregroundColor(.secondary)

                        Picker("Language", selection: $selectedCaptionLanguage) {
                            ForEach(availableCaptionLanguages, id: \.self) { lang in
                                Text(lang).tag(lang as String?)
                            }
                        }
                        .pickerStyle(MenuPickerStyle())
                        .onChange(of: selectedCaptionLanguage) { _ in
                            updateCaptions()
                        }
                    }
                }
            }
            .padding()
        }
        .onAppear {
            setupPlayer()
            // Respect user's system preference
            showCaptions = userPrefersClosedCaptions
        }
    }

    func setupPlayer() {
        guard let videoURL = Bundle.main.url(
            forResource: "sample",
            withExtension: "mp4"
        ) else { return }

        player = AVPlayer(url: videoURL)

        // Load available caption languages
        loadAvailableCaptions()
    }

    func loadAvailableCaptions() {
        guard let currentItem = player?.currentItem else { return }

        let legibleGroup = currentItem.asset.mediaSelectionGroup(
            forMediaCharacteristic: .legible
        )

        if let options = legibleGroup?.options {
            availableCaptionLanguages = options.compactMap { option in
                option.displayName
            }

            if !availableCaptionLanguages.isEmpty {
                selectedCaptionLanguage = availableCaptionLanguages.first
            }
        }
    }

    func updateCaptions() {
        guard let currentItem = player?.currentItem,
              let legibleGroup = currentItem.asset.mediaSelectionGroup(
                forMediaCharacteristic: .legible
              ) else { return }

        if showCaptions, let selectedLang = selectedCaptionLanguage {
            // Find and select the caption track matching the language
            let option = legibleGroup.options.first { option in
                option.displayName == selectedLang
            }
            currentItem.select(option, in: legibleGroup)
        } else {
            // Turn off captions
            currentItem.select(nil, in: legibleGroup)
        }
    }
}

Creating Caption Files

Captions are typically provided in WebVTT (.vtt) or SRT (.srt) format. Here's an example WebVTT file:

WEBVTT

00:00:00.000 --> 00:00:02.000
Welcome to our app tutorial.

00:00:02.500 --> 00:00:05.000
Today we'll show you how to get started.

00:00:05.500 --> 00:00:08.000
[upbeat music playing]

00:00:08.500 --> 00:00:12.000
First, tap the plus button to create a new project.

Adding Caption Files to Your Video

import AVFoundation

func createPlayerItemWithCaptions(
    videoURL: URL,
    captionURL: URL
) -> AVPlayerItem {
    let asset = AVURLAsset(url: videoURL)
    let playerItem = AVPlayerItem(asset: asset)

    // Create a mutable composition
    let composition = AVMutableComposition()

    // Add video track
    guard let videoTrack = asset.tracks(
        withMediaType: .video
    ).first else {
        return playerItem
    }

    let compositionVideoTrack = composition.addMutableTrack(
        withMediaType: .video,
        preferredTrackID: kCMPersistentTrackID_Invalid
    )

    try? compositionVideoTrack?.insertTimeRange(
        CMTimeRangeMake(start: .zero, duration: asset.duration),
        of: videoTrack,
        at: .zero
    )

    // Add caption track
    let subtitleAsset = AVURLAsset(url: captionURL)

    if let subtitleTrack = subtitleAsset.tracks(
        withMediaType: .text
    ).first {
        let compositionSubtitleTrack = composition.addMutableTrack(
            withMediaType: .text,
            preferredTrackID: kCMPersistentTrackID_Invalid
        )

        try? compositionSubtitleTrack?.insertTimeRange(
            CMTimeRangeMake(start: .zero, duration: asset.duration),
            of: subtitleTrack,
            at: .zero
        )
    }

    return AVPlayerItem(asset: composition)
}

Best Practices for Captions

  1. Respect User Preferences: Always check accessibilityShowsClosedCaptions and enable captions by default if the user has expressed this preference.

  2. Provide Accurate Timing: Captions should appear in sync with the audio, ideally staying on screen long enough to be read comfortably.

  3. Include Sound Effects: Caption important non-speech sounds like [door closes], [applause], or [suspenseful music].

  4. Identify Speakers: When multiple people speak, identify who is talking, especially if they're off-screen.

  5. Use Standard Formatting:

    • Keep lines to 32-42 characters

    • Maximum 2 lines per caption

    • Display for 1-7 seconds based on reading speed

  6. Provide Caption Controls: Give users the ability to toggle captions on/off and select language if multiple tracks are available.

  7. Test Readability: Ensure captions are readable against your video content. Good players provide a semi-transparent background.

  8. Support Multiple Languages: Provide captions in multiple languages when possible for international users.

  9. Quality Over Auto-Generation: While auto-generated captions are better than nothing, human-reviewed captions are significantly more accurate and accessible.

Caption Styling

While native video players handle caption styling, if you're creating a custom player, ensure your captions have:

struct CustomCaptionStyle {
    static let textColor = Color.white
    static let backgroundColor = Color.black.opacity(0.75)
    static let font = Font.system(size: 18, weight: .semibold)
    static let padding: CGFloat = 8
    static let cornerRadius: CGFloat = 4
}

struct CaptionOverlay: View {
    let text: String

    var body: some View {
        Text(text)
            .font(CustomCaptionStyle.font)
            .foregroundColor(CustomCaptionStyle.textColor)
            .padding(CustomCaptionStyle.padding)
            .background(
                RoundedRectangle(cornerRadius: CustomCaptionStyle.cornerRadius)
                    .fill(CustomCaptionStyle.backgroundColor)
            )
            .multilineTextAlignment(.center)
    }
}

Wrap up

Captions help deaf and hard of hearing users, but they also benefit people watching in noisy environments or learning a language — they're a usability feature as much as an accessibility one. Use accessibilityShowsClosedCaptions to respect the user's system preference and enable them by default when it's set.

Resources:

Read more

Share


Share Bluesky Mastodon Twitter LinkedIn Facebook