iOS 9 added some really cool multitasking functionalities to select devices, such as the latest iPads. One of these functionalities is PiP, or Picture in Picture. In this chapter, we’ll have a look at some of these exciting features.
I’ll break the process down into small and digestible steps:
AVPlayerLayer
. This layer will be used by a view controller to display the video.VPlayerItem
that represents the video.AVPlayer
.play()
function on the player to start normal playback.currentItem.status
property of your player and wait until the status becomes ReadyToPlay
, at which point you create an instance of the AVPictureInPictureController
class.pictureInPicturePossible
property of your controller. Once this value becomes true
, let the user know that she can now go into Picture in Picture mode.pictureInPicturePossible
from your controller for safety’s sake, and if it checks out, call the startPictureInPicture()
function on the controller to start the Picture in Picture eventually.Picture in Picture is finally here. Let’s get started. Armed with what you learned in the solution section of this recipe, let’s start defining our view. Create a view class and call it PipView
. Go into the PipView.swift file and start importing the right frameworks:
import
Foundation
import
UIKit
import
AVFoundation
Then define what a “pippable” item is. It is any type that has a PiP layer and a PiP player:
protocol
Pippable
{
var
pipLayer
:
AVPlayerLayer
{
get
}
var
pipLayerPlayer
:
AVPlayer
?
{
get
set
}
}
Extend UIView
to make it pippable:
extension
UIView
:
Pippable
{
var
pipLayer
:
AVPlayerLayer
{
get
{
return
layer
as
!
AVPlayerLayer
}
}
//shortcut into pipLayer.player
var
pipLayerPlayer
:
AVPlayer
?
{
get
{
return
pipLayer
.
player
}
set
{
pipLayer
.
player
=
newValue
}
}
override
public
func
awakeFromNib
()
{
super
.
awakeFromNib
()
backgroundColor
=
.
blackColor
()
}
}
Last but not least for this view, change the view’s layer class to AVPlayerLayer
:
class
PipView
:
UIView
{
override
class
func
layerClass
()
->
AnyClass
{
return
AVPlayerLayer
.
self
}
}
Go to your view controller’s storyboard and change the main view’s class to PipView
. Also embed your view controller in a navigation controller and put two bar button items on the nav bar, namely:
So you’ll end up with something like Figure 7-1.
Hook up the two buttons to your view controller’s code. The Play button will be hooked to a method called play()
and the PiP button to beginPip()
. Now let’s head to our view controller and import some frameworks we need:
import
UIKit
import
AVKit
import
AVFoundation
import
SharedCode
Define the KVO context for watching the properties of our player:
private
var
kvoContext
=
0
let
pipPossible
=
"pictureInPicturePossible"
let
currentItemStatus
=
"currentItem.status"
Then our view controller becomes pippable:
protocol
PippableViewController
{
var
pipView
:
PipView
{
get
}
}
extension
ViewController
:
PippableViewController
{
var
pipView
:
PipView
{
return
view
as
!
PipView
}
}
If you want to, you can define your view controller as conformant to AVPictureInPictureControllerDelegate
to get delegate messages from the PiP view controller.
I’ll also define a property for the PiP button on my view controller so that I can enable this button when PiP is available:
@
IBOutlet
var
beginPipBtn
:
UIBarButtonItem
!
We also need a player of type AVPlayer
. Don’t worry about its URL; we will set it later:
lazy
var
player
:
AVPlayer
=
{
let
p
=
AVPlayer
()
p
.
addObserver
(
self
,
forKeyPath
:
currentItemStatus
,
options
:
.
New
,
context
:
&
kvoContext
)
return
p
}()
Here we define the PiP controller and the video URL. As soon as the URL is set, we construct an asset to hold the URL, place it inside the player, and set the player on our view’s layer:
var
pipController
:
AVPictureInPictureController
?
var
videoUrl
:
NSURL
?
=
nil
{
didSet
{
if
let
u
=
videoUrl
{
let
asset
=
AVAsset
(
URL
:
u
)
let
item
=
AVPlayerItem
(
asset
:
asset
,
automaticallyLoadedAssetKeys
:
[
"playable"
])
player
.
replaceCurrentItemWithPlayerItem
(
item
)
pipView
.
pipLayerPlayer
=
player
}
}
}
I also need a method that returns the URL of the video I am going to play. I’ve embedded a public domain video to my app and it resides in my app bundle. Check out this book’s GitHub repo for sample code:
var
embeddedVideo
:
NSURL
?
{
return
NSBundle
.
mainBundle
().
URLForResource
(
"video"
,
withExtension
:
"mp4"
)
}
We need to find out whether PiP is supported by calling the isPictureInPictureSupported()
class method of the AVPictureInPictureController
class:
func
isPipSupported
()
->
Bool
{
guard
AVPictureInPictureController
.
isPictureInPictureSupported
()
else
{
//no pip
return
false
}
return
true
}
When we start our PiP controller, we also need to make sure that the audio plays well even though the player is detached from our app. For that, we have to set our app’s audio playback category:
func
setAudioCategory
()
->
Bool
{
//set the audio category
do
{
try
AVAudioSession
.
sharedInstance
().
setCategory
(
AVAudioSessionCategoryPlayback
)
return
true
}
catch
{
return
false
}
}
When PiP playback is available, we can finally construct our PiP controller with our player’s layer. Remember, if the layer is not ready yet to play PiP, constructing the PiP view controller will fail:
func
startPipController
(){
pipController
=
AVPictureInPictureController
(
playerLayer
:
pipView
.
pipLayer
)
guard
let
controller
=
pipController
else
{
return
}
controller
.
addObserver
(
self
,
forKeyPath
:
pipPossible
,
options
:
.
New
,
context
:
&
kvoContext
)
}
Write the code for play()
now. We don’t have to check for availability of PiP just because we want to play a video:
@
IBAction
func
play
()
{
guard
setAudioCategory
()
else
{
alert
(
"Could not set the audio category"
)
return
}
guard
let
u
=
embeddedVideo
else
{
alert
(
"Cannot find the embedded video"
)
return
}
videoUrl
=
u
player
.
play
()
}
As soon as the user presses the PiP button, we start PiP if the pictureInPicturePossible()
method of our PiP controller returns true
:
@
IBAction
func
beginPip
()
{
guard
isPipSupported
()
else
{
alert
(
"PiP is not supported on your machine"
)
return
}
guard
let
controller
=
pipController
else
{
alert
(
"Could not instantiate the pip controller"
)
return
}
controller
.
addObserver
(
self
,
forKeyPath
:
pipPossible
,
options
:
.
New
,
context
:
&
kvoContext
)
if
controller
.
pictureInPicturePossible
{
controller
.
startPictureInPicture
()
}
else
{
alert
(
"Pip is not possible"
)
}
}
Last but not least, we listen for KVO messages:
override
func
observeValueForKeyPath
(
keyPath
:
String
?
,
ofObject
object
:
AnyObject
?
,
change
:
[
String
:
AnyObject
]
?
,
context
:
UnsafeMutablePointer
<
Void
>
)
{
guard
context
==
&
kvoContext
else
{
return
}
if
keyPath
==
pipPossible
{
guard
let
possibleInt
=
change
?
[
NSKeyValueChangeNewKey
]
as
?
NSNumber
else
{
beginPipBtn
.
enabled
=
false
return
}
beginPipBtn
.
enabled
=
possibleInt
.
boolValue
}
else
if
keyPath
==
currentItemStatus
{
guard
let
statusInt
=
change
?
[
NSKeyValueChangeNewKey
]
as
?
NSNumber
,
let
status
=
AVPlayerItemStatus
(
rawValue
:
statusInt
.
integerValue
)
where
status
==
.
ReadyToPlay
else
{
return
}
startPipController
()
}
}
Give this a go in an iPad Air 2 or a similar device that has PiP support.
Low power mode is a feature that Apple has placed inside iOS so that users can preserve battery whenever they wish to. For instance, if you have 10% battery while some background apps are running, you can save power by:
And that’s what low power mode does. In Figure 7-2, low power mode is disabled at the moment because there is a good amount of battery left on this device. Should the battery reach about 10%, the user will automatically be asked to enable low power mode.
Let’s create an app that wants to download a URL but won’t do so when low power mode is enabled. Instead, the app will defer the download until this mode is disabled. So let’s start by listening to NSProcessInfoPowerStateDidChangeNotification
notifications:
override
func
viewDidLoad
()
{
super
.
viewDidLoad
()
NSNotificationCenter
.
defaultCenter
().
addObserver
(
self
,
selector
:
"powerModeChanged:"
,
name
:
NSProcessInfoPowerStateDidChangeNotification
,
object
:
nil
)
downloadNow
()
}
Our custom downloadNow()
method has to avoid downloading the file if the device is in low power mode:
func
downloadNow
(){
guard
let
url
=
NSURL
(
string
:
"http://localhost:8888/video.mp4"
)
where
!
processInfo
.
lowPowerModeEnabled
else
{
return
}
//do the download here
(
url
)
mustDownloadVideo
=
false
}
Last but not least, write the powerModeChanged(_:)
method that we have hooked to our notification:
class
ViewController
:
UIViewController
{
var
mustDownloadVideo
=
true
let
processInfo
=
NSProcessInfo
.
processInfo
()
func
powerModeChanged
(
notif
:
NSNotification
){
guard
mustDownloadVideo
else
{
return
}
downloadNow
()
}
...