Learning Swift(Second Edition)
上QQ阅读APP看书,第一时间看更新

Projects

If we want to move away from developing with a single file, we need to move away from playgrounds and create our first project. In order to simplify the project, we are going to create a command-line tool. This is a program without a graphical interface. As an exercise, we will redevelop our example program from Chapter 2, Building Blocks – Variables, Collections, and Flow Control which managed invitees to a party. We will develop an app with a graphical interface in Chapter 11, A Whole New World – Developing an App.

Setting up a command-line Xcode project

To create a new command-line tool project, open Xcode and from the menu bar on the top, select File | New | Project…. A window will appear allowing you to select a template for the project. You should choose Command Line Tool from the OS X | Application menu:

From there, click Next and then give the project a name like Learning Swift Command Line. Any Organization Name and Identifier are fine. Finally, make sure that Swift is selected from the Language dropdown and click Next again. Now, save the project somewhere that you can find later and click Create.

Xcode will then present you with the project development window. Select the main.swift file on the left and you should see the Hello, World! code that Xcode has generated for you:

This should feel pretty similar to a playground except that we can no longer see the output of the code on the right. In a regular project like this, the code is not run automatically for you. The code will still be analyzed for errors as you write it, but you must run it yourself whenever you want to test it. To run the code, you can click the run button on the toolbar, which looks like a play button.

The program will then build and run. Once it does, Xcode shows the console on the bottom where you will see the text Hello, World! which is the result of running this program. This is the same console as we saw in playgrounds.

Unlike a playground, we have the Project Navigator along the left. This is where we organize all of the source files that go into making the application work.

Creating and using an external file

Now that we have successfully created our command-line project, let's create our first new file. It is common to create a separate file for each type that you create. Let's start by creating a file for an invitee class. We want to add the file to the same file group as the main.swift file, so click on that group. You can then click on the plus sign (+) in the lower left of the window and select New File. From that window, select OS X | Source | Swift File and click Next:

The new file will be placed in whatever folder was selected before entering the dialog. You can always drag a file around to organize it however you want. A great place for this file is next to main.swift. Name your new file Invitee.swift and click Create. Let's add a simple Invitee structure to this file. We want Invitee to have a name and to be able to ask them to the party with or without a show:

// Invitee.swift
struct Invitee {
    let name: String

    func askToBringShowFromGenre(genre: ShowGenre) {
        print("\(self.name), bring a \(genre.name) show")
        print("\(genre.example) is a great \(genre.name)")
    }

    func askToBringThemselves() {
        print("\(self.name), just bring yourself")
    }
}

This is a very simple type and does not require inheritance, so there is no reason to use a class. Note that inheritance is not the only reason to use a class, as we will see in later chapters but, for now, a structure will work great for us. This code provides simple, well-named methods to print out the two types of invites.

We are already making use of a structure that we have not created yet called ShowGenre. We would expect it to have a name and example property. Let's implement that structure now. Create another file called ShowGenre.swift and add the following code to it:

// ShowGenre.swift
struct ShowGenre {
    let name: String
    let example: String
}

This is an even simpler structure. This is just a small improvement over using a tuple because it is given a name instead of just properties and it also gives us finer control over what is constant or not. It may seem like a waste to have an entire file for just this but this is great for maintainability in the future. It is easier to find the structure because it is in a well-named file and we may want to add more code to it later.

An important principle in code design is called separation of concerns. The idea is that every file and every type should have a clear and well-defined concern. You should avoid having two files or types responsible for the same thing and you want it to be clear why each file and type exists.

Interfacing with code from other files

Now that we have our basic data structures, we can use a smarter container for our list of invitees. This list contains the logic for assigning a random invitee a genre. Let's start by defining the structure with some properties:

// InviteList.swift
struct InviteList {
    var invited: [Invitee] = []
    var pendingInvitees: [Invitee]

    init(invitees: [Invitee]) {
        srand(UInt32(clock()))
        self.pendingInvitees = invitees
    }
}

Instead of storing a single list of both invited and pending invitees, we can store them in two separate arrays. This makes selecting a pending invitee much easier. This code also provides a custom initializer, so that all we need to provide from other classes is an invitee list without worrying whether or not it is a list of pending invitees. We could have just used the default initializer but the parameter would then have been named pendingInvitees. We also seed the random number generator for later use.

Note that we did not need to provide a value for invited in our initializer because we gave it the default value of an empty array.

Note also that we are using our Invitee structure freely in this code. Swift automatically finds code from other files in the same project and allows you to use it. Interfacing with code from other files is as simple as that.

Now, let's add a helper function to move an invitee from the pendingInvitee list to the invited list:

// InviteList.swift
struct InviteList {

    // ...

    // Move invitee from pendingInvitees to invited
    //
    // Must be mutating because we are changing the contents of
    // our array properties
    mutating func invitedPendingInviteeAtIndex(index: Int) {
        // Removing an item from an array returns that item
        let invitee = self.pendingInvitees.removeAtIndex(index)
        self.invited.append(invitee)
    }
}

This makes our other methods cleaner and easier to understand. The first thing we want to allow is the inviting of a random invitee and then asking them to bring a show from a specific genre:

// InviteList.swift
struct InviteList {

    // ...

    // Must be mutating because it calls another mutating method
    mutating func askRandomInviteeToBringGenre(genre: ShowGenre) {
        if self.pendingInvitees.count > 0 {
            let randomIndex = Int(rand()) % self.pendingInvitees.count
            let invitee = self.pendingInvitees[randomIndex]
            invitee.askToBringShowFromGenre(genre)
            self.invitedPendingInviteeAtIndex(randomIndex)
        }
    }
}

The picking of a random invitee is much cleaner than in our previous implementation. We can create a random number between 0 and the number of pending invitees instead of having to keep trying a random invitee until we find one that hasn't been invited yet. However, before we can pick that random number, we have to make sure that the number of pending invitees is greater than zero. If there were no remaining invitees we would have to pide the random number by 0 in Int(rand()) % self.pendingInvitees.count. This would cause a crash. It has the extra benefit of allowing us to handle the scenarios where there are more genres than invitees.

Lastly, we want to be able to invite everyone else to just bring themselves:

// InviteList.swift
struct InviteList {

    // ...

    // Must be mutating because it calls another mutating method
    mutating func inviteeRemainingInvitees() {
        while self.pendingInvitees.count > 0 {
            let invitee = self.pendingInvitees[0]
            invitee.askToBringThemselves()
            self.invitedPendingInviteeAtIndex(0)
        }
    }
}

Here, we have simply repeatedly invited and removed the first pending invitee from the pendingInvitees array until there are none left.

We now have all of our custom types and we can return to the main.swift file to finish the logic of the program. To switch back, you can just click on the file again in Project Navigator (the list of files on the left). Here, all we want to do is to create our invitee list and a list of genres with example shows. Then, we can loop through our genres and ask our invitee list to do the inviting:

var inviteeList = InviteList(invitees: [
    Invitee(name: "Sarah"),
    Invitee(name: "Jamison"),
    Invitee(name: "Marcos"),
    Invitee(name: "Roana"),
    Invitee(name: "Neena"),
])

let genres = [
    ShowGenre(name: "Comedy", example: "Modern Family"),
    ShowGenre(name: "Drama", example: "Breaking Bad"),
    ShowGenre(name: "Variety", example: "The Colbert Report"),
]

for genre in genres {
    inviteeList.askRandomInviteeToBringGenre(genre)
}
inviteeList.inviteeRemainingInvitees()

That is our complete program. You can now run the program by clicking the Run button and examine the output. You have just completed your first real Swift project!

File organization and navigation

As your project gets larger, it can be cumbersome to have just one single list of files. It helps to organize your files into folders to help differentiate which role they are playing in your app. In Project Navigator, folders are called groups. You can create a new group by selecting the group you would like to add the new group to, and going to File | New | Group. It isn't terribly important exactly how you group your files; the important thing is that you should be able to come up with a relatively simple system that makes sense. If you are having trouble doing that, you should consider how you could improve the way you are breaking up your code. If you are having trouble categorizing your files, then your code is probably not being broken up in a maintainable way.

I would recommend using lots of files and groups to better separate your code. However, the drawback of that is that Project Navigator can fill up pretty quickly and become hard to navigate around. A great trick in Xcode to navigate to files more quickly is to use the keyboard shortcut Command + Shift + O. This displays the Open Quickly search. Here, you can start to type the name of the file you want to open and Xcode shows you all of the matching files. Use the arrow keys to navigate up and down and press Enter to open the file you want.