This developer guide describes how to add Google Cast support to your iOS
sender app using the iOS Sender SDK.
**Note:** We recommend that you try the following:

- [**Cast iOS Sender codelab
  tutorial**](https://developers.google.com/cast/codelabs/cast-videos-ios) - Learn step-by-step how to enable an existing iOS video app to use a Google Cast device to cast videos to a TV.
- [**CastVideos-ios sample app**](https://github.com/googlecast/CastVideos-ios) (GitHub) - Run, navigate, and view this reference sample app which complies with the [UX Guidelines](https://developers.google.com/cast/docs/ux_guidelines.md.txt) and [Design Checklist](https://developers.google.com/cast/docs/design_checklist.md.txt).

The mobile device or laptop is the *sender* which controls the playback, and the
Google Cast device is the *receiver* which displays the content on the TV.

The *sender framework* refers to the Cast class library binary and associated
resources present at runtime on the sender. The *sender app* or *Cast app*
refers to an app also running on the sender. The *Web Receiver app*
refers to the HTML application running on the Web Receiver.

The sender framework uses an asynchronous callback design to inform the sender
app of events and to transition between various states of the Cast app life
cycle.

## App flow

The following steps describe the typical high-level execution flow for a sender
iOS app:

- The Cast framework starts [`GCKDiscoveryManager`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_discovery_manager.md.txt) based on the properties provided in [`GCKCastOptions`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_cast_options.md.txt) to begin scanning for devices.
- When the user clicks on the Cast button, the framework presents the Cast dialog with the list of discovered Cast devices.
- When the user selects a Cast device, the framework attempts to launch the Web Receiver app on the Cast device.
- The framework invokes callbacks in the sender app to confirm that the Web Receiver app was launched.
- The framework creates a communication channel between the sender and Web Receiver apps.
- The framework uses the communication channel to load and control media playback on the Web Receiver.
- The framework synchronizes the media playback state between sender and Web Receiver: when the user makes sender UI actions, the framework passes those media control requests to the Web Receiver, and when the Web Receiver sends media status updates, the framework updates the state of the sender UI.
- When the user clicks on the Cast button to disconnect from the Cast device, the framework will disconnect the sender app from the Web Receiver.

To troubleshoot your sender, you need to enable [logging](https://developers.google.com/cast/docs/ios_sender/integrate.md.txt#logging).

For a comprehensive list of all classes, methods and events in the Google Cast
iOS framework, see the [Google Cast iOS API
Reference](https://developers.google.com/cast/docs/reference/ios). The following sections cover the steps
for integrating Cast into your iOS app.

## Call methods from main thread

> [!IMPORTANT]
> **Key Point:** All SDK methods must be called from the main thread.

## Initialize the Cast context

The Cast framework has a global singleton object, the
[`GCKCastContext`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_cast_context.md.txt), which
coordinates all of the framework's activities. This object must be initialized
early in the application's lifecycle, typically in the
`-[application:didFinishLaunchingWithOptions:]` method of the app delegate, so
that automatic session resumption on sender app restart can trigger properly.

A [`GCKCastOptions`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_cast_options.md.txt)
object must be supplied when initializing the `GCKCastContext`.
This class contains options that affect the behavior of the framework. The most
important of these is the Web Receiver application ID, which is used to filter
discovery results and to launch the Web Receiver app when a Cast session is
started.

The `-[application:didFinishLaunchingWithOptions:]` method is also a good place
to set up a logging delegate to receive the logging messages from the framework.
These can be useful for debugging and troubleshooting.
Swift

```swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, GCKLoggerDelegate {
  let kReceiverAppID = kGCKDefaultMediaReceiverApplicationID
  let kDebugLoggingEnabled = true

  var window: UIWindow?

  func applicationDidFinishLaunching(_ application: UIApplication) {
    let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
    let options = GCKCastOptions(discoveryCriteria: criteria)
    GCKCastContext.setSharedInstanceWith(options)

    // Enable logger.
    GCKLogger.sharedInstance().delegate = self

    ...
  }

  // MARK: - GCKLoggerDelegate

  func logMessage(_ message: String,
                  at level: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if (kDebugLoggingEnabled) {
      print(function + " - " + message)
    }
  }
}
```
Objective-C

AppDelegate.h

```objective-c
@interface AppDelegate () <GCKLoggerDelegate>
@end
```

AppDelegate.m

```objective-c
@implementation AppDelegate

static NSString *const kReceiverAppID = @"AABBCCDD";
static const BOOL kDebugLoggingEnabled = YES;

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc]
                                    initWithApplicationID:kReceiverAppID];
  GCKCastOptions *options = [[GCKCastOptions alloc] initWithDiscoveryCriteria:criteria];
  [GCKCastContext setSharedInstanceWithOptions:options];

  // Enable logger.
  [GCKLogger sharedInstance].delegate = self;

  ...

  return YES;
}

...

#pragma mark - GCKLoggerDelegate

- (void)logMessage:(NSString *)message
           atLevel:(GCKLoggerLevel)level
      fromFunction:(NSString *)function
          location:(NSString *)location {
  if (kDebugLoggingEnabled) {
    NSLog(@"%@ - %@, %@", function, message, location);
  }
}

@end
```

## The Cast UX widgets

The Cast iOS SDK provides these widgets that comply with the Cast Design
Checklist:

- [Introductory Overlay](https://developers.google.com/cast/docs/design_checklist/cast-button.md.txt#prompting):
  The `GCKCastContext` class has a method,
  [`presentCastInstructionsViewControllerOnceWithCastButton`](https://developers.google.com/cast/docs/reference/ios/category_g_c_k_cast_context_07_u_i_08.md.txt#a6bcc9fe29c1f131d2303a428f461e1ba),
  which can be used to spotlight the Cast button the first time a Web Receiver
  is available. The sender app can customize the text, position of the title
  text and the Dismiss button.

- [Cast Button](https://developers.google.com/cast/docs/design_checklist/cast-button.md.txt#sender-cast-icon-available):
  Starting with Cast iOS sender SDK 4.6.0, the cast button is always visible
  when the sender device is connected to Wi-Fi. The first time the user taps
  on the Cast button after initially starting the app, a permissions dialog
  appears so the user can grant the app local network access to devices on
  the network. Subsequently, when the user taps on the cast button, a cast
  dialog is displayed which lists the discovered devices. When the user taps
  on the cast button while the device is connected, it displays the current
  media metadata (such as title, name of the recording studio and a thumbnail
  image) or allows the user to disconnect from the cast device. When the user
  taps on the cast button while there are no devices available, a screen
  will be displayed giving the user information on why devices not be found
  and how to troubleshoot.

- [Mini Controller](https://developers.google.com/cast/docs/design_checklist/sender.md.txt#sender-mini-controller):
  When the user is casting content and has navigated away from the current
  content page or expanded controller to another screen in the sender app, the
  mini controller is displayed at the bottom of the screen to allow the user to
  see the currently casting media metadata and to control the playback.

- [Expanded Controller](https://developers.google.com/cast/docs/design_checklist/sender.md.txt#sender-control-elements):
  When the user is casting content, if they click on the media notification or
  mini controller, the expanded controller launches, which displays the
  currently playing media metadata and provides several buttons to control the
  media playback.

## Add a Cast button

The framework provides a Cast button component as a `UIButton` subclass. It can
be added to the app's title bar by wrapping it in a `UIBarButtonItem`. A typical
`UIViewController` subclass can install a Cast button as follows:
Swift

```swift
let castButton = GCKUICastButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
castButton.tintColor = UIColor.gray
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: castButton)
```
Objective-C

```objective-c
GCKUICastButton *castButton = [[GCKUICastButton alloc] initWithFrame:CGRectMake(0, 0, 24, 24)];
castButton.tintColor = [UIColor grayColor];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:castButton];
```

By default, tapping the button will open the Cast dialog that is provided by the
framework.

[`GCKUICastButton`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_u_i_cast_button.md.txt)
can also be added directly to the storyboard.

## Configure device discovery

In the framework, device discovery happens automatically. There is no need to
explicitly start or stop the discovery process unless you implement a custom UI.

Discovery in the framework is managed by the class
[`GCKDiscoveryManager`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_discovery_manager.md.txt),
which is a property of the
[`GCKCastContext`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_cast_context.md.txt). The
framework provides a default Cast dialog component for device selection and
control. The device list is ordered lexicographically by device friendly name.

## How session management works

The Cast SDK introduces the concept of a Cast session, the
establishment of which combines the steps of connecting to a device, launching (or joining) a Web
Receiver app, connecting to that app, and initializing a media control channel. See the Web Receiver
[Application life cycle guide](https://developers.google.com/cast/docs/web_receiver/basic.md.txt#application_life_cycle)
for more information about Cast sessions and the Web Receiver life cycle.

Sessions are managed by the class
[`GCKSessionManager`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_session_manager.md.txt),
which is a property of the
[`GCKCastContext`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_cast_context.md.txt).
Individual sessions are represented by subclasses of the class
[`GCKSession`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_session.md.txt): for example,
[`GCKCastSession`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_cast_session.md.txt)
represents sessions with Cast devices. You can access the currently active Cast
session (if any), as the `currentCastSession` property of `GCKSessionManager`.

The
[`GCKSessionManagerListener`](https://developers.google.com/cast/docs/reference/ios/protocol_g_c_k_session_manager_listener-p.md.txt)
interface can be used to monitor session events, such as session creation,
suspension, resumption, and termination. The framework automatically suspends
sessions when the sender app is going into the background and attempts to resume
them when the app returns to the foreground (or is relaunched after an
abnormal/abrupt app termination while a session was active).

If the Cast dialog is being used, then sessions are created and torn down
automatically in response to user gestures. Otherwise, the app can start and end
sessions explicitly via methods on
[`GCKSessionManager`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_session_manager.md.txt).

If the app needs to do special processing in response to session lifecycle
events, it can register one or more `GCKSessionManagerListener` instances with
the `GCKSessionManager`. `GCKSessionManagerListener` is a protocol which defines
callbacks for such events as session start, session end, and so on.

### Stream transfer

Preserving session state is the basis of stream transfer, where
users can move existing audio and video streams across devices using voice commands, Google Home
App, or smart displays. Media stops playing on one device (the source) and continues on another (the
destination). Any Cast device with the latest firmware can serve as sources or destinations in a
stream transfer.

To get the new destination device during the stream transfer, use the
[`GCKCastSession#device`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_cast_session.md.txt)
property during the
[`[sessionManager:didResumeCastSession:]`](https://developers.google.com/cast/docs/reference/ios/protocol_g_c_k_session_manager_listener-p.md.txt#af01ab3ea8905f4886fc76bc71f156adc)
callback.

See
[Stream transfer on Web Receiver](https://developers.google.com/cast/docs/caf_receiver/core_features#stream_transfer)
for more information.

## Automatic reconnection

The Cast framework adds reconnection logic to automatically handle reconnection
in many subtle corner cases, such as:

- Recover from a temporary loss of WiFi
- Recover from device sleep
- Recover from backgrounding the app
- Recover if the app crashed

## How media control works

If a Cast session is established with a Web Receiver app that supports the media
namespace, an instance of
[`GCKRemoteMediaClient`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_remote_media_client.md.txt)
will be created automatically by the framework; it can be accessed as the
`remoteMediaClient` property of the
[`GCKCastSession`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_cast_session.md.txt)
instance.

All methods on `GCKRemoteMediaClient` which issue requests to the Web Receiver
will return a
[`GCKRequest`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_request.md.txt) object which
can be used to track that request. A
[`GCKRequestDelegate`](https://developers.google.com/cast/docs/reference/ios/protocol_g_c_k_request_delegate-p.md.txt)
can be assigned to this object to receive notifications about the eventual
result of the operation.

It is expected that the instance of `GCKRemoteMediaClient`
may be shared by multiple parts of the app, and indeed some internal components
of the framework such as the Cast dialog and mini media controls do share the
instance. To that end, `GCKRemoteMediaClient`
supports the registration of multiple
[`GCKRemoteMediaClientListener`](https://developers.google.com/cast/docs/reference/ios/protocol_g_c_k_remote_media_client_listener-p.md.txt)s.

## Set media metadata

The
[`GCKMediaMetadata`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_media_metadata.md.txt)
class represents information about a media item you want to cast. The following
example creates a new `GCKMediaMetadata` instance of a movie and sets the title,
subtitle, recording studio's name, and two images.
Swift

```swift
let metadata = GCKMediaMetadata()
metadata.setString("Big Buck Bunny (2008)", forKey: kGCKMetadataKeyTitle)
metadata.setString("Big Buck Bunny tells the story of a giant rabbit with a heart bigger than " +
  "himself. When one sunny day three rodents rudely harass him, something " +
  "snaps... and the rabbit ain't no bunny anymore! In the typical cartoon " +
  "tradition he prepares the nasty rodents a comical revenge.",
                   forKey: kGCKMetadataKeySubtitle)
metadata.addImage(GCKImage(url: URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg")!,
                           width: 480,
                           height: 360))
```
Objective-C

```objective-c
GCKMediaMetadata *metadata = [[GCKMediaMetadata alloc]
                                initWithMetadataType:GCKMediaMetadataTypeMovie];
[metadata setString:@"Big Buck Bunny (2008)" forKey:kGCKMetadataKeyTitle];
[metadata setString:@"Big Buck Bunny tells the story of a giant rabbit with a heart bigger than "
 "himself. When one sunny day three rodents rudely harass him, something "
 "snaps... and the rabbit ain't no bunny anymore! In the typical cartoon "
 "tradition he prepares the nasty rodents a comical revenge."
             forKey:kGCKMetadataKeySubtitle];
[metadata addImage:[[GCKImage alloc]
                    initWithURL:[[NSURL alloc] initWithString:@"https://commondatastorage.googleapis.com/"
                                 "gtv-videos-bucket/sample/images/BigBuckBunny.jpg"]
                    width:480
                    height:360]];
```

See the [Image Selection and
Caching](https://developers.google.com/cast/docs/ios_sender/advanced#override_image_selection_and_caching)
section on the use of images with media metadata.

## Load media

To load a media item, create a
[`GCKMediaInformation`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_media_information.md.txt)
instance using the media's metadata. Then get the current
[`GCKCastSession`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_cast_session.md.txt) and
use its
[`GCKRemoteMediaClient`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_remote_media_client.md.txt)
to load the media on the receiver app. You can then use `GCKRemoteMediaClient`
for controlling a media player app running on the receiver, such as for play,
pause, and stop.
Swift

```swift
let url = URL.init(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")
guard let mediaURL = url else {
  print("invalid mediaURL")
  return
}

let mediaInfoBuilder = GCKMediaInformationBuilder.init(contentURL: mediaURL)
mediaInfoBuilder.streamType = GCKMediaStreamType.none;
mediaInfoBuilder.contentType = "video/mp4"
mediaInfoBuilder.metadata = metadata;
mediaInformation = mediaInfoBuilder.build()

guard let mediaInfo = mediaInformation else {
  print("invalid mediaInformation")
  return
}

if let request = sessionManager.currentSession?.remoteMediaClient?.loadMedia(mediaInfo) {
  request.delegate = self
}
```
Objective-C

```objective-c
GCKMediaInformationBuilder *mediaInfoBuilder =
  [[GCKMediaInformationBuilder alloc] initWithContentURL:
   [NSURL URLWithString:@"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"]];
mediaInfoBuilder.streamType = GCKMediaStreamTypeNone;
mediaInfoBuilder.contentType = @"video/mp4";
mediaInfoBuilder.metadata = metadata;
self.mediaInformation = [mediaInfoBuilder build];

GCKRequest *request = [self.sessionManager.currentSession.remoteMediaClient loadMedia:self.mediaInformation];
if (request != nil) {
  request.delegate = self;
}
```

Also see the section on
[using media tracks](https://developers.google.com/cast/docs/ios_sender/media_tracks.md.txt#use_media_tracks).

### 4K video format

To determine what video format your media is, use the `videoInfo` property of
[`GCKMediaStatus`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_media_status.md.txt)
to get the current instance of
[`GCKVideoInfo`](https://developers.google.com/cast/reference/ios/interface_g_c_k_video_info).
This instance contains the type of HDR TV format and the height and width in
pixels. Variants of 4K format are indicated in the `hdrType` property by enum
values `GCKVideoInfoHDRType`.

## Add mini controllers

According to the [Cast Design
Checklist](https://developers.google.com/cast/docs/design_checklist/sender#sender-mini-controller),
a sender app should provide a persistent control known as the [mini
controller](https://developers.google.com/cast/docs/design_checklist/sender#mini-controller)
that should appear when the user navigates away from the current content page.
The mini controller provides instant access and a visible reminder for the
current Cast session.

The Cast framework provides a control bar,
[`GCKUIMiniMediaControlsViewController`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_u_i_mini_media_controls_view_controller.md.txt),
which can be added to the scenes in which you want to show the mini controller.

When your sender app is playing a video or audio live stream, the SDK
automatically displays a play/stop button in place of the play/pause button
in the mini controller.

See [Customize iOS Sender UI](https://developers.google.com/cast/docs/ios_sender/customize_ui.md.txt) for how your
sender app can configure the appearance of the Cast widgets.

There are two ways to add the mini controller to a sender app:

- Let the Cast framework manage the layout of the mini controller by wrapping your existing view controller with its own view controller.
- Manage the layout of the mini controller widget yourself by adding it to your existing view controller by providing a subview in the storyboard.

### Wrap using the GCKUICastContainerViewController

The first way is to use the
[`GCKUICastContainerViewController`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_u_i_cast_container_view_controller.md.txt)
which wraps another view controller and adds a
[`GCKUIMiniMediaControlsViewController`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_u_i_mini_media_controls_view_controller.md.txt)
at the bottom. This approach is limited in that you cannot customize the
animation and cannot configure the behavior of the container view controller.

This first way is typically done in the
`-[application:didFinishLaunchingWithOptions:]` method of the app delegate:
Swift

```swift
func applicationDidFinishLaunching(_ application: UIApplication) {
  ...

  // Wrap main view in the GCKUICastContainerViewController and display the mini controller.
  let appStoryboard = UIStoryboard(name: "Main", bundle: nil)
  let navigationController = appStoryboard.instantiateViewController(withIdentifier: "MainNavigation")
  let castContainerVC =
          GCKCastContext.sharedInstance().createCastContainerController(for: navigationController)
  castContainerVC.miniMediaControlsItemEnabled = true
  window = UIWindow(frame: UIScreen.main.bounds)
  window!.rootViewController = castContainerVC
  window!.makeKeyAndVisible()

  ...
}
```
Objective-C

```objective-c
- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  // Wrap main view in the GCKUICastContainerViewController and display the mini controller.
  UIStoryboard *appStoryboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
  UINavigationController *navigationController =
          [appStoryboard instantiateViewControllerWithIdentifier:@"MainNavigation"];
  GCKUICastContainerViewController *castContainerVC =
          [[GCKCastContext sharedInstance] createCastContainerControllerForViewController:navigationController];
  castContainerVC.miniMediaControlsItemEnabled = YES;
  self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
  self.window.rootViewController = castContainerVC;
  [self.window makeKeyAndVisible];
  ...

}
```

> [!NOTE]
> **Note:** Optional: The mini controller is automatically shown when content is playing and is hidden when the expanded controller is displayed. If you wish to manually control the visibility of the mini controller, add the following variable and setter/getter to the app delegate:

Swift

```swift
var castControlBarsEnabled: Bool {
  set(enabled) {
    if let castContainerVC = self.window?.rootViewController as? GCKUICastContainerViewController {
      castContainerVC.miniMediaControlsItemEnabled = enabled
    } else {
      print("GCKUICastContainerViewController is not correctly configured")
    }
  }
  get {
    if let castContainerVC = self.window?.rootViewController as? GCKUICastContainerViewController {
      return castContainerVC.miniMediaControlsItemEnabled
    } else {
      print("GCKUICastContainerViewController is not correctly configured")
      return false
    }
  }
}
```
Objective-C

AppDelegate.h

```objective-c
@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (nonatomic, strong) UIWindow *window;
@property (nonatomic, assign) BOOL castControlBarsEnabled;

@end
```

AppDelegate.m

```objective-c
@implementation AppDelegate

...

- (void)setCastControlBarsEnabled:(BOOL)notificationsEnabled {
  GCKUICastContainerViewController *castContainerVC;
  castContainerVC =
      (GCKUICastContainerViewController *)self.window.rootViewController;
  castContainerVC.miniMediaControlsItemEnabled = notificationsEnabled;
}

- (BOOL)castControlBarsEnabled {
  GCKUICastContainerViewController *castContainerVC;
  castContainerVC =
      (GCKUICastContainerViewController *)self.window.rootViewController;
  return castContainerVC.miniMediaControlsItemEnabled;
}

...

@end
```

### Embed in existing view controller

The second way is to add the mini controller directly to your existing view
controller by using
[`createMiniMediaControlsViewController`](https://developers.google.com/cast/docs/reference/ios/category_g_c_k_cast_context_07_u_i_08.md.txt#ab26e1d13aa2a56f2fc158be68982c37e)
to create a
[`GCKUIMiniMediaControlsViewController`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_u_i_mini_media_controls_view_controller.md.txt)
instance and then adding it to the container view controller as a subview.

Set up your view controller in the app delegate:
Swift

```swift
func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  ...

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true
  window?.clipsToBounds = true

  let rootContainerVC = (window?.rootViewController as? RootContainerViewController)
  rootContainerVC?.miniMediaControlsViewEnabled = true

  ...

  return true
}
```
Objective-C

```objective-c
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  self.window.clipsToBounds = YES;

  RootContainerViewController *rootContainerVC;
  rootContainerVC =
      (RootContainerViewController *)self.window.rootViewController;
  rootContainerVC.miniMediaControlsViewEnabled = YES;

  ...

  return YES;
}
```

In your root view controller, create a `GCKUIMiniMediaControlsViewController`
instance and add it to the container view controller as a subview:
Swift

```swift
let kCastControlBarsAnimationDuration: TimeInterval = 0.20

@objc(RootContainerViewController)
class RootContainerViewController: UIViewController, GCKUIMiniMediaControlsViewControllerDelegate {
  @IBOutlet weak private var _miniMediaControlsContainerView: UIView!
  @IBOutlet weak private var _miniMediaControlsHeightConstraint: NSLayoutConstraint!
  private var miniMediaControlsViewController: GCKUIMiniMediaControlsViewController!
  var miniMediaControlsViewEnabled = false {
    didSet {
      if self.isViewLoaded {
        self.updateControlBarsVisibility()
      }
    }
  }

  var overriddenNavigationController: UINavigationController?

  override var navigationController: UINavigationController? {

    get {
      return overriddenNavigationController
    }

    set {
      overriddenNavigationController = newValue
    }
  }
  var miniMediaControlsItemEnabled = false

  override func viewDidLoad() {
    super.viewDidLoad()
    let castContext = GCKCastContext.sharedInstance()
    self.miniMediaControlsViewController = castContext.createMiniMediaControlsViewController()
    self.miniMediaControlsViewController.delegate = self
    self.updateControlBarsVisibility()
    self.installViewController(self.miniMediaControlsViewController,
                               inContainerView: self._miniMediaControlsContainerView)
  }

  func updateControlBarsVisibility() {
    if self.miniMediaControlsViewEnabled && self.miniMediaControlsViewController.active {
      self._miniMediaControlsHeightConstraint.constant = self.miniMediaControlsViewController.minHeight
      self.view.bringSubview(toFront: self._miniMediaControlsContainerView)
    } else {
      self._miniMediaControlsHeightConstraint.constant = 0
    }
    UIView.animate(withDuration: kCastControlBarsAnimationDuration, animations: {() -> Void in
      self.view.layoutIfNeeded()
    })
    self.view.setNeedsLayout()
  }

  func installViewController(_ viewController: UIViewController?, inContainerView containerView: UIView) {
    if let viewController = viewController {
      self.addChildViewController(viewController)
      viewController.view.frame = containerView.bounds
      containerView.addSubview(viewController.view)
      viewController.didMove(toParentViewController: self)
    }
  }

  func uninstallViewController(_ viewController: UIViewController) {
    viewController.willMove(toParentViewController: nil)
    viewController.view.removeFromSuperview()
    viewController.removeFromParentViewController()
  }

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "NavigationVCEmbedSegue" {
      self.navigationController = (segue.destination as? UINavigationController)
    }
  }

...
```
Objective-C

RootContainerViewController.h

```objective-c
static const NSTimeInterval kCastControlBarsAnimationDuration = 0.20;

@interface RootContainerViewController () <GCKUIMiniMediaControlsViewControllerDelegate> {
  __weak IBOutlet UIView *_miniMediaControlsContainerView;
  __weak IBOutlet NSLayoutConstraint *_miniMediaControlsHeightConstraint;
  GCKUIMiniMediaControlsViewController *_miniMediaControlsViewController;
}

@property(nonatomic, weak, readwrite) UINavigationController *navigationController;

@property(nonatomic, assign, readwrite) BOOL miniMediaControlsViewEnabled;
@property(nonatomic, assign, readwrite) BOOL miniMediaControlsItemEnabled;

@end
```

RootContainerViewController.m

```objective-c
@implementation RootContainerViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  GCKCastContext *castContext = [GCKCastContext sharedInstance];
  _miniMediaControlsViewController =
      [castContext createMiniMediaControlsViewController];
  _miniMediaControlsViewController.delegate = self;

  [self updateControlBarsVisibility];
  [self installViewController:_miniMediaControlsViewController
              inContainerView:_miniMediaControlsContainerView];
}

- (void)setMiniMediaControlsViewEnabled:(BOOL)miniMediaControlsViewEnabled {
  _miniMediaControlsViewEnabled = miniMediaControlsViewEnabled;
  if (self.isViewLoaded) {
    [self updateControlBarsVisibility];
  }
}

- (void)updateControlBarsVisibility {
  if (self.miniMediaControlsViewEnabled &&
      _miniMediaControlsViewController.active) {
    _miniMediaControlsHeightConstraint.constant =
        _miniMediaControlsViewController.minHeight;
    [self.view bringSubviewToFront:_miniMediaControlsContainerView];
  } else {
    _miniMediaControlsHeightConstraint.constant = 0;
  }
  [UIView animateWithDuration:kCastControlBarsAnimationDuration
                   animations:^{
                     [self.view layoutIfNeeded];
                   }];
  [self.view setNeedsLayout];
}

- (void)installViewController:(UIViewController *)viewController
              inContainerView:(UIView *)containerView {
  if (viewController) {
    [self addChildViewController:viewController];
    viewController.view.frame = containerView.bounds;
    [containerView addSubview:viewController.view];
    [viewController didMoveToParentViewController:self];
  }
}

- (void)uninstallViewController:(UIViewController *)viewController {
  [viewController willMoveToParentViewController:nil];
  [viewController.view removeFromSuperview];
  [viewController removeFromParentViewController];
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
  if ([segue.identifier isEqualToString:@"NavigationVCEmbedSegue"]) {
    self.navigationController =
        (UINavigationController *)segue.destinationViewController;
  }
}

...

@end
```

The
[`GCKUIMiniMediaControlsViewControllerDelegate`](https://developers.google.com/cast/docs/reference/ios/protocol_g_c_k_u_i_mini_media_controls_view_controller_delegate-p.md.txt)
tells the host view controller when the mini controller should be visible:
Swift

```swift
  func miniMediaControlsViewController(_: GCKUIMiniMediaControlsViewController,
                                       shouldAppear _: Bool) {
    updateControlBarsVisibility()
  }
```
Objective-C

```objective-c
- (void)miniMediaControlsViewController:
            (GCKUIMiniMediaControlsViewController *)miniMediaControlsViewController
                           shouldAppear:(BOOL)shouldAppear {
  [self updateControlBarsVisibility];
}
```

## Add expanded controller

The Google Cast Design Checklist requires a sender app to provide an [expanded
controller](https://developers.google.com/cast/docs/design_checklist/sender#sender-expanded-controller)
for the media being cast. The expanded controller is a full screen version of
the mini controller.

The expanded controller is a full screen view which offers full control of the
remote media playback. This view should allow a casting app to manage every
manageable aspect of a cast session, with the exception of Web Receiver volume
control and session lifecycle (connect/stop casting). It also provides all the
status information about the media session (artwork, title, subtitle, and so
forth).

The functionality of this view is implemented by the
[`GCKUIExpandedMediaControlsViewController`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_u_i_expanded_media_controls_view_controller.md.txt)
class.

The first thing you have to do is enable the default expanded controller in the
cast context. Modify the app delegate to enable the default expanded controller:
Swift

```swift
func applicationDidFinishLaunching(_ application: UIApplication) {
  ..

  GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true

  ...
}
```
Objective-C

```objective-c
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  [GCKCastContext sharedInstance].useDefaultExpandedMediaControls = YES;

  ..
}
```

Add the following code to your view controller to load the expanded controller
when the user starts to cast a video:
Swift

```swift
func playSelectedItemRemotely() {
  GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()

  ...

  // Load your media
  sessionManager.currentSession?.remoteMediaClient?.loadMedia(mediaInformation)
}
```
Objective-C

```objective-c
- (void)playSelectedItemRemotely {
  [[GCKCastContext sharedInstance] presentDefaultExpandedMediaControls];

  ...

  // Load your media
  [self.sessionManager.currentSession.remoteMediaClient loadMedia:mediaInformation];
}
```

The expanded controller will also be launched automatically when the user
taps the mini controller.

When your sender app is playing a video or audio live stream, the SDK
automatically displays a play/stop button in place of the play/pause button
in the expanded controller.

See [Apply Custom Styles to Your iOS
App](https://developers.google.com/cast/docs/ios_sender/customize_ui#apply_custom_styles_to_your_ios_app)
for how your sender app can configure the appearance of the Cast widgets.

## Volume control

The Cast framework automatically manages the volume for the sender app. The
framework automatically synchronizes with the Web Receiver volume for the
supplied UI widgets. To sync a slider provided by the app, use
[`GCKUIDeviceVolumeController`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_u_i_device_volume_controller.md.txt).

### Physical button volume control

> [!WARNING]
> **Warning:** Due to changes in iOS, controlling the volume of a Cast session using the physical volume buttons is not supported. We are exploring alternatives to restore this functionality in a future release.

The physical volume buttons on the sender device can be used to change the
volume of the Cast session on the Web Receiver using the
`physicalVolumeButtonsWillControlDeviceVolume` flag on the
[`GCKCastOptions`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_cast_options.md.txt),
which is set on the
[`GCKCastContext`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_cast_context.md.txt).
Swift

```swift
let criteria = GCKDiscoveryCriteria(applicationID: kReceiverAppID)
let options = GCKCastOptions(discoveryCriteria: criteria)
options.physicalVolumeButtonsWillControlDeviceVolume = true
GCKCastContext.setSharedInstanceWith(options)
```
Objective-C

```objective-c
GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc]
                                          initWithApplicationID:kReceiverAppID];
GCKCastOptions *options = [[GCKCastOptions alloc]
                                          initWithDiscoveryCriteria :criteria];
options.physicalVolumeButtonsWillControlDeviceVolume = YES;
[GCKCastContext setSharedInstanceWithOptions:options];
```

## Handle errors

It is very important for sender apps to handle all error callbacks and decide
the best response for each stage of the Cast life cycle. The app can display
error dialogs to the user or it can decide to end the Cast session.

Note that some errors, including the
[`GCKErrorCode`](https://developers.google.com/cast/docs/reference/ios/g_c_k_error_8h.md.txt)
`GCKErrorCodeCancelled`, are intended behavior.

Don't attempt to re-try a connection that failed with `GCKErrorCodeCancelled`.
Doing so may lead to unexpected behavior.

## Logging

[`GCKLogger`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_logger.md.txt)
is a singleton used for logging by the framework. Use the
[`GCKLoggerDelegate`](https://developers.google.com/cast/docs/reference/ios/protocol_g_c_k_logger_delegate-p.md.txt)
to customize how you handle log messages.

Using the `GCKLogger`, the SDK produces logging output in the form of debug
messages, errors, and warnings. These log messages aid debugging and are useful
for troubleshooting and identifying problems. By default, the log output is
suppressed, but by assigning a `GCKLoggerDelegate`, the sender app can receive
these messages from the SDK and log them to the system console.
Swift

```swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, GCKLoggerDelegate {
  let kReceiverAppID = kGCKDefaultMediaReceiverApplicationID
  let kDebugLoggingEnabled = true

  var window: UIWindow?

  func applicationDidFinishLaunching(_ application: UIApplication) {
    ...

    // Enable logger.
    GCKLogger.sharedInstance().delegate = self

    ...
  }

  // MARK: - GCKLoggerDelegate

  func logMessage(_ message: String,
                  at level: GCKLoggerLevel,
                  fromFunction function: String,
                  location: String) {
    if (kDebugLoggingEnabled) {
      print(function + " - " + message)
    }
  }
}
```
Objective-C

AppDelegate.h

```objective-c
@interface AppDelegate () <GCKLoggerDelegate>
@end
```

AppDelegate.m

```objective-c
@implementation AppDelegate

static NSString *const kReceiverAppID = @"AABBCCDD";
static const BOOL kDebugLoggingEnabled = YES;

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  ...

  // Enable logger.
  [GCKLogger sharedInstance].delegate = self;

  ...

  return YES;
}

...

#pragma mark - GCKLoggerDelegate

- (void)logMessage:(NSString *)message
           atLevel:(GCKLoggerLevel)level
      fromFunction:(NSString *)function
          location:(NSString *)location {
  if (kDebugLoggingEnabled) {
    NSLog(@"%@ - %@, %@", function, message, location);
  }
}

@end
```

To enable debug and verbose messages as well, add this line to the code after
setting the delegate (shown previously):
Swift

```swift
let filter = GCKLoggerFilter.init()
filter.minimumLevel = GCKLoggerLevel.verbose
GCKLogger.sharedInstance().filter = filter
```
Objective-C

```objective-c
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setMinimumLevel:GCKLoggerLevelVerbose];
[GCKLogger sharedInstance].filter = filter;
```

You can also filter the log messages produced by
[`GCKLogger`](https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_logger.md.txt).
Set the minimum logging level per class, for example:
Swift

```swift
let filter = GCKLoggerFilter.init()
filter.setLoggingLevel(GCKLoggerLevel.verbose, forClasses: ["GCKUICastButton",
                                                            "GCKUIImageCache",
                                                            "NSMutableDictionary"])
GCKLogger.sharedInstance().filter = filter
```
Objective-C

```objective-c
GCKLoggerFilter *filter = [[GCKLoggerFilter alloc] init];
[filter setLoggingLevel:GCKLoggerLevelVerbose
             forClasses:@[@"GCKUICastButton",
                          @"GCKUIImageCache",
                          @"NSMutableDictionary"
                          ]];
[GCKLogger sharedInstance].filter = filter;
```

The class names can be either literal names or glob patterns, for example,
`GCKUI\*` and `GCK\*Session`.