Custom Controls not working in AVPlayerViewController in iOS 12

I recently stumbled across a bug in the AVPlayerViewController class. If you’re not familiar with this AVKit class, it’s a handy object used to play videos in iOS.

The bug happens when you use custom playback controls. Under some circumstances, your controls will stop responding. The bug is fixed in the upcoming iOS 13, but the app I was developing will be used in devices that currently have iOS 12 and can’t be upgraded. So finding a solution was our only option. It turned out that solution was easy to implement. Let me explain the details of the bug and how to fix it.

System-supplied and custom playback controls

By default, an AVPlayerViewController object shows system-supplied playback controls. You can combine or replace these controls with your own custom controls.

The showsPlaybackControls property of AVPlayerViewController objects controls whether you want the system-supplied playback controls.

To provide your custom controls, access the contentOverlayView property and add your views using addSubview

Custom controls not responding

For my case, I needed the following:

1-Start playback with custom playback controls.

2-At some point, hide the custom controls and show the system-supplied playback controls.

3-After some time, go back to the custom playback controls.

The problem was that, after hiding the system-supplied playback controls (point 3, above), the custom controls stopped working. See the video below for more details.

The view hierarchy explains it all

If you examine the view hierarchy you will find that the system-supplied playback controls are contained in a view of type AVPlaybackControlsView (this class is private and was introduced in iOS 11).

In iOS 12, starting playback with showsPlaybackControls = false will result in an object with no AVPlaybackControlsView view, as shown in Image 1 below.

Image 1 – Starting playback with showsPlaybackControls = false will result in an object with no AVPlaybackControlsView view

If you later set showsPlaybackControls = true, a view of type AVPlaybackControlsView will be created. However, if later on you deactivate the system-supplied playback controls using showsPlaybackControls = false, its view will stay at the top of the hierarchy, thus preventing input from reaching your custom controls. See Image 2 below.

Image 2 – Even if you deactivate the system-supplied playback controls, its view (of type AVPlaybackControlsView) will stay at the top of the hierarchy, thus preventing input from reaching your custom controls.

The fix

Since an AVPlaybackControlsView view is blocking the input, we can either remove it or hide/disable it.

I have found that, if removed, it won’t be added again when setting showsPlaybackControls = true. So the best fix simply consists in hiding/disabling it. Of course, if later on you want to show the system controls again, you will need to unhide/enable it.

The video below shows the result after the above fix is applied.

A demo project

You can download a project which implements the fix.

 

Using DispatchSemaphore to make synchronous calls in Swift

Modern-day CPUs are capable of running multiple execution threads simultaneously. This capability is called multi-threading and allows executing multiple tasks at the same time.

In programming, this capability is closely related to what is called synchronous (sync) calls and asynchronous (async) calls. A synchronous call will block the execution of the thread until the call is completed.

An asynchronous call, on the other hand, will not block the current thread because it will be executed in a different thread.

Nowadays, many API calls are asynchronous. When the call completes, the result is passed to the calling thread. That’s the current state of things, and we’re happy and grateful for that.

Nevertheless there are some cases where you might want to wait, that is, block execution, until a number of async calls complete. To accomplish this, you can use a semaphore. The following playground code shows how:

import UIKit

func synchronousTask(taskID : Int) {
    let semaphore = DispatchSemaphore(value: 0) // see note below
    print("Waiting for task \(taskID) to complete")

    // dispatch work to a different thread
    DispatchQueue.global(qos: .background).async {
        print("Starting task \(taskID)")
        sleep(3)    // simulate the duration of the task
        print("Ending task \(taskID)")
        semaphore.signal()
    }
    semaphore.wait()        // block until the task completes
    print("Task \(taskID) is completed")
}

// semaphores shouldn't be used on the main thread
DispatchQueue.global(qos: .background).async {
    synchronousTask(taskID: 1)
}

Value used for semaphore initialization.

The value used in the DispatchSemaphore initialization is related to its purpose. According to Apple: “Passing zero for the value is useful for when two threads need to reconcile the completion of a particular event.”

Since this is what we want, we initialize the semaphore with a value of zero.

The above code produces the following output:

Waiting for more than one task to complete.

If you need to wait for the completion of more than 1 task, simply write a wrapper function to call the synchronous code, as shown in the following code:

import UIKit

func synchronousTask(taskID : Int) {
    let semaphore = DispatchSemaphore(value: 0)
    print("Waiting for task \(taskID) to complete")

    // dispatch work to a different thread
    DispatchQueue.global(qos: .background).async {
        print("Starting task \(taskID)")
        sleep(1)    // simulate the duration of the task
        print("Ending task \(taskID)")
        semaphore.signal()
    }

    semaphore.wait()        // block until the task completes
    print("Task \(taskID) is completed")
}

func synchronousCall() {
    print("Waiting for tasks to be executed")
    for i in 1...3 {
        synchronousTask(taskID: i)
    }
    print("All tasks were executed")
}

// semaphores shouldn't be used on the main thread
DispatchQueue.global(qos: .background).async {
    synchronousCall()
}

The above code produces the following output: