Chapter 16. Music Library

An iOS device can be used for the same purpose as the original iPod — to hold and play music, podcasts, and audiobooks. These items constitute the device’s music library. (The relevant guide in Apple’s documentation, iPod Library Access Programming Guide, preserves a more archaic name.) iOS provides the programmer with various forms of access to the device’s music library; you can:

  • Explore the music library.

  • Play an item from the music library.

  • Learn and control what the Music app’s music player is doing.

  • Present a standard interface for allowing the user to select a music library item.

These abilities are provided by the Media Player framework; you’ll need to import MediaPlayer.

Music Library Authorization

New in iOS 10, access to the music library requires authorization from the user. You’ll need to include in your Info.Plist an entry under the “Privacy — Media Library Usage Description” key (NSAppleMusicUsageDescription) justifying to the user your desire for access. This will be used in the alert that will be presented to the user on your behalf by the system (Figure 16-1).

pios 2901
Figure 16-1. The system prompts for music library access

The system will present the authorization alert once, automatically, the first time your app attempts to access the music library. Instead of letting that happen, you will probably want to take control by checking for authorization yourself and requesting it if necessary. To learn whether you already have authorization, call the MPMediaLibrary class method authorizationStatus. The result is an MPMediaLibraryAuthorizationStatus. Here are the status cases and what they mean for how you might proceed:

.notDetermined

Authorization has never been requested. In that case, you’ll want to request authorization, causing the system’s authorization alert to appear.

.authorized

There is nothing to do; we’re already authorized. Go ahead and access the music library.

.denied

This means that we have been denied authorization. It is possible to do nothing, but if your app depends upon music library access, it is also reasonable to put up an alert begging for authorization. Your alert can even take the user directly to the spot in the Settings app where the user can provide authorization.

.restricted

This means that we have been denied authorization and that the user may not have the power to authorize us. There’s no point harassing the user about this, so it is best to do nothing.

If the authorization status is .notDetermined, as I just mentioned, you’ll want to request authorization. To do so, call the MPMediaLibrary class method requestAuthorization. This method executes asynchronously — that is, your code just continues on its merry way after the alert appears (see Appendix C). To hear about the user’s response to the alert, you pass a completion function as parameter; it will be called, possibly on a background thread, when the user dismisses the alert, with an MPMediaLibraryAuthorizationStatus parameter. If the status is now .authorized, you can proceed to access the music library.

All of that sounds delightfully simple and straightforward — but it isn’t. In fact, devising a robust implementation for checking and requesting authorization can be quite tricky. Here’s why. Keep in mind that the user, while running your app, can switch to the Settings app and change your authorization status at any time. Thus, there’s no point checking and requesting authorization just once, when the app launches. Rather, you will ideally want to perform this check-and-request procedure just before any action that requires authorization; if you have authorization, or can get it, you’ll proceed to take that authorized action.

So far, so good; but remember what I said a moment ago — the call to requestAuthorization returns asynchronously. But the call to authorizationStatus does not return asynchronously. Thus, if we want to cover all cases, we need to proceed to our actual authorized action via two different paths — either because we already have authorization, or because we requested authorization and received it. Here’s a sketch of the architecture we’re going to need:

let status = MPMediaLibrary.authorizationStatus()
switch status {
case .authorized:
    // do next action
case .notDetermined:
    MPMediaLibrary.requestAuthorization() { status in
        if status == .authorized {
            // get on main thread, do next action
        }
    }
case .restricted:
    // do nothing
case .denied:
    // do nothing, or beg the user to authorize us in Settings
}

Do you see where this is heading? Let’s say our app contains a dozen different actions that the user can somehow initiate, each of which can be performed only if we have authorization. Then we’re going to have to run that code in a dozen different places, with a different “next action” in every case! If we are not to end up with a dreadful quantity of repeated boilerplate, we’d better express this as a single function that can be called with any “next action.” And it’s going to be important to do this, not only in the case of accessing the music library, but also for several other kinds of access that require user authorization along just the same lines. We already encountered an example in Chapter 14, and there will be several more in succeeding chapters. Thus, it will be greatly to our benefit to solve this problem once and for all.

Since the “next action” can presumably be expressed as a function call, it makes sense to solve the problem by writing a general authorization function that takes the “next action” as a function parameter, like this:

func checkForMusicLibraryAccess(andThen f:(()->())? = nil) {
    let status = MPMediaLibrary.authorizationStatus()
    switch status {
    case .authorized:
        f?()
    case .notDetermined:
        MPMediaLibrary.requestAuthorization() { status in
            if status == .authorized {
                DispatchQueue.main.async {
                    f?()
                }
            }
        }
    case .restricted:
        // do nothing
        break
    case .denied:
        // do nothing, or beg the user to authorize us in Settings
        break
    }
}

If you want the .denied case to put up an alert offering to take the user to the Settings app, the appropriate code might look something like this:

case .denied:
    let alert = UIAlertController(title: "Need Authorization",
    message: "Wouldn't you like to authorize this app" +
    " to use your music library?", preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "No", style: .cancel))
    alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
        let url = URL(string:UIApplicationOpenSettingsURLString)!
        UIApplication.shared.open(url)
    })
    self.present(alert, animated:true)

We can call this function in any of three ways. We can call it with no parameter; this is useful in case there is no next action (for example, in application(_:didFinishLaunchingWithOptions:), or in a view controller’s viewDidLoad):

checkForMusicLibraryAccess()

We can call it as a trampoline to another function:

@IBAction func doGetMusicAlbums (_ sender: Any!) {
    checkForMusicLibraryAccess(andThen:self.reallyGetMusicAlbums)
}

And we can call it with an anonymous function defined inline:

@IBAction func doGetMusicAlbums (_ sender: Any!) {
    checkForMusicLibraryAccess {
        // we're authorized! get music albums here
    }
}
Tip

To retest the system authorization request alert and other access-related behaviors, go to the Settings app and choose General → Reset → Reset Location & Privacy.

Exploring the Music Library

Everything in the music library, as seen by your code, is an MPMediaEntity. This is an abstract class. It has two concrete subclasses:

MPMediaItem

An MPMediaItem is a single item (a “song”).

MPMediaCollection

An MPMediaCollection is an ordered list of MPMediaItems, rather like an array; it has a count, and its items property is an array.

MPMediaEntity endows its subclasses with the ability to describe themselves through key–value pairs called properties. The property keys have names like MPMediaItemPropertyTitle. To fetch a property’s value, call value(forProperty:) with its key. You can fetch multiple properties with enumerateValues(forProperties:using:). Thus, the use of the word “properties” here has nothing to do with object properties; these properties are more like entries in a dictionary. As a convenience, MPMediaEntity and its subclasses do also have instance properties whose names correspond to the property names. Thus, for example, with an MPMediaItem you can say either myItem.value(forProperty:MPMediaItemPropertyTitle) or myItem.title, and in most cases you will surely prefer the latter. (You will, however, still need the full property key name if you’re going to form an MPMediaPropertyPredicate, as I’ll demonstrate later.)

An MPMediaItem has a type (mediaType, or MPMediaItemPropertyMediaType); it might, for example, be music, a podcast, or an audiobook. Different types of item have slightly different properties; but these will be intuitively familiar from your use of iTunes. For example, a song (music) has a title, an album title, a track number, an artist, a composer, and so on; a podcast, in addition to its normal title, has a podcast title.

A playlist is an MPMediaPlaylist, a subclass of MPMediaCollection. Its properties include a title, a flag indicating whether it is a “smart” playlist, and so on.

An item’s artwork image is available through an instance of the MPMediaItemArtwork class, from which you are supposed to be able to get the image itself scaled to a specified size by calling image(at:). My experience, however, is that in reality you’ll receive an image of any old size the system cares to give you, so you may have to scale it further yourself. This, for example, is what my Albumen app does:

let art : MPMediaItemArtwork = // ...
let sz = CGSize(36,36) // or whatever the desired size is
guard let im = art.image(at: sz) else { return }  // no image
let rect = AVMakeRect(aspectRatio: im.size,
    insideRect: CGRect(origin:.zero, size:sz))
let r = UIGraphicsImageRenderer(size:sz)
let im2 = r.image { _ in
    im.draw(in:rect)
}

Querying the Music Library

Obtaining actual information from the music library requires a query, an MPMediaQuery. First, you form the query. There are three main ways to do this:

Without limits

Create a simple MPMediaQuery by calling init (that is, MPMediaQuery()). The result is an unlimited query; it asks for everything in the music library.

With a convenience constructor

MPMediaQuery provides several class methods that form a query ready to ask the music library for a limited subset of its contents — all of its songs, or all of its podcasts, and so on. Here’s the complete list:

  • songs

  • podcasts

  • audiobooks

  • playlists

  • albums

  • artists

  • composers

  • genres

  • compilations

With filter predicates

You can limit a query more precisely by attaching to the query one or more MPMediaPropertyPredicate instances. These predicates filter the music library according to criteria you specify; to be included in the result, a media item must successfully pass through all the filters (in other words, the predicates are combined using logical-and). A predicate is a simple comparison. It has three aspects:

A property

The key name of the property you want to compare against. Not every property can be used in a filter predicate; the documentation makes the distinction clear (and you can get additional help from an MPMediaEntity class method, canFilter(byProperty:)).

A value

The value that the property must have in order to pass through the filter.

A comparison type (optional)

An MPMediaPredicateComparison. In order to pass through the filter, a media item’s property value can either match the value you provide (.equalTo, the default) or contain the value you provide (.contains).

The two ways of forming a limited query are actually the same; a convenience constructor is just a quick way of obtaining a query already endowed with a filter predicate.

A query also groups its results, according to its groupingType (MPMediaGrouping). Your choices are:

  • .title

  • .album

  • .artist

  • .albumArtist

  • .composer

  • .genre

  • .playlist

  • .podcastTitle

The query convenience constructors all supply a groupingType in addition to a filter predicate. Indeed, the grouping is often the salient aspect of the query. For example, an albums query is in fact merely a songs query with the added feature that its results are grouped by album.

The groups resulting from a query are collections; that is, each is an MPMediaItemCollection. This class, you will recall, is an MPMediaEntity subclass, so a collection has properties. In addition, it has items and a count. It also has a representativeItem property, which gives you just one item from the collection. The reason you need this is that properties of a collection are often embodied in its items rather than in the collection itself. For example, an album has no title; rather, its items have album titles that are all the same. So to learn the title of an album, you ask for the album title of a representative item.

After you form the query, you perform the query. You do this simply by asking for the query’s results. You can ask either for its collections, if you care about the groups returned from the query, or for its items. Here, I’ll discover the titles of all the albums:

let query = MPMediaQuery.albums()
guard let result = query.collections else {return}
// prove we've performed the query, by logging the album titles
for album in result {
    print(album.representativeItem!.albumTitle!)
}
/*
Bach, CPE, Symphonies
Beethoven Canons
Beethoven Dances
Scarlatti Continuo
*/

Now let’s make our query more elaborate; we’ll get the titles of all the albums whose name contains “Beethoven.” We simply add a filter predicate to the previous query:

let query = MPMediaQuery.albums()
let hasBeethoven = MPMediaPropertyPredicate(value:"Beethoven",
    forProperty:MPMediaItemPropertyAlbumTitle,
    comparisonType:.contains)
query.addFilterPredicate(hasBeethoven)
guard let result = query.collections else {return}
for album in result {
    print(album.representativeItem!.albumTitle!)
}
/*
Beethoven Canons
Beethoven Dances
*/

Similarly, we can get the titles of all the albums containing any songs whose name contains “Sonata.” This is like the previous example, but here we are concerned with the song’s own title rather than its album title:

let query = MPMediaQuery.albums()
let hasSonata = MPMediaPropertyPredicate(value:"Sonata",
    forProperty:MPMediaItemPropertyTitle,
    comparisonType:.contains)
query.addFilterPredicate(hasSonata)
guard let result = query.collections else {return}
for album in result {
    print(album.representativeItem!.albumTitle!)
}
/*
Scarlatti Continuo
*/

The results of an albumsQuery are actually songs (MPMediaItems). That means we can immediately access any song in any of those albums. Let’s modify the output from our previous query to print the titles of all the matching songs in the first album returned, which happens to be the only album returned. We don’t have to change our query, so I’ll start at the point where we perform it; result is the array of collections returned from our query, so result[0] is an MPMediaItemCollection holding the filtered songs of one album:

// ... same as before ...
let album = result[0]
for song in album.items {
    print(song.title!)
}
/*
Sonata in E minor Kk 81 - I Grave
Sonata in E minor Kk 81 - II Allegro
Sonata in E minor Kk 81 - III Grave
Sonata in E minor Kk 81 - IV Allegro
Sonata in G minor Kk 88 - I Grave
... and so on ...
*/

Here are some more examples of query filter predicates that arise in my own apps. The user’s music library can include songs that are actually off in “the cloud.” For example, suppose the user subscribes to iTunes Match (now termed iCloud Music Library in the Music settings). Some songs may have been downloaded for offline listening; others may still be in the cloud. But they are all listed in the library. In my Albumen app, I don’t want to include cloud-based songs in my results:

let notCloud = MPMediaPropertyPredicate(
    value: false, forProperty: MPMediaItemPropertyIsCloudItem,
    comparisonType: .equalTo)
query.addFilterPredicate(notCloud)

Similarly, when gathering playlists, I don’t want to include smart playlists, genius playlists, or on-the-go playlists created on the user’s device. Here’s how I eliminate those:

let notSmart = MPMediaPropertyPredicate(value: 0,
    forProperty: MPMediaPlaylistPropertyPlaylistAttributes,
    comparisonType: .equalTo)
query.addFilterPredicate(notSmart)

Persistence and Change in the Music Library

One of the properties of an MPMediaEntity is its persistentID, which uniquely identifies it. All sorts of things have persistent IDs — entities in general, songs (media items), albums, artists, composers, and more. Two songs or two playlists can have the same title, but a persistent ID is unique — and persistent! Using the persistent ID, you can retrieve again at a later time the same song or playlist you retrieved earlier, even across launches of your app.

While you are maintaining the results of a search, the contents of the music library may themselves change. For example, the user might connect the device to a computer and add or delete music with iTunes; or the user might switch to Settings and turn on or off iTunes Match (iCloud Music Library). This can put your results out of date. For this reason, the library’s own modified date is available through the MPMediaLibrary class. Call the class method default to get the actual library instance; now you can ask it for its lastModifiedDate. You can also register to receive a notification, .MPMediaLibraryDidChange, when the music library is modified. This notification is not emitted unless you first send the library beginGeneratingLibraryChangeNotifications; you should eventually balance this with endGeneratingLibraryChangeNotifications.

Warning

The library’s notion of what constitutes a change can be somewhat surprising with regard to cloud-based items. For example, the user playing or downloading a cloud song can cause .MPMediaLibraryDidChange to be triggered numerous times in quick succession.

Music Player

The Media Player framework class for playing an MPMediaItem is MPMusicPlayerController. It comes in two flavors, depending on which class method you use to get an instance:

systemMusicPlayer

The global music player — the very same player used by the Music app. This might already be playing an item, or it might be paused with a current item, at any time while your app runs; you can learn or change what item this is. The global music player continues playing independently of the state of your app, and the user, by way of the Music app, can at any time alter what it is doing. (The name systemMusicPlayer supersedes the name iPodMusicPlayer from iOS 7 and before, which is deprecated.)

applicationMusicPlayer

What you do with this music player doesn’t affect, and is not affected by, what the Music app does; the song it is playing can be different from the Music app’s current song. Nevertheless, it is actually the global music player behaving differently; it isn’t really inside your app. It has its own audio session. You cannot play its audio when your app is in the background. You cannot make it the target of remote control events. If these limitations prove troublesome, use the systemMusicPlayer (or some other means of playing the song, as discussed later in this chapter).

A music player doesn’t merely play an item; it plays from a queue of items. This behavior is familiar from iTunes and the Music app. For example, in the Music app, when you tap the first song of a playlist to start playing it, when the end of that song is reached, we proceed by default to the next song in the playlist. That’s because tapping the first song of a playlist causes the queue to be the totality of songs in the playlist. The music player behaves the same way: when it reaches the end of a song, it proceeds to the next song in its queue.

Your methods for controlling playback also reflect this queue-based orientation. In addition to the expected play, pause, and stop commands, there’s a skipToNextItem and skipToPreviousItem command. Anyone who has ever used iTunes or the Music app (or, for that matter, an old-fashioned iPod) will have an intuitive grasp of this and everything else a music player does. You can even set a music player’s repeatMode and shuffleMode, just as in iTunes.

You provide a music player with its queue in one of two ways:

With a query

You hand the music player an MPMediaQuery. The query’s items are the items of the queue.

With a collection

You hand the music player an MPMediaItemCollection. This might be obtained from a query you performed, but you can also assemble your own collection of MPMediaItems in any way you like, putting them into an array and calling MPMediaItemCollection’s init(items:).

In this example, we collect all songs actually present in the library shorter than 30 seconds and set them playing in random order using the application music player. Observe that I explicitly stop the player before setting its queue; I have found that this is the most reliable approach:

let query = MPMediaQuery.songs()
let isPresent = MPMediaPropertyPredicate(value:false,
    forProperty:MPMediaItemPropertyIsCloudItem,
    comparisonType:.equalTo)
query.addFilterPredicate(isPresent)
guard let items = query.items else {return}
let shorties = items.filter {
    let dur = $0.playbackDuration
    return dur < 30
}
guard shorties.count > 0 else {
    print("no songs that short!")
    return
}
let queue = MPMediaItemCollection(items:shorties)
let player = MPMusicPlayerController.applicationMusicPlayer()
player.stop()
player.setQueue(with:queue)
player.shuffleMode = .songs
player.play()

You can ask a music player for its nowPlayingItem, and since this is an MPMediaItem, you can learn all about it through its properties. Unfortunately, you can’t query a music player as to its queue, but you can keep your own pointer to the MPMediaItemCollection constituting the queue when you hand it to the music player, and you can ask the music player which song within the queue is currently playing (indexOfNowPlayingItem). The user can completely change the queue of the systemMusicPlayer, however, so if control over the queue is important to you, use the applicationMusicPlayer.

A music player has a playbackState that you can query to learn what it’s doing (whether it is playing, paused, stopped, or seeking). It also emits notifications informing you of changes in its state:

  • .MPMusicPlayerControllerPlaybackStateDidChange

  • .MPMusicPlayerControllerNowPlayingItemDidChange

  • .MPMusicPlayerControllerVolumeDidChange

These notifications are not emitted until you tell the music player to beginGeneratingPlaybackNotifications. (You should eventually balance this call with endGeneratingPlaybackNotifications.) This is an instance method, so you can arrange to receive notifications from either of the two music players. If you are receiving notifications from both, you can distinguish them by examining the Notification’s object and comparing it to each player.

To illustrate, I’ll extend the previous example to set the text of a UILabel in our interface (self.label) every time a different song starts playing. After configuring queue and player, but before we start the player playing, we insert these lines to generate the notifications:

player.beginGeneratingPlaybackNotifications()
NotificationCenter.default.addObserver(self,
    selector: #selector(self.changed),
    name: .MPMusicPlayerControllerNowPlayingItemDidChange,
    object: player)
self.q = queue // retain a pointer to the queue

And here’s how we respond to those notifications:

func changed(_ n:Notification) {
    self.label.text = ""
    let player = MPMusicPlayerController.applicationMusicPlayer()
    guard let obj = n.object, obj as AnyObject === player else { return }
    guard let title = player.nowPlayingItem?.title else {return}
    let ix = player.indexOfNowPlayingItem
    guard ix != NSNotFound else {return}
    self.label.text = "(ix+1) of (self.q.count): (title)"
}

There’s no periodic notification as a song plays and the current playhead position advances. To get this information, you’ll have to resort to polling. This is not objectionable as long as your polling interval is reasonably sparse; your display may occasionally fall a little behind reality, but this won’t usually matter. To illustrate, let’s add to our existing example a UIProgressView (self.prog) showing the current percentage of the current song being played by the music player. I’ll use a Timer to poll the state of the player every second:

self.timer = Timer.scheduledTimer(timeInterval:1,
    target: self, selector: #selector(self.timerFired),
    userInfo: nil, repeats: true)
self.timer.tolerance = 0.1

When the timer fires, the progress view displays the state of the currently playing item:

func timerFired(_: Any) {
    let player = MPMusicPlayerController.applicationMusicPlayer()
    guard let item = player.nowPlayingItem,
        player.playbackState != .stopped else {
            self.prog.isHidden = true
            return
    }
    self.prog.isHidden = false
    let current = player.currentPlaybackTime
    let total = item.playbackDuration
    self.prog.progress = Float(current / total)
}

MPVolumeView

The Media Player framework offers a slider for letting the user set the system output volume, along with an AirPlay route button if appropriate; this is an MPVolumeView. An MPVolumeView works only on a device — not in the Simulator. It is customizable similarly to a UISlider (Chapter 12); you can set the images for the two halves of the track, the thumb, and even the AirPlay route button, for both the normal and the highlighted state (while the user is touching the thumb).

For further customization, you can subclass MPVolumeView and override volumeSliderRect(forBounds:). (An additional overridable method is documented, volumeThumbRect(forBounds:volumeSliderRect:value:), but in my testing it is never called; I regard this as a bug.)

You can register for notifications when a wireless route (Bluetooth or AirPlay) appears or disappears and when a wireless route becomes active or inactive:

  • .MPVolumeViewWirelessRoutesAvailableDidChange

  • .MPVolumeViewWirelessRouteActiveDidChange

Playing Songs with AV Foundation

MPMusicPlayerController is convenient and simple, but it’s also simpleminded. Its audio session isn’t your audio session; the music player doesn’t really belong to you. So what else can you use to play an MPMediaItem? The answer lies in the fact that an MPMediaItem has an assetURL property whose value is a URL. Now you have a reference to the music file on disk — and everything from Chapters 14 and 15 comes into play.

So, for example, having obtained an MPMediaItem’s asset URL, you could use that URL to initialize an AVAudioPlayer, an AVPlayer, or an AVAsset. Each of these ways of playing an MPMediaItem has its advantages. For example, an AVAudioPlayer is easy to use, and lets you loop a sound, poll the power value of its channels, and so forth. An AVAsset gives you the full power of the AV Foundation framework, letting you edit the sound, assemble multiple sounds, perform a fadeout effect, and even attach the sound to a video (and then play it with an AVPlayer). An AVPlayer can be assigned to an AVPlayerViewController, which gives you a built-in play/pause button and playhead slider. Another major advantage of an AVPlayerViewController is that it automatically manages the software remote control interface for you (unless you set its updatesNowPlayingInfoCenter property to false).

In this example, I’ll use an AVQueuePlayer (an AVPlayer subclass) to play a sequence of MPMediaItems, just as MPMusicPlayerController does. We might be tempted to treat the AVQueuePlayer as a playlist, handing it the entire array of songs to be played:

let arr = // array of MPMediaItem
let items = arr.map {
    let url = $0.assetURL!
    let asset = AVAsset(url:url)
    return AVPlayerItem(asset: asset)
}
self.qp = AVQueuePlayer(items:items)
self.qp.play()

That, however, is not what we’re supposed to do. Instead of adding a whole batch of AVPlayerItems to an AVQueuePlayer all at once, we should add just a few AVPlayerItems to start with, and then append each additional AVPlayerItem when an item finishes playing. So I’ll start out by adding just three AVPlayerItems, and use key–value observing to watch for changes in the AVQueuePlayer’s currentItem:

let arr = // array of MPMediaItem
self.items = arr.map {
    let url = $0.assetURL!
    let asset = AVAsset(url:url)
    return AVPlayerItem(asset: asset)
}
let seed = min(3,self.items.count)
self.qp = AVQueuePlayer(items:Array(self.items.prefix(upTo:seed)))
self.items = Array(self.items.suffix(from:seed))
// use .initial option so that we get an observation for the first item
self.qp.addObserver(self, forKeyPath:#keyPath(AVQueuePlayer.currentItem),
    options:.initial, context:nil)
self.qp.play()

In observeValue(forKeyPath:of:change:context:), we pull an AVPlayerItem off the front of our items array and add it to the end of the AVQueuePlayer’s queue. The AVQueuePlayer itself deletes an item from the start of its queue after playing it, so in this way the queue never exceeds three items in length:

guard keyPath == #keyPath(AVQueuePlayer.currentItem) else {return}
guard let item = self.qp.currentItem else {return}
guard self.items.count > 0 else {return}
let newItem = self.items.removeFirst()
self.qp.insert(newItem, after:nil) // means "at end"

As long as observeValue is notifying us each time a new song starts playing, let’s insert some code to update a label’s text with the title of each successive song. This will demonstrate how to extract metadata from an AVAsset by way of an AVMetadataItem; here, we fetch the AVMetadataCommonKeyTitle and get its value property:

var arr = item.asset.commonMetadata
arr = AVMetadataItem.metadataItems(from:arr,
    withKey:AVMetadataCommonKeyTitle,
    keySpace:AVMetadataKeySpaceCommon)
let met = arr[0]
let value = #keyPath(AVMetadataItem.value)
met.loadValuesAsynchronously(forKeys:[value]) {
    if met.statusOfValue(forKey:value, error: nil) == .loaded {
        guard let title = met.value as? String else {return}
        DispatchQueue.main.async {
            self.label.text = "(title)"
        }
    }
}

We can also update a progress view to reflect the current item’s current time and duration. Unlike MPMusicPlayerController, we don’t need to poll with a Timer; we can install a time observer on our AVQueuePlayer:

self.ob = self.qp.addPeriodicTimeObserver(
    forInterval: CMTime(seconds:0.5, preferredTimescale:600),
    queue: nil, using: self.timerFired)

To get our AVPlayerItems to load their duration property, we’ll need to go back and modify the code we used to initialize them:

let url = $0.assetURL!
let asset = AVAsset(url:url)
return AVPlayerItem(asset: asset,
    automaticallyLoadedAssetKeys: [#keyPath(AVAsset.duration)])

Our time observer now calls our timerFired method periodically, reporting the current time of the current player item; we obtain the current item’s duration and configure our progress view (self.prog):

func timerFired(time:CMTime) {
    if let item = self.qp.currentItem {
        let asset = item.asset
        let dur = #keyPath(AVAsset.duration)
        if asset.statusOfValue(forKey:dur, error: nil) == .loaded {
            let dur = asset.duration
            self.prog.setProgress(Float(time.seconds/dur.seconds),
                animated: false)
        }
    }
}

Media Picker

The media picker (MPMediaPickerController), supplied by the Media Player framework, is a view controller whose view is a self-contained navigation interface in which the user can select a media item from the music library, similar to the Music app. You are expected to treat the picker as a presented view controller.

As with any access to the music library, the media picker requires user authorization in iOS 10. If you present the media picker without authorization, there is no penalty, but nothing will appear to happen (and the picker will report that the user cancelled).

You can use the initializer, init(mediaTypes:), to limit the type of media items displayed. You can make a prompt appear at the top of the navigation bar (prompt). You can govern whether the user can choose multiple media items or just one, with the allowsPickingMultipleItems property. You can filter out items stored in the cloud by setting showsCloudItems to false.

Warning

Starting in iOS 9, the mediaTypes: values .podcast and .audioBook don’t work. I believe that this is because podcasts are considered to be the purview of the Podcasts app, and audiobooks are considered to be the purview of iBooks — not the Music app. You can see podcasts and audiobooks as MPMediaEntity objects in the user’s music library, but not by way of an MPMediaPickerController.

While the view is showing, you learn what the user is doing through two delegate methods (MPMediaPickerControllerDelegate); the presented view controller is not automatically dismissed, so it is up to you dismiss it in these delegate methods:

  • mediaPicker(_:didPickMediaItems:)

  • mediaPickerDidCancel(_:)

The behavior of the delegate methods depends on the value of the controller’s allowsPickingMultipleItems:

The controller’s allowsPickingMultipleItems is false (the default)

There’s a Cancel button. When the user taps a media item, your mediaPicker(_:didPickMediaItems:) is called, handing you an MPMediaItemCollection consisting of that item; you are likely to dismiss the presented view controller at this point. When the user taps Cancel, your mediaPickerDidCancel(_:) is called.

The controller’s allowsPickingMultipleItems is true

There’s a Done button. Every time the user taps a media item, it is checked to indicate that it has been selected. When the user taps Done, your mediaPicker(_:didPickMediaItems:) is called, handing you an MPMediaItemCollection consisting of all items the user tapped — unless the user tapped no items, in which case your mediaPickerDidCancel(_:) is called.

In this example, we put up the media picker; we then play the user’s chosen media item(s) with the application music player. The example works equally well whether allowsPickingMultipleItems is true or false:

func presentPicker (_ sender: Any) {
    checkForMusicLibraryAccess {
        let picker = MPMediaPickerController(mediaTypes:.music)
        picker.delegate = self
        self.present(picker, animated: true)
    }
}
func mediaPicker(_ mediaPicker: MPMediaPickerController,
    didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
        let player = MPMusicPlayerController.applicationMusicPlayer()
        player.setQueue(with:mediaItemCollection)
        player.play()
        self.dismiss(animated:true)
}
func mediaPickerDidCancel(_ mediaPicker: MPMediaPickerController) {
    self.dismiss(animated:true)
}

On the iPad, the media picker can be displayed as a fullscreen presented view, but it also works reasonably well in a popover, especially if we increase its preferredContentSize. This code presents as fullscreen on an iPhone and as a reasonably-sized popover on an iPad:

let picker = MPMediaPickerController(mediaTypes:.music)
picker.delegate = self
// picker.allowsPickingMultipleItems = true
picker.modalPresentationStyle = .popover
picker.preferredContentSize = CGSize(500,600)
self.present(picker, animated: true)
if let pop = picker.popoverPresentationController {
    if let b = sender as? UIBarButtonItem {
        pop.barButtonItem = b
    }
}
..................Content has been hidden....................

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