Using important Swift features

We've now covered the basics of Swift, in addition to the building blocks we need to create robust classes and organized data with structs and enums. With these tools, you'd be able to accomplish some great things, but there are a few other important features of Swift that can save you a lot of time, and help you squeeze out even more performance from your code. In this last section, we are going to introduce closures, protocols, class extensions, and Swift's error handling features.

Closures

We've already talked about functions, where we can take a chunk of code and turn it into a reusable command. However, in Swift there is another way to achieve that kind of functionality (no pun intended): closures. Using closures is a great way to pass a chunk of code (sometimes called a block) into a function as an argument, and they're commonly used as completion or error handers. Let's take a look at an example:

// defining a simple closure
let myClosure: () -> Void = {
    print("Hello from this closure!")
}

// executing a simple closure
myClosure()

// using closure like a variable
someOtherFunction(closure: myClosure)

In the first part, we create a closure in a way that is similar to declaring a variable, and also similar to declaring a function. We start out with a let (or var) keyword followed by the name of the closure. Then we declare the closure's type, which is composed of the closure's input and output type signature. Here we see that the type is () -> Void, meaning it has no parameters, and a void return type. Finally, we set the closure equal to a code block surrounded by curly braces.

Below that, you'll see that executing a closure looks exactly the same as calling a function. The fun part, however, is what else we can do with a closure. At the bottom you can see that we're passing our closure into another function, like a variable. That's because as long as we don't use the () at the end of the closure's name, it behaves just like a variable.

Let's take a look at a closure with a more interesting type signature:

let convertIntToFloat = { (value: Int) -> Float in
    return Float(value)
}

let myNewFloat: Float = convertIntToFloat(6)

The first thing you might notice here is that we don't define the type signature like we did in the first closure; Swift can infer the type here as well! To define parameters and return types in a closure, we begin the closure with the () -> () pattern followed by the in keyword. On the left side of the -> arrow, we put our parameters inside parentheses in the same format we do for a function declaration. On the right side of the arrow, we do the same for our return value types, although with a single return type we don't need parentheses.

Note

Closures (and functions) can return multiple values using something called a tuple. A tuple is a grouping of several values into a single value. A tuple can be defined as follows:

let myTuple: (Float, Int) = (3.14, 33)

The two (or three, or four, and so on) values in a tuple don't have to be the same type. You'll see them fairly frequently when coding in Swift, and I encourage you to play around with them in a playground!

So, looking at this new closure, we can see that the first line reads (value: Int) -> Float in. We can tell that the closure takes one parameter, an Int named value, and returns a Float. Then in the example below that, we see that when we execute the closure we pass it an integer, and assign the result to a Float.

Closures are used extensively throughout iOS frameworks, so you'll be seeing them quite often. We could probably spend a whole chapter just looking at the many forms they can take, but we need to keep moving on so we can start making apps!

Protocols

Since you'll be making a lot of subclasses of common objects when developing for iOS apps in Swift (such as UIViewControllers), class hierarchies need to be as streamlined as possible. Sometimes, you need to ensure that a class is equipped to perform a certain task, but class inheritance doesn't really make sense or the object already inherits from another object. In these instances, you can use what is called a protocol.

As you might be able to guess, a protocol is a set of instructions that a class must adhere to. It is similar to a class, except instead of programming functionality into a protocol, you only describe what variables and methods need to be implemented; it's up to the actual class that adopts the protocol to give them definitions. Here's an example:

protocol MyProtocol {
    var value: Float { get set }
    func someMethod(someParameter: Float) -> Int
}

class MyClass : MyProtocol {
    var value: Float = 10
   
    func someMethod(someParameter: Float) -> Int {
        return Int(value * someParameter)
    }
}

First we define a new protocol just like a class, but with the protocol keyword. Inside the curly braces, you can see some minor differences. First, we describe a property by assigning it a type, and determining if it should have a getter and/or setter. Then we describe a function using only the method signature; there is no implementation given for this method.

Later on, we make a simple class that adopts our new protocol. You'll notice that it looks very similar to the way we specify a parent class. Inside the body of our class, you can see that we actually have to redefine the properties and methods of the protocol since there was no real inheritance involved. If you don't implement all of the properties and methods exactly as described in the protocol, the compiler will complain.

While it is a very useful tool, and you can certainly create your own protocols, most beginners will usually spend a lot of time implementing protocols that exist in iOS frameworks. Here's an example of one of the most common cases:

Class MyViewController: UIViewController, UITableViewDataSource {…}

Here you can see that we are creating a new subclass of UIViewController named MyViewController, but we use a comma after UIViewController to show that we are also adopting the UITableViewDataSource protocol. In fact, we can inherit from a class and adopt many protocols at once by continuing to separate them with commas.

Class extensions

Sometimes you come across a situation where you need something to have a bit more functionality. In some cases, it might make sense to create a subclass, but in Swift there is a smarter and cleaner way to achieve the same result. With extensions, you can add functionality to any type, even to a class that isn't part of your source code. Specifically, in Swift 2 and Swift 3, we can extend classes. There's not much else to say, so let's look at how this works:

class BoringClass {
    var boringInt: Int = 0
    var moreBoringInt: Int = 0
   
    func add() -> Int {
        return boringInt + moreBoringInt
    }
}

First we have a BoringClass, which stores two values and a single function that adds the two values together. Now let's imagine that we didn't write that BoringClass, so we couldn't add functionality directly. Instead we could extend the class, like so:

extension BoringClass {
    func subtract() -> Int {
        return boringInt - moreBoringInt
    }
    func multiply() -> Int {
        return boringInt * moreBoringInt
    }
    func divide() -> Float {
        if moreBoringInt != 0 {
            return Float(boringInt / moreBoringInt)
        } else {
            fatalError("tried to divide by zero")
        }
    }
}

Now our class is decidedly less boring, and much more functional.

While class extensions are certainly useful, you can extend a lot of other things as well. For example, we can even extend an Int:

extension Int {
    var inches: Int { return self }
    var feet: Float { return Float(inches / 12) }
    var yards: Float { return feet/3.0 }
    var miles: Float { return feet/5280 }
}

let distance = 3392
print(distance.inches)  //  3,392
print(distance.feet)    //  282.0
print(distance.yards)    //   94.0
print(distance.miles)    // 0.0534

While extensions can be amazingly useful, you should always think about all the ways to solve a problem and choose the best fit for the job. Sometimes it is just better to use a subclass.

Error handling

When building an application for use in the real world, sometimes there are errors that will occur at runtime which are okay. For example, if you tried to modify a message that had been deleted since that page had last refreshed, or if a network connection is lost while transmitting a file. These are the kinds of error that you can't prevent from a programming standpoint, so instead you just need to know how to react and alert the user. For these instances, we can use the error handling functionality built into Swift.

Before you can handle an error, you need to be able to define it. In Swift, there is an empty protocol called ErrorProtocol that you can use to denote that some value represents an error and can be used for error handling. Adding the ErrorProtocol protocol to an enum allows you to create some very simple and descriptive error types for the problems you may encounter.

Let's say that we're making a class that models a coffee machine. The user can press a button that will brew them a cup of coffee, but we need to make sure that we are prepared for cases in which the machine cannot complete the task. We'll create an enum that stores all the possible cases for failure:

enum CoffeeMachineError: Error {
    case NotEnoughWater
    case NotEnoughGrounds
    case ReplaceFilter
}

So, here we've defined a new enum called CoffeeMachineError, which adopts the protocol Error:

class CoffeeMachine {
    let groundsNeededPerCup: Float = 0.1
    let waterNeededPerCup: Float = 0.33
   
    var grounds: Float = 10.0
    var water: Float = 10.0
   
    func brewCup() throws  {
        guard grounds > groundsNeededPerCup else {
            throw CoffeeMachineError.NotEnoughGrounds
        }
       
        guard water > waterNeededPerCup else {
            throw CoffeeMachineError.NotEnoughWater
        }
       
        grounds -= groundsNeededPerCup
        water -= waterNeededPerCup
        print("coffee is brewed")
    }
}

Here, we've modeled the rest of the coffee machine; it knows how much coffee and water it needs to brew a cup, the current levels of those supplies, and a function that attempts to brew.

In this example, we use a throwing function to handle errors. At the end of the brewCup function declaration, you'll see that we wrote throws instead of a standard return type. This means that the function has the ability to throw an error. You'll also see that we use the guard keyword that we looked at earlier in the chapter. Here it makes a bit more sense, since we are saying guard against this undesirable condition occurring; otherwise, complain. As long as the code makes it through all of the guards there won't be a problem, but if the guards get upset they can throw an error.

So this is what it would look like to call this function that might throw an error:

let coffeeMachine = CoffeeMachine()

do {
    try coffeeMachine.brewCup()
   
} catch CoffeeMachineError.NotEnoughWater {
    print ("Please refill the water container")
   
} catch CoffeeMachineError.NotEnoughGrounds {
    print ("Please add more coffee grounds")
   
} catch CoffeeMachineError.ReplaceFilter {
    print ("Please replace the filter")
}

To handle the errors that can be thrown, we use a do-catch statement, and call our throwing function with a try statement which is placed in the do block. We need to use try to call our function because of the fact that it might return an error. If it completes successfully, our code will carry on outside of the do-catch statement.

In order to handle an error, we need to catch them. After the _ block that we wrote here, we have a separate catch statement for each of our possible errors. If one of those errors is caught, it will execute the code in that branch. In this example, we are just printing to the console, but in theory we could require user input or actually fetch new materials, whatever will best resolve the error.

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

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