Sample CodeiOS 15.0+, iPadOS 15.0+, Mac Catalyst 15.0+Reviewed 2026-07-21View on Apple Developer

Supporting coordinated media playback

At a glance

Item Summary
Purpose Coordinate movie selection and AVPlayer transport across a SharePlay group session.
App architecture UIKit view controllers share a singleton CoordinationManager; Combine publishes movie/session changes into list and player controllers.
Main patterns MVC, mediator/shared coordinator, observer, framework protocol conformance.
Project style App bootstrap, Model, and UI folders plus feature-level coordination/player/activity files.

Project structure

GroupWatching/
├── App/
│   ├── AppDelegate.swift
│   └── SceneDelegate.swift
├── Model/
│   ├── Library.swift
│   └── Poster.swift
├── UI/
│   ├── MovieListViewController.swift
│   ├── MovieDetailViewController.swift
│   └── MovieInfoViewController.swift
├── CoordinationManager.swift
├── MovieWatchingActivity.swift
└── MoviePlayerViewController.swift

The app uses UIKit MVC but moves cross-controller SharePlay state into one coordinator instead of placing it in a view controller.

Overall architecture

Reference code

GroupWatching/CoordinationManager.swift:23 — one shared object accepts sessions and publishes the selected movie (abridged).

private init() {
    Task {
        for await groupSession in MovieWatchingActivity.sessions() {
            self.groupSession = groupSession
            groupSession.join()
            groupSession.$activity.sink { [weak self] activity in
                self?.enqueuedMovie = activity.movie
            }.store(in: &subscriptions)
        }
    }
}

The manager is a mediator between framework session lifecycle and controllers. It does not own playback; the player controller does.

Ownership and state

Ownership evidence

GroupWatching/MoviePlayerViewController.swift:15 — playback ownership stays in the player controller (abridged).

class MoviePlayerViewController: UIViewController {
    private let player = AVPlayer()
    private var groupSession: GroupSession<MovieWatchingActivity>? {
        didSet {
            guard let session = groupSession else {
                player.rate = 0
                return
            }
            player.playbackCoordinator.coordinateWithSession(session)
        }
    }
}
Owner Object or state Relationship Mutation authority
SceneDelegate Root controllers Creates and composes phone/tablet layouts Scene bootstrap only.
CoordinationManager Active group session, enqueued movie Process-wide shared references Session loop and prepareToPlay update published state.
MoviePlayerViewController AVPlayer, player VC, subscriptions Creates and retains Player controller replaces items and coordinates the session.
Library Decoded movie list Singleton immutable-ish catalog Private initializer loads bundled data.

Class and protocol design

Reference code

GroupWatching/MovieWatchingActivity.swift:22 — a value type is the serialized group-activity contract.

struct MovieWatchingActivity: GroupActivity {
    let movie: Movie

    var metadata: GroupActivityMetadata {
        var metadata = GroupActivityMetadata()
        metadata.type = .watchTogether
        metadata.fallbackURL = movie.url
        metadata.title = movie.title
        return metadata
    }
}

The sample has no app-defined coordinator protocol. Its protocol-oriented boundary is the framework GroupActivity conformance; controller decoupling is provided by Combine publications.

Access control

Symbol Access Verified effect Likely rationale
CoordinationManager.init private Only shared can construct the coordinator. Enforces one active session authority.
Manager subscriptions private Consumers cannot cancel session observation. Protects the shared lifecycle.
Player, session, movie private Other controllers cannot directly mutate playback internals. All changes arrive through manager publications or player methods.
Library.init and loader private Catalog construction is singleton-controlled. Avoids repeated bundled decoding.
enqueuedMovie, groupSession internal @Published var Any target code can mutate them, not just the manager. Educational simplicity; a production boundary could use private(set).

GroupWatching/CoordinationManager.swift:13:

class CoordinationManager {
    static let shared = CoordinationManager()
    private var subscriptions = Set<AnyCancellable>()
    @Published var enqueuedMovie: Movie?
    @Published var groupSession: GroupSession<MovieWatchingActivity>?
    private init() { /* session loop */ }
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Session discovery, activation, movie publication CoordinationManager Shared cross-controller workflow.
Playback item and playback coordinator MoviePlayerViewController Keeps AVPlayer lifetime with its presentation controller.
Movie selection MovieListViewController UI sends one intent to the coordinator.
Activity metadata/value contract MovieWatchingActivity.swift Codable value crosses the group-session boundary.
Root composition SceneDelegate Selects phone vs. split-view controller hierarchy.

Design patterns

Pattern Source evidence Purpose or tradeoff
MVC GroupWatching/UI/MovieListViewController.swift:11 Controllers own UI behavior and observe shared state.
Mediator/shared coordinator GroupWatching/CoordinationManager.swift:13 Prevents each controller from managing SharePlay independently.
Observer GroupWatching/MoviePlayerViewController.swift:68 Combine projects movie/session publications into private player state.
Framework protocol GroupWatching/MovieWatchingActivity.swift:23 Encapsulates the activity payload and system metadata.

Naming conventions

  • Controllers use the UIKit ViewController suffix; shared process roles use Manager and Library.
  • Published values name domain state (enqueuedMovie, groupSession) rather than UI elements.
  • Commands state intent (prepareToPlay, performWhatHappened); callbacks use framework vocabulary.
  • Model files use concise domain nouns: Movie, Poster, Library.

Architecture takeaways

  • Keep group-session state in one mediator and playback state in the player owner.
  • Publish domain values—not view commands—to synchronize multiple controllers.
  • Use the framework playback coordinator rather than implementing transport synchronization.
  • Private initialization communicates singleton lifecycle, but writable publications leave mutation broadly internal.

Source map

Source file Relevant symbols
GroupWatching/CoordinationManager.swift Session/activity mediator
GroupWatching/MovieWatchingActivity.swift Movie, group activity
GroupWatching/MoviePlayerViewController.swift Player ownership/coordination
GroupWatching/UI/MovieListViewController.swift Selection intent
GroupWatching/App/SceneDelegate.swift Root controller composition