Chapter 7. Multitasking

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.

7.1 Adding Picture in Picture Playback Functionality

Problem

You want to let a user shrink a video to occupy a portion of the screen, so that she can view and interact with other content in other apps.

Solution

I’ll break the process down into small and digestible steps:

  1. You need a view that has a layer of type AVPlayerLayer. This layer will be used by a view controller to display the video.
  2. Instantiate an item of type VPlayerItem that represents the video.
  3. Take the player item and place it inside an instance of AVPlayer.
  4. Assign this player to your view’s layer player object. (Don’t worry if this sounds confusing. I’ll explain it soon.)
  5. Assign this view to your view controller’s main view and issue the play() function on the player to start normal playback.
  6. Using KVO, listen to changes to the currentItem.status property of your player and wait until the status becomes ReadyToPlay, at which point you create an instance of the AVPictureInPictureController class.
  7. Start a KVO listener on the pictureInPicturePossible property of your controller. Once this value becomes true, let the user know that she can now go into Picture in Picture mode.
  8. Now when the user presses a button to start Picture in Picture, read the value of 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.

Discussion

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:

  • Play (give it a play button style)
  • PiP (by pressing this we enable PiP; disable this button by default and hook it to an outlet in your code.)

So you’ll end up with something like Figure 7-1.

Figure 7-1. Your view controller should look like this (should is too strong a word really!)

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
  }
}
Note

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()

      }

  }
Note

Give this a go in an iPad Air 2 or a similar device that has PiP support.

See Also

Recipe 3.11

7.2 Handling Low Power Mode and Providing Alternatives

Problem

You want to know whether the device is in low power mode and want to be updated on the status of this mode as the user changes it.

Solution

Read the value of the lowPowerModeEnabled property of your process (of type NSProcessInfo) to find out whether the device is in low power mode, and listen to NSProcessInfoPowerStateDidChangeNotification notifications to find out when this state changes.

Discussion

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:

  • Disabling background apps
  • Reducing network activity
  • Disabling automatic mail pulls
  • Disabling animated backgrounds
  • Disabling visual effects

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.

Figure 7-2. Low power mode in the Settings app

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
    print(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()

  }

  ...
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset