Creating Classes

Many programmers—professionals and students, hobbyists and cowboy coders—have grown up in the mind-set of object-oriented programming. As Janie once said on the NSBrief podcast, “I didn’t think I was learning object-oriented programming. I thought I was learning programming…like that was the only way to do it.”

And it’s not like anyone’s wrong to learn OO! It’s the dominant paradigm for a good reason: it has proven over the decades to be a good way to write applications. Whole languages are built around the concepts of OO: it’s nigh-impossible to break out of the OO paradigm in Java, and Objective-C has OO in its very name, after all!

So let’s see how Swift supports object-oriented programming. The heart and soul of OO is to create classes, collections of common behavior from which we will create individual instances called objects. We’ll begin by creating a new playground called ClassesPlayground, and deleting the "Hello, playground" line as usual.

In the last chapter’s collections examples, we used arrays, sets, and dictionaries to represent various models of iOS devices. But it’s not easy or elegant to collect much more than a name that way, and there are lots of things we want in an iOS device model. So we will create a class to represent iOS devices.

We’ll start by tracking a device’s model name and its physical dimensions: width and height. Type the following into the playground:

 class​ ​IOSDevice​ {
 var​ name : ​String
 var​ screenHeight : ​Double
 var​ screenWidth : ​Double
 }

In Swift, we declare a class with the class keyword, followed by the class name. If we were subclassing some other class, we would have a colon and the name of the superclass, like class MyClass : MySuperclass, but we don’t need that for this simple class.

Next, we have properties, the variables or constants associated with an object instance. In this case, we are creating three variables: name, screenHeight, and screenWidth.

There’s just one problem: this code produces an error. We need to start thinking about how our properties work.

Properties

The error flag tells us “Class IOSDevice has no initializers,” and the red-circle instant-fix icon offers three problems and solutions. The problem for each is that there is no initial value for these properties. Before accepting the instant fix, let’s consider what the problem is.

The properties we have defined are not optionals, so, by definition, they must have values. The tricky implication of that is that they must always have values. The value can change, but it can’t be absent: that’s what optionals are for.

We have a couple of options. We could accept the instant-fix suggestions and assign default values for each. That would give us declarations like

 var​ name : ​String​ = ​""
 var​ screenHeight : ​Double​ = 0.0
 var​ screenWidth : ​Double​ = 0.0

That’s one solution, as long as we’re OK with the default values. But here they don’t quite make sense because we probably never want an iOS device with an empty string for a name.

Plan B: we can make everything optionals. To do this, we append the optional type ? to the properties.

 var​ name : ​String​?
 var​ screenHeight : ​Double​?
 var​ screenWidth : ​Double​?

Again, no more error, so that’s good. Problem now is that any code that wants to access these properties has to do the if let dance from the last chapter to safely unwrap the optionals. And again, do we ever want the device name to be nil? That seems kind of useless.

Fortunately, we have another alternative: Swift’s rule is that all properties must be initialized by the end of every initializer. So we can write an initializer to take initial values for these properties, and since that will be the only way to create an IOSDevice, we can know that these values will always be populated.

So rewrite the class like this:

1: class​ ​IOSDevice​ {
var​ name : ​String
var​ screenHeight : ​Double
var​ screenWidth : ​Double
5: 
init (name: ​String​, screenHeight: ​Double​, screenWidth: ​Double​) {
self​.name = name
self​.screenHeight = screenHeight
self​.screenWidth = screenWidth
10:  }
}

The initializer runs from lines 6 to 10. The first line is the important one, as it starts with init and then takes a name and type for each of the parameters to be provided to the initializer code. In the initializer itself, we just use the self keyword to assign the properties to these arguments.

Easy Come, Easy Go

images/aside-icons/note.png

All types also have a deinitializer, which is called when the object is destroyed. This means that if your object needs to clean things up before it disappears, you can just override deinit.

To create an instance of IOSDevice, we call the initializer by the name of the class, and provide these arguments by name. Create the constant iPhone7 after the class’s closing brace, as follows (note that a line break has been added to suit the book’s formatting; it’s OK to write this all on one line).

 let​ iPhone7 = ​IOSDevice​(name: ​"iPhone 7"​,
  screenHeight: 138.1, screenWidth: 67.0)

Congratulations! You’ve instantiated your first custom object, as the “IOSDevice” in the results pane indicates. Notice that the names of the arguments to the initializer are used as labels in actually calling the initializer. This helps us keep track of which argument is which, something that can be a problem in other languages when you call things that have lots of arguments.

Computed Properties

The three properties we’ve added to our class are stored properties, meaning that Swift creates the in-memory storage for the String and the two Doubles. We access these properties on an instance with dot syntax, like iPhone7.name.

Swift also has another kind of property, the computed property, which is a property that doesn’t need storage because it can be produced by other means.

Right now we have a screenWidth and a screenHeight. Obviously, it would be easy to get the screen’s area by just multiplying those two together. Instead of making the caller do that math, we can have IOSDevice expose it as a computed property. Back inside the class’s curly braces—just after the other variables and before the init is the customary place for it—add the following:

 var​ screenArea : ​Double​ {
 get​ {
 return​ screenWidth * screenHeight
  }
 }

Back at the bottom of the file, after creating the iPhone7 constant, fetch the computed property by calling it with the same dot syntax as with a stored property:

 iPhone7.screenArea

The results pane shows the computed area, 9,252.7 (or possibly 9252.699…).

With only a get block, the screenArea is a read-only computed property. We could also provide a set, but that doesn’t really make sense in this case.

It’s also possible for stored properties to run arbitrary code; instead of computing values, we can give stored properties willSet and didSet blocks to run immediately before or after setting the property’s value. We’ll use this approach later on in the book.

Methods

Speaking of running arbitrary code, one other thing we expect classes to do is to let us, you know, do stuff. In object-oriented languages, classes have methods that instruct the class to perform some function. Of course, Swift makes this straightforward.

Let’s take our web radio player from the first chapter and add that to our IOSDevice. After all, real iOS devices are used for playing music all the time, right? We’ll start by adding the import statement to bring in the audio-video APIs, and the special code we used to let the playground keep playing. Add the following at the top of the file, below the existing import UIKit line:

 import​ ​AVFoundation
 import​ ​PlaygroundSupport
 PlaygroundPage​.current.needsIndefiniteExecution = ​true

We need our IOSDevice to have an AVPlayer we can start and stop, so add that as a property after the existing name, screenHeight, and screenWidth:

 private​ ​var​ audioPlayer : ​AVPlayer​?

Notice that this property is an optional type, AVPlayer?, since it will be nil until it is needed.

Now, let’s add a method to the class. We do this with the func keyword, followed by the method name, a list of arguments, and a return type. Add this playAudio method somewhere inside the class’s curly braces, ideally after the init’s closing brace, since we usually write our initializers first and our methods next.

 func​ playAudioFrom(url: ​URL​) -> ​Void​ {
  audioPlayer = ​AVPlayer​(url: url)
  audioPlayer!.play()
 }

Like the init, the parentheses contain the parameters to the method and their types. In Swift, we label each parameter, which is why we have url: preceding the URL type. If playAudioFrom also took a rate argument, we would call it like playAudioFrom(url: someURL, rate: 1.0). Compared to some languages, the labeled parameters may seem chatty or verbose, but in practice they make the code more readable by exposing what each value is there for.

Inner and Outer Parameter Names

images/aside-icons/tip.png

In fact, a parameter can have two names—an “outer” name that callers see, and an “inner” name used as a variable inside the method. So it would be somewhat more elegant to declare this method as:

 func playAudio(fromURL url: URL)

and then call it like this:

 playAudio(fromURL: foo)

We’ll use outer names when they make our code more elegant, either for callers or inside the methods’ implementations.

After the parameters, the return type is indicated by the -> arrow. In this case, the method returns nothing, so we return Void. (In fact, when we return Void we can omit the arrow and the return type.) The rest of the method is the two lines of code we used in the first chapter to create the AVPlayer and start playing.

Now let’s call it and start playing music. Put the following at the bottom of the file, after where we create the iPhone7 instance.

 if​ ​let​ url = ​URL​(string: ​"http://www.npr.org/streams/aac/live1_aac.pls"​) {
  iPhone7.playAudioFrom(url: url)
 }

The first line attempts to create a URL out of the provided string. We use an if let because, if our string is garbage, what we get back from the initializer could be nil. This is because the URL type provides a failable initializer, one that reserves the right to return nil instead of a new object. It’s denoted this way in the documentation with the keyword init?, where the ? clues us in to the fact that optionals are in play.

Wrapping this in an if let means that we will only enter the curly-braced region if the initialization succeeds and assigns the value to the local variable url. This is the proper practice for failable initializers and gets around the bad practice we used in the first chapter when we just force-unwrapped the URL? optional with the ! operator.

And once we’re safely inside the if let, we call the playAudioFrom method that we just wrote, and the music starts playing. If we wanted to write a proper stopAudio method, that would look like this:

 func​ stopAudio() -> ​Void​ {
 if​ ​let​ audioPlayer = audioPlayer {
  audioPlayer.pause()
  }
  audioPlayer = ​nil
 }

Again, we use an if let to safely unwrap the audioPlayer optional, and only if that succeeds do we pause it. Then we can set audioPlayer back to nil.

Turn That Music Down

images/aside-icons/tip.png

Remember that any change to the playground text will cause the contents to be rebuilt and rerun, which means that any change we make from here out will restart the audio. It’s funny the first few times, but it gets annoying.

If you want to turn it off, just comment out the call to playAudioWithURL. Swift uses the same comment syntax as all C-derived languages (Objective-C, C#, Java, etc.). That means you can either put // on the start of a line to turn it into a comment, or surround a whole range of lines with a starting /* and a closing */.

Protocols

Swift classes are single-inheritance, in that a given class can have only a single superclass. We can’t declare that IOSDevice is a subclass of two different classes and inherit the behaviors of both. (In practice, that kind of thing gets messy!) Actually, IOSDevice isn’t currently declared as the subclass of anything, so it’s just a generic top-level class.

In many languages, we can get common behavior across multiple classes by providing a list of methods that all of them are expected to implement. In Java and C#, for example, the interface keyword performs this function. In Swift, we have protocols, and types that provide implementations for methods defined in a protocol are said to “conform to” the protocol. In Swift, protocols aren’t limited to methods: they can also specify that a given property is to be made available.

Let’s try it out to do something useful. At the bottom of the file where we create the iPhone7, and then again on the line that plays the music, the evaluation pane on the right just says IOSDevice. That’s because those lines evaluate to just the iPhone7 object, but the playground doesn’t know what it can tell us about the object other than its class. We can do better than that.

Swift defines a protocol called CustomStringConvertible that lets any type declare how it is to be represented as a String. Playgrounds use this for the evaluation pane, as does print when using the () substitution syntax, like in print ("I just bought this: (iPhone7)"). To implement CustomStringConvertible, we just need to provide a property called description, whose type is a String.

To implement the protocol, we first have to change our class definition. In Swift, the class keyword is followed by a colon, the superclass that our class subclasses (if any), and then a comma-separated list of protocols we implement. So rewrite the class definition like this:

 class​ ​IOSDevice​ : ​CustomStringConvertible​ {

As soon as we do this, we will start seeing an error message. That’s OK, because the error is that we don’t yet conform to the protocol, since we haven’t provided a suitable description. Let’s do so now, as a computed property. Put this right before or after our other computed property, the screenArea:

 var​ description: ​String​ {
 return​ ​"​​(​name​)​​, ​​(​screenHeight​)​​ x ​​(​screenWidth​)​​"
 }

This method just uses string substitution to show the device name and its dimensions. As soon as we finish writing this, the evaluation pane starts using this description instead of the bare class name:

images/stylishswift/playground-description.png

There are many other protocols we’ll be implementing throughout the book. Some, like CustomStringConvertible, come from the Swift language itself, but most are from UIKit and the other iOS frameworks we’ll be working with.

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

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