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
flowchart LR
Scene["SceneDelegate"] --> List["MovieListViewController"]
Scene --> Player["MoviePlayerViewController"]
List --> Manager["CoordinationManager.shared"]
Manager --> Activity["MovieWatchingActivity"]
Manager --> Session["GroupSession"]
Manager --> Player
Player --> AVPlayer
AVPlayer --> Coordinator["AVPlayerPlaybackCoordinator"]
Coordinator --> Session
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
classDiagram
SceneDelegate *-- MovieListViewController
SceneDelegate *-- MoviePlayerViewController
CoordinationManager o-- GroupSession : externally supplied active session
CoordinationManager *-- AnyCancellable : session subscriptions
MoviePlayerViewController *-- AVPlayer
MoviePlayerViewController *-- AVPlayerViewController
MoviePlayerViewController --> CoordinationManager : observes shared
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
classDiagram
class GroupActivity {
<<protocol>>
}
MovieWatchingActivity ..|> GroupActivity
CoordinationManager --> MovieWatchingActivity
CoordinationManager --> GroupSession
MoviePlayerViewController --> AVPlayerPlaybackCoordinator
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
ViewControllersuffix; shared process roles useManagerandLibrary. - 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 |