Connecting Outlets

In fact, our player is so successful, there’s no way to actually stop it in the app. Click the Stop button on the Xcode toolbar to kill the app.

Let’s make our button both a Play and Pause button. To do that, we should only play if the player isn’t already playing. If it is playing, we need to pause.

A look at the AVPlayer documentation shows us the two pieces of information we need to make this work. First, in the “Managing Playback” section, right after play, there’s a pause method. And just after that, there’s rate, a Float that indicates how fast the media is playing, where 0.0 is paused and 1.0 is normal speed forward. This also means if we wanted to provide the popular “play podcast at one-and-a-half speed” feature, we could just set the rate to 1.5.

With rate and pause, we can rewrite handlePlayPauseTapped to properly support playing and pausing, as its name implies:

 @IBAction​ ​func​ handlePlayPauseTapped(_ sender: ​Any​) {
 if​ ​let​ player = player {
 if​ player.rate == 0 {
  player.play()
  } ​else​ {
  player.pause()
  }
  }
 }

Run again, press the app’s Play button, and once it’s playing, press again to pause. We can now play and pause to our heart’s delight.

This is great, but we’re still not done: the button only ever says “Play.” Wouldn’t it be nice to have it say “Pause” when it’s playing, so the button always represents what tapping it will actually do? Also, we have those other parts of the UI we’ve never filled in, like the current track label.

To do that, we need more connections. So far, we have an IBAction, a connection that goes from the UI to the code. Now we need to go from the code to the UI, to change the text of labels and buttons. For that, we need IBOutlets.

Creating and Coding Outlets

Recall that we talked about outlets back in Connecting Actions. Whereas an action goes from a UI component in the storyboard to a method, an outlet goes from a property in our code to a UI component. The idea is that once we have outlets as properties, we can use them like any other variables: we can set their values, call methods on them, and so on.

We make an outlet the same way we made the action from the button: with a Control-drag from the storyboard to the source. To do that, switch to the Main.storyboard file, and then use the toolbar to switch to the Assistant Editor (the button with the two rings). Make sure the right pane is showing our ViewController.swift files; the first tab of its breadcrumb-style menus has an Automatic item that should do the right thing.

In the left pane, select the “Track Title” label, and Control-drag into the source code in the right pane. As you hover near the private var player : AVPlayer that we already created, the drag point will show a box that says “Insert Outlet or Outlet Collection,” as shown in the figure. Release the drag here to create the outlet.

images/connecting/xcode-connect-outlet.png
images/connecting/xcode-outlet-popover.png

When you release the drag, a pop-up appears to fill in details of the connection. We saw this before when we created the button’s action. We need to fill in these values and click Connect to actually create the outlet.

Leave Connection set to Outlet, and enter titleLabel for the name. Leave the Type as UILabel, make sure the Storage is Strong, and click Connect. This creates a new property in the ViewController.swift source file:

 @IBOutlet​ ​var​ titleLabel: ​UILabel​!

Now repeat this same process with the button: select it, Control-drag it into the source file, and create an outlet with the name playPauseButton and type UIButton. This should create the following line of code:

 @IBOutlet​ ​var​ playPauseButton: ​UIButton​!

Now we have properties for a UILabel and UIButton that we can call methods on, just like any other variables in our app. Let’s start easy with the titleLabel.

We won’t have a proper podcast episode title until later when we’re parsing real podcast feeds. For now, we could just take the last part of the URL. Checking the documentation for the URL class shows that this is available as the property lastPathComponent. And if we look up UILabel, we find the way to change what it’s showing is to set its text property. So now we have a simple way to update the UI. Rewrite set(url:) as follows:

 func​ setURL(url: ​URL​) {
  player = ​AVPlayer​(url: url)
  titleLabel.text = url.lastPathComponent
 }

Run the app again, and notice that as soon as it comes up, the title label shows the filename from the URL.

images/connecting/simulator-title-label.png

Looking good! We calculated a string in text (the lastPathComponent), and by just setting a property on an IBOutlet, we were able to update what the user sees in the UI. It’ll be even better later when we can get a real title from the podcast feed. It’s still good progress for now.

Speaking of getting data from the podcast feed, we also said in the last chapter that we were going to get each podcast’s logo and put it in the image view. We’re not ready to do that yet, but let’s create the connection now, so it’s ready to populate later. Control-drag from the image view into the source to create another outlet. This one can be called logoView.

 @IBOutlet​ ​var​ logoView: ​UIImageView​!

Constantly Changing Outlets

Of course, we still have the Play/Pause button that always says “Play.” We need a way to fix that. The docs tell us that UIButton has a method setTitle(for:) that takes a string title and a UIControlState (so we could have, say, different titles for normal and disabled button states).

The question, actually, is knowing whether to set the title as Play or Pause, and when to change it. We could just do so in our button-tap handler, but that runs the risk of getting out of sync (for example, if we change the label because we think the audio is playing, but for some reason it failed to actually start playing). Also, that approach wouldn’t automatically reset the button when the audio stops playing at the end of the file.

We want this button to use the playing state of the AVPlayer as its model, and for the player to tell us when it needs us to update the button.

If we look in the AVPlayer documentation, we find there’s a discussion called “General State Observations,” which says:

You can use key-value observing (KVO) to observe state changes to many of the player’s dynamic properties, such as its currentItem or its playback rate.

Perfect! Observing changes to the rate is exactly what we need. So, what the heck is KVO?

Key-value observing is a somewhat older Mac and iOS technology that implements an observer pattern. Interested objects can start observing a given property (if it supports KVO; not all properties do), and when it changes, they get a callback. We start observing by calling the addObserver method on objects that support KVO, and when the value changes, we get a callback on a method called observeValue(forKeyPath:of:context:. In other words, we write a method with that signature, and it gets called automatically when the value we’re observing changes.

So we start by telling the player that we want to observe its rate. We’ll do this when we create it, in set(url:).

 func​ set(url: ​URL​) {
  player = ​AVPlayer​(url: url)
  titleLabel.text = url.lastPathComponent
  player?.addObserver(​self​,
  forKeyPath: ​"rate"​,
  options: [],
  context: ​nil​)
 }

The addObserver call takes four parameters:

  • observer: The object to call back to. In this case, it’s self (that is to say, the ViewController class we’re writing).

  • forKeyPath: The property we want to observe, which is the player’s rate.

  • options: Behavior options for the callback, like whether both old and new values should be delivered.

  • context: A C-style pointer that’s not useful in Swift (it’s left over from the Objective-C language that iOS and Mac apps originally used).

Why It’s Called Key Path

images/aside-icons/info.png

The key path is interesting—and is named the way it is—because it lets us observe properties of properties. For example, the AVPlayer has a currentItem, which itself has observable properties like status. So we could observe changes to the status by using the key path currentItem.status. The keyPath, as it turns out, is a string representing a dot-separated path of properties.

KVO requires us to remove any observers we add, and failure to do so is a crashing bug. That will matter more later when this isn’t the only scene in the app. For now, let’s remove the observer when this object is purged from memory. When that happens, the deinit method—so named because it’s the opposite of the init that we’re familiar with—is called. We typically put deinit near the top of the class, after any init overrides and before any instance methods, so do that here:

 deinit​ {
  player?.removeObserver(​self​, forKeyPath: ​"rate"​)
 }

Now we’re finally ready to write our observer. This will be called any time the player’s rate changes, giving us an opportunity to update our UI.

1: override​ ​func​ observeValue(forKeyPath keyPath: ​String​?,
of object: ​Any​?,
change: [​NSKeyValueChangeKey​ : ​Any​]?,
context: ​UnsafeMutableRawPointer​?)
5: {
if​ keyPath == ​"rate"​,
let​ player = object ​as?​ ​AVPlayer​ {
playPauseButton.setTitle(player.rate == 0 ? ​"Play"​ : ​"Pause"​,
for: .normal)
10:  }
}

Lines 1-5 are the boilerplate for the method signature. Fortunately, you can just start typing observeValue and Xcode should offer you an auto-complete, which you can accept by just pressing the tab key ().

What matters is how we respond to the event, starting on line 6 where we check to see that this is even the rate property that we’re interested in (since all events this object is observing will call back to this same method). If so, then on line 7, we can try casting the sender to an AVPlayer by using an if let. Granted, we could just access self.player directly, but preferring to work with the information provided by the event is better practice.

Finally, we’re ready to make use of our outlet to the button! On lines 8-9, we call the button’s setTitle(for:) method to set the title for the .normal state. And the title we use is based on the rate of the player. If it’s 0, we’re paused and the button should say “Play.” For any other value, the player is playing, so the button should say “Pause.”

We’re done with our KVO implementation, so click Run on the toolbar to try it out. When you tap the Play button, it should now update to be a Pause button as the audio starts playing, and vice versa. Pretty slick!

images/connecting/simulator-pause-button.png

The last part of our UI that isn’t wired up yet is the current time label. There are a couple ways we could implement that, but the cleanest will require learning a new coding technique, so we’re going to put it off for a little bit.

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

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