Quantcast
Channel: Envato Tuts+ Code - Mobile Development
Viewing all 1836 articles
Browse latest View live

Create a Plane Fighting Game in Corona: Interface

$
0
0
Final product image
What You'll Be Creating

Introduction

In this short series, I will be showing you how to create a plane fighting game reminiscent of the old 1942 arcade game. Along the way, you'll learn about Corona's storyboard functionality, timers, moving a character, and a bit of trigonometry.

The graphics used in this tutorial were created by Ari Feldman and provided under the Common Public License, which means that they are free to use. Make sure to check out Ari Feldman's SpriteLibLet's get started.

1. New Project

Open the Corona Simulator, click New Project, and configure the project as shown below. Select a location to save your project and click OK. This will create a folder with a number of icons and three files that are important to us, main.lua, config.lua, and build.settings. We'll take a look at each file in the next few steps.

2. Build.Settings

The build.settings file is responsible for the build time properties of the project. Open this file, remove its contents, and populate it with the following code snippet.


settings =
{
    orientation =
    {
        default ="portrait",
        supported =
        {
          "portrait"
        },
    },
}

In build.settings, we are setting the default orientation and restricting the application to only support a portrait orientation. You can learn which other settings you can include in build.settings by exploring the Corona Documentation.

3. Config.lua

The config.lua file handles the application's configuration. As we did with build.settings, open this file, remove its contents, and add the following code.


application =
{
    content =
    {
      width = 768,
      height = 1024,
      scale = "letterbox",
      fps = 30,
    }
}

This sets the default width and height of the screen, uses letterbox to scale the images, and sets the frames per second to 30.

You can learn what other properties you can set in config.lua by checking out the Corona Documentation.

4. Main.lua

The main.lua file is the file that the application loads first and uses to bootstrap the application. We will be using main.lua to set a few default settings for the application and the Storyboard module to load the first screen.

If you're not familiar with Corona's Storyboard module, then I recommend giving the Documentation a quick read. In short, Storyboard is the built-in solution to scene ("screens") creation and management in Corona. This library provides developers an easy way to create and transition between individual scenes.

5. Hide Status Bar

We don't want the status bar showing in our application. Add the following code snippet to main.lua to hide the status bar.


display.setStatusBar(display.HiddenStatusBar)

6. Set Default Anchor Points

To set the default anchor (registration points) add the following to main.lua.

display.setDefault( "anchorX", 0)
display.setDefault( "anchorY", 0)

The anchorX and anchorY properties specify where you want the registration point of your display objects to be. Note that the value ranges from 0.0 to 1.0. For example, if you'd want the registration point to be the center of the display object, then you'd set both properties to 0.5.

7. Seed Random Generator

Our game will be using Lua's math.random function to generate random numbers. To make sure that the numbers are truly random each time the application runs, you must provide a seed value. If you don't provide a seed value, the application will generate the same randomness every time.

A good seed value is Lua's os.time function since it will be different each time the application is run. Add the following code snippet to main.lua.

math.randomseed( os.time() ) 

8. Require Storyboard

Before we can use the Storyboard module, we must first require it. Add the following to main.lua.

local storyboard = require "storyboard"

9. Loading the Start Screen

Enter the following code snippet below the line in which you required the Storyboard module.

storyboard.gotoScene( "start" )

This will make the application go to the screen named start, which is also a Lua file, start.lua. You don't need to append the file extension when calling the gotoScene function.

10. Start Screen

Create a new Lua file named start.lua in the project's main directory. This will be a storyboard file, which means we need to require the Storyboard module and create a storyboard scene. Add the following snippet to start.lua.

local storyboard = require( "storyboard" )
local scene = storyboard.newScene()
The call to newScene makes start.lua part of the application's storyboard. This means that it becomes a screen within the game, which we can call storyboard methods on.

11. Local Variables

We only need one local variable, startbutton, in the main chunk of start.lua. This local variable is used to reference the start button in this scene.

local startbutton 

It's important to understand that local variables in the main chunk only get called once, when the scene is loaded for the first time. When navigating through the storyboard, for example, by invoking methods like gotoScence, the local variables will already be initialized.

This is important to remember if you want the local variables to be reinitialized when navigating back to a particular scene. The easiest way to do this is to remove the scene from the storyboard by calling the removeScence method. The next time you navigate to that scene, it will be automatically reloaded. That's the approach we'll be taking in this tutorial.

12. Storyboard Events

If you've taken the time to read the Documentation on storyboards I linked to earlier, you will have noticed the documentation includes a template that contains every possible storyboard event. The comments are very useful as they indicate which events to leverage for initializing assets, timers, etc.

The template can seem a little overwhelming at first. However, for this tutorial, we're only interested in three events, createSceneenterScene, and exitScene.

13. Create Scene

Add the following to snippet to start.lua.

function scene:createScene( event )
    local group = self.view
    local background = display.newRect( 0, 0, display.contentWidth, display.contentHeight)
    background:setFillColor( 0,.39,.75)
	group:insert(background)
	local bigplane = display.newImage("bigplane.png",0,0)
	group:insert(bigplane)
    startbutton= display.newImage("startbutton.png",264,670)
	group:insert(startbutton)
end

This method is called when the scene's view doesn't exist yet. This is where you should initialize the display objects and add them to the scene. The group variable is pointing to self.view, which is a GroupObject for the entire scene.

We then draw a rectangle for the background. Corona's Display object comes with some drawing methods, such as newCircle, newLine, and, as shown in the above snippet, newRect. We also invoke setFillColor to give the rectangle a blueish color. The parameters we pass are percentages.

We then insert the background into the group. It's important to insert display objects into the view's group to make sure they are removed when the main view is removed. Finally, create the start button and add it to the group as well. The display object has the newImage method, which takes as its parameters the path to the image and the x and y values for the image's position on-screen.

14. Enter Scene

The enterScene method is called immediately after the scene has moved on-screen. This is where you can add event listeners, start timers, load audio, etc. Add the following snippet below the code you added in the previous step.


function scene:enterScene( event )
    startbutton:addEventListener("tap",startGame)
end

In enterScene, we're adding an event listener to the startbutton, which will call the startGame function when the users taps the start button.

15. Exit Scene

The exitScene method is called when the scene is about to move off-screen. This is where you want to undo whatever you set up in the enterScene method, such as removing event listeners, stop timers, unload audio, etc. Add the following snippet below the code you added in the previous step.

function scene:exitScene( event )
    startbutton:removeEventListener("tap",startGame)
end

All we do in the exitScene method is removing the event listener we added to the startbutton.

16. Start Game

The startGame function is simple. It has the storyboard load the gamelevel scene.

function startGame()
    storyboard.gotoScene("gamelevel")
end

17. Adding Listeners to Scene

Having added the createScene, enterScene, and exitScene methods, we must register the scene to listen for these events. Enter the following code snippet to do this.

scene:addEventListener( "createScene", scene )
scene:addEventListener( "enterScene", scene )
scene:addEventListener( "exitScene", scene )

18. Return the Scene

The last thing you must make sure you do in a storyboard is returning the scene, because it's a module.

return scene

19. Adding the GameLevel scene

Before we can test our progress, we need to add the gamelevel scene. Create a new Lua file in the project's main directory and name it gamelevel.lua. Add the following code snippet to the file you've just created.

local storyboard = require( "storyboard" )
local scene = storyboard.newScene()

return scene

As we saw earlier, we first require the storyboard module, create a new scene, and return the scene. The gamelevel scene is the scene in which we'll implement the gameplay of the game.

Conclusion

This bring the first part of this series to a close. In the next part of this series, we'll begin implementing the game's gameplay. Thanks for reading and I will see you there.

2014-07-02T17:35:00.000Z2014-07-02T17:35:00.000ZJames Tynerhttp://code.tutsplus.com/tutorials/create-a-plane-fighting-game-in-corona-interface--cms-21223

Open Source 101: Licensing

$
0
0

There are a number of things to consider when it comes to licensing an open source project. Who can distribute it? Where can it be stored? Who owns the copyright? In this article, we'll take a look at the various types of licenses available for open source projects and how to choose one for a project.

As I mentioned in the first part of the series, one of the main reasons developers want to publish their work as an open source project is for other developers to be able to work on their projects, suggest and make changes, and help improve the project.

The only way that this is possible from a legal perspective is through the open source licensing system. In this article, we'll take a look at some popular open source licenses and what they entail.

1. GNU General Public License

The GNU license is the most popular open source license. The GNU (GPL) license is very clear about what is permitted and what's not.

  • The software can be copied onto your own servers, your client's server, or basically anywhere you want to place it. There are absolutely no limits on how many copies you can make, which allows for fast editing no matter where you are.
  • The software can be distributed however you want, through a download link on your website or on physical media like a flash drive. It can literally be distributed anywhere and any way you want.
  • The software can be modified by anybody without restrictions in terms of the changes they make. This also means that you can use the source code or part of it in another project as long as that project is also licensed under the GPL license.
  • The software can be distributed for a fee, however, the receiver must get a copy of the GPL license as well as the software, so that they too understand what they can and cannot do with the software.

2. MIT License

The MIT license is one of the shortest and most flexible licenses available for open source projects. The MIT license was created with flexibility in mind. In fact, the MIT license is only a handful of paragraphs long. The most important part of the MIT license is:

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

In layman's terms, you can do absolutely anything you want with the software, as long as the MIT license is included.

3. Apache License

Like the other licenses we've covered so far, the Apache license, version 2.0, grants a number of rights to its users. The key elements to remember are:

  • rights apply worldwide
  • rights are everlasting, in other words, there's no expiration date
  • rights are irrevocable, nobody can revoke them once they've been granted
  • rights are free, you won't ever be charged any kind of fee or royalty to use the license
  • rights are non-exclusive, you can use the licensed software, but so can anybody else

BSD License

The BSD licenses are a small group of permissive, free software licenses. In comparison to many of the other free licenses we've discussed so far, the BSD licenses have very few restrictions on how the source code can be distributed.

Out of all the BSD licenses, there are two that stand out, the New BSD License/Modified BSD License and the Simplified BSD License/FreeBSD License. The Open Source Initiative has approved these licenses and both have been verified as being GPL-compatible free software licenses.

Below is the main content of the New BSD License, also known as the BSD 3-Clause License.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

This means that as long as you include a copy of the license with any distributed forms of the source code and you don't use the copyright owner's name to show off your newer version of the project, you're good to go.

Creative Commons License

I feel that whilst the Creative Commons (CC) licenses aren't really open source licenses, they should be mentioned to explain one other licensing option that is commonly used for design projects.

There are four basic parts to a Creative Commons license. The license can be used in any combination you wish, including being used individually.

  1. Attribution: The attribution clause says that the original author must be credited and attributed as the creator of the work, though the work can be modified, distributed, copied, and otherwise used.
  2. Share Alike: This clause allows the work to be modified, distributed, and copied, providing it's done under the same Creative Commons license.
  3. Non-Commercial: The work can be modified and distributed, but not for commercial use.
  4. No Derivative Works: You distribute, copy, and use the work as is, but can't make any changes to it at all.

Conclusion

You should now have a better understanding of open source licenses and how you can decide which license to use for a particular project. If you have any questions, I'd be happy to answer them in the comments below.

2014-07-03T17:10:46.000Z2014-07-03T17:10:46.000ZSam Bersonhttp://code.tutsplus.com/tutorials/open-source-101-licensing--cms-21279

Core Data from Scratch: Relationships and More Fetching

$
0
0
tag:code.tutsplus.com,2005:PostPresenter/cms-21505

In the previous article, we learned about NSManagedObject and how easy it is to create, read, update, and delete records using Core Data. However, I didn't mention relationships in that discussion. Aside from a few caveats you need to be aware of, relationships are just as easy to manipulate as attributes. In this article, we will focus on relationships and we'll also continue our exploration of NSFetchRequest.

1. Relationships

We've already worked with relationships in the Core Data model editor and what I'm about to tell you will therefore sound familiar. Relationships are, just like attributes, accessed using key-value coding. Remember that the data model we created earlier in this series defines a Person entity and an Address entity. A person is linked to one or more addresses and an address is linked to one or more persons. This is a many-to-many relationship.

To fetch the addresses of a person, we simply invoke valueForKey: on the person, an instance of NSManagedObject, and pass in addresses as the key. Note that addresses is the key we defined in the data model. What type of object do you expect? Most people new to Core Data expect a sorted NSArray, but Core Data returns an NSSet, which is unsorted. Working with NSSet has its advantages as you'll learn later.

Creating Records

Enough with the theory, open the project from the previous article or clone it from GitHub. Let's start by creating a person and then link it to an address. To create a person, update the application:didFinishLaunchingWithOptions: method as shown below.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Initialize Window
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Configure Window
    [self.window setBackgroundColor:[UIColor whiteColor]];
    [self.window makeKeyAndVisible];
    // Create Person
    NSEntityDescription *entityPerson = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:self.managedObjectContext];
    NSManagedObject *newPerson = [[NSManagedObject alloc] initWithEntity:entityPerson insertIntoManagedObjectContext:self.managedObjectContext];
    // Set First and Lats Name
    [newPerson setValue:@"Bart" forKey:@"first"];
    [newPerson setValue:@"Jacobs" forKey:@"last"];
    [newPerson setValue:@44 forKey:@"age"];
    return YES;
}

This should look familiar if you've read the previous article. Creating an address looks similar as you can see below.

// Create Address
NSEntityDescription *entityAddress = [NSEntityDescription entityForName:@"Address" inManagedObjectContext:self.managedObjectContext];
NSManagedObject *newAddress = [[NSManagedObject alloc] initWithEntity:entityAddress insertIntoManagedObjectContext:self.managedObjectContext];

// Set First and Last Name
[newAddress setValue:@"Main Street" forKey:@"street"];
[newAddress setValue:@"Boston" forKey:@"city"];

Because every attribute of the Address entity is marked as optional, we don't need to assign a value to each attribute. In the above example, we only set the record's street and city attributes.

Creating a Relationship

To link the newAddress to the newPerson, we invoke setValue:forKey:, passing in addresses as the key. The value that we pass in is a NSSet that contains newAddress. Take a look at the following code block for clarification.

// Add Address to Person
[newPerson setValue:[NSSet setWithObject:newAddress] forKey:@"addresses"];

// Save Managed Object Context
NSError *error = nil;
if (![newPerson.managedObjectContext save:&error]) {
    NSLog(@"Unable to save managed object context.");
    NSLog(@"%@, %@", error, error.localizedDescription);
}

We call save: on the managed object context of the newPerson object to propagate the changes to the persistent store. Remember that calling save: on a managed object context saves the state of the managed object context. This means that newAddress is also written to the backing store as well as the relationships we just defined.

You may be wondering why we didn't link newPerson to newAddress, because we did define an inverse relationship in our data model. Core Data creates this relationship for us. If a relationship has an inverse relationship, then Core Data takes care of this automatically. You can verify this by asking the newAddress object for its persons.

Fetching and Updating a Relationship

Updating a relationship isn't difficult either. The only caveat is that we need to add or remove elements from the immutable NSSet instance Core Data hands to us. To make this task easier, however, NSManagedObject declares a convenience method mutableSetValueForKey:, which returns an NSMutableSet object. We can then simply add or remove an item from the collection to update the relationship.

Take a look at the following code block in which we create another address and associate it with newPerson. We do this by invoking mutableSetValueForKey: on newPerson and adding otherAddress to the mutable set. There is no need to tell Core Data that we've updated the relationship. Core Data keeps track of the mutable set that it gave us and updates the relationship accordingly.

// Create Address
NSManagedObject *otherAddress = [[NSManagedObject alloc] initWithEntity:entityAddress insertIntoManagedObjectContext:self.managedObjectContext];

// Set First and Last Name
[otherAddress setValue:@"5th Avenue" forKey:@"street"];
[otherAddress setValue:@"New York" forKey:@"city"];

// Add Address to Person
NSMutableSet *addresses = [newPerson mutableSetValueForKey:@"addresses"];
[addresses addObject:otherAddress];

Deleting a Relationship

Deleting a relationship is as simple as invoking setValue:forKey:, passing in nil as the value and the name of the relationship as the key. This unlinks every address from newPerson.

// Delete Relationship
[newPerson setValue:nil forKey:@"addresses"];

2. One-To-One and One-To-Many Relationships

One-To-One Relationships

Even though our data model doesn't define a one-to-one relationship, you've learned everything you need to know to work with this type of relationship. Working with a one-to-one relationship is identical to working with attributes. The only difference is that the value you get back from valueForKey: and the value you pass to setValue:forKey: is an NSManagedObject instance.

Let's update our data model to illustrate this. Open Core_Data.xcdatamodeld and select the Person entity. Create a new relationship and name it spouse. Set the Person entity as the destination and set the spouse relationship as the inverse relationship.

As you can see, it's perfectly possible to create a relationship in which the destination of the relationship is the same entity as the entity that defines the relationship. Also note that we always set the inverse of the relationship. As the documentation states, there are very few situations in which you would want to create a relationship that doesn't have an inverse relationship.

Do you know what will happen if you were to build and run the application? That's right, the application would crash. Because we changed the data model, the existing store, a SQLite database in this example, is no longer compatible with the data model. To remedy this, remove the application from your device or iOS Simulator and run the application. Don't worry though, we'll solve this problem more elegantly in a future installment using migrations.

If you can run the application without problems, then it's time for the next step. Head back to the application delegate class and add the following code block.

// Create Another Person
NSManagedObject *anotherPerson = [[NSManagedObject alloc] initWithEntity:entityPerson insertIntoManagedObjectContext:self.managedObjectContext];

// Set First and Last Name
[anotherPerson setValue:@"Jane" forKey:@"first"];
[anotherPerson setValue:@"Doe" forKey:@"last"];
[anotherPerson setValue:@42 forKey:@"age"];

To set anotherPerson as the spouse of newPerson, we invoke setValue:forKey: on newPerson and pass in anotherPerson and @"spouse" as the arguments. We can achieve the same result by invoking setValue:forKey: on anotherPerson and passing in newPerson and @"spouse" as the arguments.

// Create Relationship
[newPerson setValue:anotherPerson forKey:@"spouse"];

One-To-Many Relationships

Let's finish with a look at one-to-many relationships. Open Core_Data.xcdatamodeld, select the Person entity, and create a relationship named children. Set the destination to Person , set the type to To Many, and leave the inverse relationship empty for now.

Create another relationship named father, set the destination to Person, and set the inverse relationship to children. This will automatically populate the inverse relationship of the children relationship we left blank a moment ago. We've now created a one-to-many relationship, that is, a father can have many children, but a child can only have one father.

Head back to the application delegate and add the following code block. We create another Person record, set its attributes, and set it as a child of newPerson by asking Core Data for a mutable set for the key children and adding the new record to the mutable set.

// Create a Child Person
NSManagedObject *newChildPerson = [[NSManagedObject alloc] initWithEntity:entityPerson insertIntoManagedObjectContext:self.managedObjectContext];

// Set First and Last Name
[newChildPerson setValue:@"Jim" forKey:@"first"];
[newChildPerson setValue:@"Doe" forKey:@"last"];
[newChildPerson setValue:@21 forKey:@"age"];

// Create Relationship
NSMutableSet *children = [newPerson mutableSetValueForKey:@"children"];
[children addObject:newChildPerson];

The following code block accomplishes the same result by setting the father attribute of anotherChildPerson. The result is that newPerson becomes the father of anotherChildPerson and anotherChildPerson becomes a child of newPerson.

// Create Another Child Person
NSManagedObject *anotherChildPerson = [[NSManagedObject alloc] initWithEntity:entityPerson insertIntoManagedObjectContext:self.managedObjectContext];

// Set First and Last Name
[anotherChildPerson setValue:@"Lucy" forKey:@"first"];
[anotherChildPerson setValue:@"Doe" forKey:@"last"];
[anotherChildPerson setValue:@19 forKey:@"age"];

// Create Relationship
[anotherChildPerson setValue:newPerson forKeyPath:@"father"];

3. More Fetching

The data model of our sample application has grown quite a bit in terms of complexity. We've created one-to-one, one-to-many, and many-to-many relationships. We've seen how easy it is to create records, including relationships. However, if we also want to be able to pull that data from the persistent store, then we need to know more about fetching. Let's start with a simple example in which we see how to sort the results returned by a fetch request.

Sort Descriptors

To sort the records we get back from the managed object context, we use the NSSortDescriptor class. Take a look at the following code snippet.

// Fetching
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"Person"];

// Add Sort Descriptor
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"first" ascending:YES];
[fetchRequest setSortDescriptors:@[sortDescriptor]];

// Execute Fetch Request
NSError *fetchError = nil;
NSArray *result = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];

if (!fetchError) {
    for (NSManagedObject *managedObject in result) {
        NSLog(@"%@, %@", [managedObject valueForKey:@"first"], [managedObject valueForKey:@"last"]);
    }
    
} else {
    NSLog(@"Error fetching data.");
    NSLog(@"%@, %@", fetchError, fetchError.localizedDescription);
}

We initialize a fetch request by passing in the entity that we're interested in, Person. We then create an NSSortDescriptor object by invoking sortDescriptorWithKey:ascending:, passing in the attribute of the entity we'd like to sort by, first, and a boolean indicating whether the records need to be sorted in ascending or descending order.

We tie the sort descriptor to the fetch request by invoking setSortDescriptors: on the fetch request, passing in an array that includes the sort descriptor. Because setSortDescriptors: accepts an array, it is possible to pass in more than one sort descriptor. We'll take a look at this option in a moment.

The rest of the code block should look familiar. The fetch request is passed to the managed object context, which executes the fetch request when we invoke executeFetchRequest:error:. It's important to always pass in a pointer to an NSError object to know what went wrong if the execution of the fetch request fails.

Run the application and inspect the output in Xcode's console. The output should look similar to what is shown below. As you can see, the records are sorted by their first name.

Core Data[1080:613] Bart, Jacobs
Core Data[1080:613] Jane, Doe
Core Data[1080:613] Jim, Doe
Core Data[1080:613] Lucy, Doe

If you see duplicates in the output, then make sure to comment out the code we wrote earlier to create the records. Every time you run the application, the same records are created, resulting in duplicate records.

As I mentioned earlier, it's possible to combine multiple sort descriptors. Let's sort the records by their last name and age. We first set the key of the first sort descriptor to last. We then create another sort descriptor with a key of age and add it to the array of sort descriptors that we pass to setSortDescriptors:.

// Add Sort Descriptor
NSSortDescriptor *sortDescriptor1 = [NSSortDescriptor sortDescriptorWithKey:@"last" ascending:YES];
NSSortDescriptor *sortDescriptor2 = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES];
[fetchRequest setSortDescriptors:@[sortDescriptor1, sortDescriptor2]];

The output shows that the order of the sort descriptors in the array is important. The records are first sorted by their last name and then by their age.

Core Data[1418:613] Lucy, Doe (19)
Core Data[1418:613] Jim, Doe (21)
Core Data[1418:613] Jane, Doe (42)
Core Data[1418:613] Bart, Jacobs (44)

Predicates

Sort descriptors are great and easy to use, but predicates are what really makes fetching powerful in Core Data. While sort descriptors tell Core Data how the records need to be sorted, predicates tell it what records you're interested in. The class we'll be working with is NSPredicate.

Let's start by fetching every member of the Doe family. This is very easy to do and the syntax will remind some of you of SQL.

// Fetching
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"Person"];

// Create Predicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K == %@", @"last", @"Doe"];
[fetchRequest setPredicate:predicate];

// Add Sort Descriptor
NSSortDescriptor *sortDescriptor1 = [NSSortDescriptor sortDescriptorWithKey:@"last" ascending:YES];
NSSortDescriptor *sortDescriptor2 = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES];
[fetchRequest setSortDescriptors:@[sortDescriptor1, sortDescriptor2]];

// Execute Fetch Request
NSError *fetchError = nil;
NSArray *result = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];

if (!fetchError) {
    for (NSManagedObject *managedObject in result) {
        NSLog(@"%@, %@", [managedObject valueForKey:@"first"], [managedObject valueForKey:@"last"]);
    }
    
} else {
    NSLog(@"Error fetching data.");
    NSLog(@"%@, %@", fetchError, fetchError.localizedDescription);
}

We haven't changed much apart from creating an NSPredicate object by invoking predicateWithFormat: and tying the predicate to the fetch request by passing it as an argument of a setPredicate: call. The idea behind predicateWithFormat: is similar to stringWithFormat: in that it accepts a variable number of arguments.

Note that  the predicate format string uses %K for the property name and %@ for the value. As stated in the Predicate Programming Guide, %K is a variable argument substitution for a key path while %@ is a variable argument substitution for an object value. This means that the predicate format string of our example evaluates to last == "Doe".

If you run the application one more time and inspect the output in Xcode's console, you should see the following result:

Core Data[1582:613] Lucy, Doe (19)
Core Data[1582:613] Jim, Doe (21)
Core Data[1582:613] Jane, Doe (42)

There are many operators we can use for comparison. In addition to = and ==, which are identical as far as Core Data is concerned, there's also >= and =>, <= and =>, != and <>, and > and <. I encourage you to experiment with these operators to learn how they affect the results of the fetch request.

The following predicate illustrates how we can use the >= operator to only fetch Person records with an age attribute greater than 30.

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K >= %@", @"age", @(30)];

We also have operators for string comparison, CONTAINS, LIKE, MATCHES, BEGINSWITH, and ENDSWITH. Let's fetch every Person record whose name CONTAINS the letter j.

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K CONTAINS %@", @"first", @"j"];

If you run the application now, the array of results will be empty since the string comparison is case sensitive by default. We can change this by adding a modifier like so:

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K CONTAINS[c] %@", @"first", @"j"];

You can also create compound predicates using the keywords AND, OR, and NOT. In the following example, we fetch every person whose first name contains the letter j and is younger than 30.

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K CONTAINS[c] %@ AND %K < 30", @"first", @"j", @"age", @(30)];

Predicates also make it very easy to fetch records based on their relationship. In the following example, we fetch every person whose father's name is equal to Bart.

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K == %@", @"father.first", @"Bart"];

The above predicate works as expected, because %K is a variable argument substitution for a key path, not just a key.

What you need to remember is that predicates enable you to query the backing store without you knowing anything about the store. Even though the syntax of the predicate format string is reminiscent of SQL in some ways, it doesn't matter if the backing store is a SQLite database or an in-memory store. This is a very powerful concept that isn't unique to Core Data. Rails's Active Record is another fine example of this paradigm.

There is much more to predicates than what I've shown you in this article. If you'd like to learn more about predicates, I suggest you take a peak at Apple's Predicate Programming Guide. We'll also work more with predicates in the next few articles of this series.

Conclusion

We now have a good grasp of the basics of Core Data and it's time to start working with the framework by creating an application that leverages its power. In the next article, we meet another important class of the Core Data framework, NSFetchedResultsController. This class will help us manage a collection of records, but you'll learn that it does quite a bit more than that.

2014-07-07T16:45:39.000Z2014-07-07T16:45:39.000ZBart Jacobs

Create a Plane Fighting Game in Corona: Gameplay

$
0
0
Final product image
What You'll Be Creating

Introduction

In the first part of this series, we managed to get the start screen showing and were able to transition to the gamelevel screen. In this tutorial, we continue where we left of and start implementing the gameplay.

1. Local Variables

Open gamelevel.lua, the file we created in the first tutorial, and add the following below the line local scene = storyboard.newScene().

local playerSpeedY = 0
local playerSpeedX = 0
local playerMoveSpeed = 7
local playerWidth  = 60
local playerHeight = 48
local bulletWidth  = 8
local bulletHeight =  19
local islandHeight = 81
local islandWidth = 100
local numberofEnemysToGenerate = 0
local numberOfEnemysGenerated = 0
local playerBullets = {} -- Holds all the bullets the player fires
local enemyBullets = {} -- Hold the bullets from "all" enemy planes
local islands = {} --  Holds all the islands
local planeGrid = {} -- Holds 0 or 1 (11 of them for making a grid system)
local enemyPlanes = {}  -- Holds all of the enemy planes
local livesImages = {}  -- Holds all of the "free life" images
local numberOfLives = 3
local freeLifes = {} -- Holds all the ingame free lives
local playerIsInvincible = false
local gameOver = false
local numberOfTicks = 0 -- A number that is incremented each frame of the game
local islandGroup -- A group to hold all of the islands 
local planeGroup -- A group that holds all the planes, bullets, etc
local player
local  planeSoundChannel -- SoundChannel for the plane sound
local firePlayerBulletTimer
local generateIslandTimer
local fireEnemyBulletsTimer
local generateFreeLifeTimer
local rectUp -- The "up" control on the DPAD
local rectDown -- The "down" control on the DPAD
local rectLeft -- The "left" control on the DPAD
local rectRight -- The "right" control on the DPAD

Most of these are self-explanatory, but I've included comments for clarification. From here on out, all code should be inserted above the line return scene.

2. createScene

Start by adding the createScene function to main.lua. The createScene function is called when the scene's view doesn't yet exist. We'll add the game's display objects in this function.

function scene:createScene( event )
    local group = self.view
end
scene:addEventListener( "createScene", scene )

3. setupBackground

function setupBackground ()
    local background = display.newRect( 0, 0, display.contentWidth, display.contentHeight)
    background:setFillColor( 0,0,1)
    scene.view:insert(background)
end

In setupBackground, we create a blue background using the Display object's newRect method. The setFillColor method takes RGB values, as percentages. Invoke the setupBackground function in createScene as shown below.

function scene:createScene( event )
    local group = self.view
    setupBackground()
end

4. setupGroups

The setupGroups function instantiates the islandGroup and planeGroup groups, and inserts them into the scene's view. The GroupObject is a special type of display object into which you can add other display objects. It's important to first add the islandGroup to the view to make sure the islands are below the planes.

function setupGroups()
    islandGroup = display.newGroup()
    planeGroup = display.newGroup()
    scene.view:insert(islandGroup)
    scene.view:insert(planeGroup)
end

Invoke the setupGroups function in createScene as shown below.

function scene:createScene( event )
    local group = self.view
    setupBackground()
    setupGroups()
end

5. setupDisplay

The setupDisplay function draws a black rectangle at the bottom of the screen and inserts dpad and plane images into the view.

function setupDisplay ()
    local tempRect = display.newRect(0,display.contentHeight-70,display.contentWidth,124);
    tempRect:setFillColor(0,0,0);
    scene.view:insert(tempRect)
    local logo = display.newImage("logo.png",display.contentWidth-139,display.contentHeight-70);
    scene.view:insert(logo)
    local dpad = display.newImage("dpad.png",10,display.contentHeight - 70)
    scene.view:insert(dpad)
end

Again, invoke this function in createScene as shown below.

function scene:createScene( event )
    local group = self.view
    setupBackground()
    setupGroups()
    setupDisplay()
end

6. setupPlayer

The setupPlayer function simply adds the player image to the screen. The Display object comes with two read-only properties, contentWidth and contentHeight, representing the original width and height of the content in pixels. These values default to the screen width and height, but may have other values if you're using content scaling in config.lua. We use these properties to align the player in the  scene.

function setupPlayer()
    player = display.newImage("player.png",(display.contentWidth/2)-(playerWidth/2),(display.contentHeight - 70)-playerHeight)
    player.name = "Player"
    scene.view:insert(player)
end

Invoke the setupPlayer function in createScene.

function scene:createScene( event )
    local group = self.view
    setupBackground()
    setupGroups()
    setupDisplay()
    setupPlayer()
end

7. setupLivesImages

The setupLivesImages function sets up six life images and positions them at the top left of the screen. We then insert these images into the livesImages table, so that we're able to reference them later. Lastly, we make sure that only the first three images are visible.

function setupLivesImages()
for i = 1, 6 do
      local tempLifeImage = display.newImage("life.png",  40* i - 20, 10)
      table.insert(livesImages,tempLifeImage)
      scene.view:insert(tempLifeImage)
      if( i > 3) then
           tempLifeImage.isVisible = false;
      end
end
end

The setupLivesImages function is also invoked in the createScene function.

function scene:createScene( event )
        local group = self.view
         setupBackground()
         setupGroups()
         setupDisplay()
         setupPlayer()
         setupLivesImages()
end

8. setupDPad

The setupDPad function sets up the four rectangles rectUp, rectDown, rectLeft, and rectRight. We carefully position them on top of the dpad image, configure them to not be visible, and make sure the isHitTestable property is set to true.

If you set display objects to not be visible, you're initially unable to interact with them. However, by setting the isHitTestable property to true, this behavior is overridden.

function setupDPad()
    rectUp = display.newRect( 34, display.contentHeight-70, 23, 23)
    rectUp:setFillColor(1,0,0)
    rectUp.id ="up"
    rectUp.isVisible = false;
    rectUp.isHitTestable = true;
    scene.view:insert(rectUp)

    rectDown = display.newRect( 34,display.contentHeight-23, 23,23)
    rectDown:setFillColor(1,0,0)
    rectDown.id ="down"
    rectDown.isVisible = false;
    rectDown.isHitTestable = true;
    scene.view:insert(rectDown)

    rectLeft = display.newRect( 10,display.contentHeight-47,23, 23)
    rectLeft:setFillColor(1,0,0)
    rectLeft.id ="left"
    rectLeft.isVisible = false;
    rectLeft.isHitTestable = true;
    scene.view:insert(rectLeft)

    rectRight= display.newRect( 58,display.contentHeight-47, 23,23)
    rectRight:setFillColor(1,0,0)
    rectRight.id ="right"
    rectRight.isVisible = false;
    rectRight.isHitTestable = true;
    scene.view:insert(rectRight)
end

You've guessed it. This function is also invoked in createScene.

function scene:createScene( event )
    local group = self.view
    setupBackground()
    setupGroups()
    setupDisplay()
    setupPlayer()
    setupLivesImages()
    setupDPad()
end

9. resetPlaneGrid

The resetPlaneGrid function resets the planeGrid table and inserts eleven zeros. The planeGrid table imitates eleven spots across the x axis, in which an enemy plane can be positioned. This will make more sense once we start generating enemy planes.

function resetPlaneGrid()
planeGrid = {}
     for i=1, 11 do
         table.insert(planeGrid,0)
     end
end

Invoke this function in createScene.

function scene:createScene( event )
    local group = self.view
    setupBackground()
    setupGroups()
    setupDisplay()
    setupPlayer()
    setupLivesImages()
    setupDPad()
    resetPlaneGrid()
end

10. enterScene

Now that all the display objects are in place, it's time to add event listeners, timers, etc. If you recall from the previous part of this tutorial, the enterScene function is a good place to set these up. Start by inserting the following code snippet.

function scene:enterScene( event )
    local group = self.view
end
scene:addEventListener( "enterScene", scene )

11. Removing the Previous Storyboard

When we enter this scene, we need to remove the previous scene. Add the following code to the enterScene function to do this.

local previousScene = storyboard.getPrevious()
storyboard.removeScene(previousScene)
When you enter a new scene, the previous scene you were on can be referenced by calling getPrevious on the storyboard object. We remove it completely from the storyboard by calling removeScene on the storyboard object.

12. Add Event Listeners to Dpad Rectangles

Add the following code below the code you entered in the previous step. This code snippet adds touch listeners to each of the rectangles, invoking movePlane with every touch. Let's take a look at this movePlane function in the next step.

rectUp:addEventListener( "touch", movePlane)
rectDown:addEventListener( "touch", movePlane)
rectLeft:addEventListener( "touch", movePlane)
rectRight:addEventListener( "touch", movePlane)

13.movePlane

The movePlane function is responsible for setting the planes speed. We check if the touch event's phase is equal to began, which means the player has touched down but not lifted their finger back up. If this is true, we set the speed and direction according to which rectangle was touched. If the touch event's phase is equal to ended, then we know the player has lifted their finger, which means we set the speed to 0.

function movePlane(event)
    if event.phase == "began" then
        if(event.target.id == "up") then
          playerSpeedY = -playerMoveSpeed
        end
        if(event.target.id == "down") then
          playerSpeedY = playerMoveSpeed
        end
        if(event.target.id == "left") then
          playerSpeedX = -playerMoveSpeed
        end
        if(event.target.id == "right") then
          playerSpeedX = playerMoveSpeed
        end
    elseif event.phase == "ended" then
        playerSpeedX = 0
        playerSpeedY = 0 
   end
end

14. PlaneSound

Let's add some sound to our game. Add the following code snippet to the enterScene function. It loads and plays planesound.mp3. By setting the loops property to -1, the sound will loop forever. If you want to learn more about audio in Corona, be sure to check out the documentation.

local planeSound = audio.loadStream("planesound.mp3")
planeSoundChannel = audio.play( planeSound, {loops=-1} )

15. enterFrame Event

We also add a runtime event listener named enterFrame that will call thegameLoop function. The frequency with which the enterFrame event occurs depends on the frames per second (FPS) value you set in config.lua. In our example, it will be called 30 times per second. Add this event listener in the enterScene function.

 Runtime:addEventListener("enterFrame", gameLoop)

16. gameLoop

In the gameLoop function we update the sprite positions and perform any other logic that needs to take place every frame. If you are interested in reading more on the topic of game loops, Michael James Williams wrote a great article that explains how a common game loop works. Add the following code snippet.


function gameLoop()
    movePlayer()
end

17.movePlayer

The movePlayer function manages the moving of the player's plane. We move the plane according to the playerSpeedX and playerSpeedY values, which will either be 7 or 0, depending on whether the player is touching on the DPad or not. Refer back to the movePlane function if this is unclear. We also do some bounds checking, making sure the plane cannot move off-screen.

function movePlayer()
    player.x = player.x + playerSpeedX
    player.y = player.y + playerSpeedY
    if(player.x < 0) then
        player.x = 0
   end
   if(player.x > display.contentWidth - playerWidth) then
       player.x = display.contentWidth - playerWidth
   end
   if(player.y   < 0) then
       player.y = 0
  end
  if(player.y > display.contentHeight - 70- playerHeight) then
      player.y = display.contentHeight - 70 - playerHeight
  end
end

If you test the game now, you should be able to navigate the plane around the screen using the DPad.

Conclusion

This brings the second tutorial of this series to a close. In the next installment of this series, we will continue with the gameplay. Thanks for reading and see you there.

2014-07-09T16:45:28.000Z2014-07-09T16:45:28.000ZJames Tyner

Create a Plane Fighting Game in Corona: Gameplay

$
0
0
tag:code.tutsplus.com,2005:PostPresenter/cms-21313
Final product image
What You'll Be Creating

Introduction

In the first part of this series, we managed to get the start screen showing and were able to transition to the gamelevel screen. In this tutorial, we continue where we left of and start implementing the gameplay.

1. Local Variables

Open gamelevel.lua, the file we created in the first tutorial, and add the following below the line local scene = storyboard.newScene().

local playerSpeedY = 0
local playerSpeedX = 0
local playerMoveSpeed = 7
local playerWidth  = 60
local playerHeight = 48
local bulletWidth  = 8
local bulletHeight =  19
local islandHeight = 81
local islandWidth = 100
local numberofEnemysToGenerate = 0
local numberOfEnemysGenerated = 0
local playerBullets = {} -- Holds all the bullets the player fires
local enemyBullets = {} -- Hold the bullets from "all" enemy planes
local islands = {} --  Holds all the islands
local planeGrid = {} -- Holds 0 or 1 (11 of them for making a grid system)
local enemyPlanes = {}  -- Holds all of the enemy planes
local livesImages = {}  -- Holds all of the "free life" images
local numberOfLives = 3
local freeLifes = {} -- Holds all the ingame free lives
local playerIsInvincible = false
local gameOver = false
local numberOfTicks = 0 -- A number that is incremented each frame of the game
local islandGroup -- A group to hold all of the islands 
local planeGroup -- A group that holds all the planes, bullets, etc
local player
local  planeSoundChannel -- SoundChannel for the plane sound
local firePlayerBulletTimer
local generateIslandTimer
local fireEnemyBulletsTimer
local generateFreeLifeTimer
local rectUp -- The "up" control on the DPAD
local rectDown -- The "down" control on the DPAD
local rectLeft -- The "left" control on the DPAD
local rectRight -- The "right" control on the DPAD

Most of these are self-explanatory, but I've included comments for clarification. From here on out, all code should be inserted above the line return scene.

2. createScene

Start by adding the createScene function to main.lua. The createScene function is called when the scene's view doesn't yet exist. We'll add the game's display objects in this function.

function scene:createScene( event )
    local group = self.view
end
scene:addEventListener( "createScene", scene )

3. setupBackground

function setupBackground ()
    local background = display.newRect( 0, 0, display.contentWidth, display.contentHeight)
    background:setFillColor( 0,0,1)
    scene.view:insert(background)
end

In setupBackground, we create a blue background using the Display object's newRect method. The setFillColor method takes RGB values, as percentages. Invoke the setupBackground function in createScene as shown below.

function scene:createScene( event )
    local group = self.view
    setupBackground()
end

4. setupGroups

The setupGroups function instantiates the islandGroup and planeGroup groups, and inserts them into the scene's view. The GroupObject is a special type of display object into which you can add other display objects. It's important to first add the islandGroup to the view to make sure the islands are below the planes.

function setupGroups()
    islandGroup = display.newGroup()
    planeGroup = display.newGroup()
    scene.view:insert(islandGroup)
    scene.view:insert(planeGroup)
end

Invoke the setupGroups function in createScene as shown below.

function scene:createScene( event )
    local group = self.view
    setupBackground()
    setupGroups()
end

5. setupDisplay

The setupDisplay function draws a black rectangle at the bottom of the screen and inserts dpad and plane images into the view.

function setupDisplay ()
    local tempRect = display.newRect(0,display.contentHeight-70,display.contentWidth,124);
    tempRect:setFillColor(0,0,0);
    scene.view:insert(tempRect)
    local logo = display.newImage("logo.png",display.contentWidth-139,display.contentHeight-70);
    scene.view:insert(logo)
    local dpad = display.newImage("dpad.png",10,display.contentHeight - 70)
    scene.view:insert(dpad)
end

Again, invoke this function in createScene as shown below.

function scene:createScene( event )
    local group = self.view
    setupBackground()
    setupGroups()
    setupDisplay()
end

6. setupPlayer

The setupPlayer function simply adds the player image to the screen. The Display object comes with two read-only properties, contentWidth and contentHeight, representing the original width and height of the content in pixels. These values default to the screen width and height, but may have other values if you're using content scaling in config.lua. We use these properties to align the player in the  scene.

function setupPlayer()
    player = display.newImage("player.png",(display.contentWidth/2)-(playerWidth/2),(display.contentHeight - 70)-playerHeight)
    player.name = "Player"
    scene.view:insert(player)
end

Invoke the setupPlayer function in createScene.

function scene:createScene( event )
    local group = self.view
    setupBackground()
    setupGroups()
    setupDisplay()
    setupPlayer()
end

7. setupLivesImages

The setupLivesImages function sets up six life images and positions them at the top left of the screen. We then insert these images into the livesImages table, so that we're able to reference them later. Lastly, we make sure that only the first three images are visible.

function setupLivesImages()
for i = 1, 6 do
      local tempLifeImage = display.newImage("life.png",  40* i - 20, 10)
      table.insert(livesImages,tempLifeImage)
      scene.view:insert(tempLifeImage)
      if( i > 3) then
           tempLifeImage.isVisible = false;
      end
end
end

The setupLivesImages function is also invoked in the createScene function.

function scene:createScene( event )
        local group = self.view
         setupBackground()
         setupGroups()
         setupDisplay()
         setupPlayer()
         setupLivesImages()
end

8. setupDPad

The setupDPad function sets up the four rectangles rectUp, rectDown, rectLeft, and rectRight. We carefully position them on top of the dpad image, configure them to not be visible, and make sure the isHitTestable property is set to true.

If you set display objects to not be visible, you're initially unable to interact with them. However, by setting the isHitTestable property to true, this behavior is overridden.

function setupDPad()
    rectUp = display.newRect( 34, display.contentHeight-70, 23, 23)
    rectUp:setFillColor(1,0,0)
    rectUp.id ="up"
    rectUp.isVisible = false;
    rectUp.isHitTestable = true;
    scene.view:insert(rectUp)

    rectDown = display.newRect( 34,display.contentHeight-23, 23,23)
    rectDown:setFillColor(1,0,0)
    rectDown.id ="down"
    rectDown.isVisible = false;
    rectDown.isHitTestable = true;
    scene.view:insert(rectDown)

    rectLeft = display.newRect( 10,display.contentHeight-47,23, 23)
    rectLeft:setFillColor(1,0,0)
    rectLeft.id ="left"
    rectLeft.isVisible = false;
    rectLeft.isHitTestable = true;
    scene.view:insert(rectLeft)

    rectRight= display.newRect( 58,display.contentHeight-47, 23,23)
    rectRight:setFillColor(1,0,0)
    rectRight.id ="right"
    rectRight.isVisible = false;
    rectRight.isHitTestable = true;
    scene.view:insert(rectRight)
end

You've guessed it. This function is also invoked in createScene.

function scene:createScene( event )
    local group = self.view
    setupBackground()
    setupGroups()
    setupDisplay()
    setupPlayer()
    setupLivesImages()
    setupDPad()
end

9. resetPlaneGrid

The resetPlaneGrid function resets the planeGrid table and inserts eleven zeros. The planeGrid table imitates eleven spots across the x axis, in which an enemy plane can be positioned. This will make more sense once we start generating enemy planes.

function resetPlaneGrid()
planeGrid = {}
     for i=1, 11 do
         table.insert(planeGrid,0)
     end
end

Invoke this function in createScene.

function scene:createScene( event )
    local group = self.view
    setupBackground()
    setupGroups()
    setupDisplay()
    setupPlayer()
    setupLivesImages()
    setupDPad()
    resetPlaneGrid()
end

10. enterScene

Now that all the display objects are in place, it's time to add event listeners, timers, etc. If you recall from the previous part of this tutorial, the enterScene function is a good place to set these up. Start by inserting the following code snippet.

function scene:enterScene( event )
    local group = self.view
end
scene:addEventListener( "enterScene", scene )

11. Removing the Previous Storyboard

When we enter this scene, we need to remove the previous scene. Add the following code to the enterScene function to do this.

local previousScene = storyboard.getPrevious()
storyboard.removeScene(previousScene)
When you enter a new scene, the previous scene you were on can be referenced by calling getPrevious on the storyboard object. We remove it completely from the storyboard by calling removeScene on the storyboard object.

12. Add Event Listeners to Dpad Rectangles

Add the following code below the code you entered in the previous step. This code snippet adds touch listeners to each of the rectangles, invoking movePlane with every touch. Let's take a look at this movePlane function in the next step.

rectUp:addEventListener( "touch", movePlane)
rectDown:addEventListener( "touch", movePlane)
rectLeft:addEventListener( "touch", movePlane)
rectRight:addEventListener( "touch", movePlane)

13.movePlane

The movePlane function is responsible for setting the planes speed. We check if the touch event's phase is equal to began, which means the player has touched down but not lifted their finger back up. If this is true, we set the speed and direction according to which rectangle was touched. If the touch event's phase is equal to ended, then we know the player has lifted their finger, which means we set the speed to 0.

function movePlane(event)
    if event.phase == "began" then
        if(event.target.id == "up") then
          playerSpeedY = -playerMoveSpeed
        end
        if(event.target.id == "down") then
          playerSpeedY = playerMoveSpeed
        end
        if(event.target.id == "left") then
          playerSpeedX = -playerMoveSpeed
        end
        if(event.target.id == "right") then
          playerSpeedX = playerMoveSpeed
        end
    elseif event.phase == "ended" then
        playerSpeedX = 0
        playerSpeedY = 0 
   end
end

14. PlaneSound

Let's add some sound to our game. Add the following code snippet to the enterScene function. It loads and plays planesound.mp3. By setting the loops property to -1, the sound will loop forever. If you want to learn more about audio in Corona, be sure to check out the documentation.

local planeSound = audio.loadStream("planesound.mp3")
planeSoundChannel = audio.play( planeSound, {loops=-1} )

15. enterFrame Event

We also add a runtime event listener named enterFrame that will call thegameLoop function. The frequency with which the enterFrame event occurs depends on the frames per second (FPS) value you set in config.lua. In our example, it will be called 30 times per second. Add this event listener in the enterScene function.

 Runtime:addEventListener("enterFrame", gameLoop)

16. gameLoop

In the gameLoop function we update the sprite positions and perform any other logic that needs to take place every frame. If you are interested in reading more on the topic of game loops, Michael James Williams wrote a great article that explains how a common game loop works. Add the following code snippet.


function gameLoop()
    movePlayer()
end

17.movePlayer

The movePlayer function manages the moving of the player's plane. We move the plane according to the playerSpeedX and playerSpeedY values, which will either be 7 or 0, depending on whether the player is touching on the DPad or not. Refer back to the movePlane function if this is unclear. We also do some bounds checking, making sure the plane cannot move off-screen.

function movePlayer()
    player.x = player.x + playerSpeedX
    player.y = player.y + playerSpeedY
    if(player.x < 0) then
        player.x = 0
   end
   if(player.x > display.contentWidth - playerWidth) then
       player.x = display.contentWidth - playerWidth
   end
   if(player.y   < 0) then
       player.y = 0
  end
  if(player.y > display.contentHeight - 70- playerHeight) then
      player.y = display.contentHeight - 70 - playerHeight
  end
end

If you test the game now, you should be able to navigate the plane around the screen using the DPad.

Conclusion

This brings the second tutorial of this series to a close. In the next installment of this series, we will continue with the gameplay. Thanks for reading and see you there.

2014-07-09T16:45:28.000Z2014-07-09T16:45:28.000ZJames Tyner

Integrating the Dolby Audio API with Marmalade

$
0
0
Final product image
What You'll Be Creating

Introduction

If you're developing a mobile app that has any kind of audio output, be it a game or any other kind of app, then you will want your audio to sound the best it possibly can on every device it is compatible with.

Some mobile devices support Dolby Digital Plus, in particular the Kindle Fire range but also a growing number of other Android devices. Dolby Digital Plus can dramatically improve the audio output of your app by applying filters that can boost certain parts of your audio output, for example music or voice. This may sound like a complicated thing to achieve, but luckily Dolby have provided an API that makes using this functionality incredibly simple.

In this tutorial, you will learn how you can develop an app using the Marmalade SDK that can take advantage of Dolby Digital Plus using the Dolby Audio API Marmalade extension. If you're only interested in integrating the Dolby Audio API into your Marmalade application, then head to the bottom of this article.

1. Overview

The app we'll be creating in this tutorial will provide a simple user interface containing a top row of buttons that can start and stop different kinds of audio, and a bottom row of buttons to set the type of audio filtering that needs to be applied.

We'll first create a new Marmalade project, and the folders and source files that will make up the project.

Next I'll explain how to implement the user interface by loading an image containing the required button images and drawing different parts of it on the screen. I will also illustrate how to use Marmalade to respond to user touches.

Once the user interface is up and running, I'll show you how to make Marmalade play two different types of audio, compressed audio, such as an MP3 file, and raw sound sample data.

Finally, I'll integrate the Dolby Audio API into the app and make use of the different audio filter types it provides.

Throughout this tutorial, I will assume you are developing on a machine running Windows and already have the Marmalade SDK and a version of Microsoft Visual Studio installed. Marmalade can also be used with Xcode on OS X and you should still find it easy to follow the steps of this tutorial if you are using a Mac for development.

2. Setting Up the Marmalade Project

Step 1: Creating the Source Files and Folder Structure

Create a top level folder called DolbyTestApp to contain the project files. Within this folder, create another folder called source. Let's also create the files we'll be needing in our project. In the source folder, create five empty files called button.cpp, button.h, main.cpp, sound.cpp, and sound.h.

Step 2: Creating the MKB File

Create an empty file called DolbyTestApp.mkb in the DolbyTestApp folder and open it in a code editor. The MKB file is the file used by Marmalade to configure the project. It brings together all your source and data files, and allows you to configure things like the icons used by your app when installed on the different platforms supported by Marmalade. Add the following to the DolbyTestApp.mkb file.

{
    [Source]
    (source)
    button.cpp
    button.h
    main.cpp
    sound.cpp
    sound.h
}

subprojects
{
    iwgeom
    iwgx
}

The files section of an MKB file is used to list all the source files needed by your project. It also allows you to apply arbitrary groupings to these files and reference files from more than one folder. Groups are defined using square brackets, so the line [Source] will create a group called Source. Using groups allows us to create organizational folders within the Visual Studio solution that we'll generate in the next step.

Rounded brackets are used to specify a folder in which Marmalade should look for source files. The line (source) instructs Marmalade to look in the source subfolder of our main project folder. The five source files for the project are listed after this line.

The subprojects section allows you to reference other source code modules that your project requires. This project will need the iwgeom and iwgx modules which are standard components provided by the Marmalade SDK.

Step 3: Creating a Visual Studio Project

You can now use the MKB file to create a Visual Studio solution that can be used to build and test the app. Double-click the MKB file in Windows File Explorer, which should automatically launch Visual Studio and open the solution.

If you take a look at the DolbyTestApp folder, you will see that a few new files have been created. There's a folder called build_dolbytestapp_vcxx, where the xx part of the folder name depends on the version of Visual Studio you're using. This folder is used by Marmalade to contain all files needed as part of the build process, including the Visual Studio solution file.

A data folder has also been created, which is where you should place any resources required by the app, such as graphics and audio files. Two files have also been automatically created for you in this folder:

  • app.icf: a configuration file that allows you to change app settings, such as the maximum amount of RAM the app can use
  • app.config.txt: used to define custom application specific parameters that can then be used in the app.icf file

For this tutorial you can safely ignore these files as no changes will need to be made to it.

3. Building the User Interface

Step 1: Implementing the Main Program Loop

Let's start writing the main program loop for our app. In Visual Studio open the Source folder in the Solution Explorer, double-click the main.cpp file, and add the following code snippet.

#include "IwGx.h"
#include "s3e.h"
#include "button.h"
#include "sound.h"

void Initialise()
{
}

void Terminate()
{
}

void Update()
{
}

void Render()
{
}

int main()
{
    Initialise();
    while (!s3eDeviceCheckQuitRequest())
    {
        Update();
        Render();
    }
    Terminate();
    return 0;
}

This code snippet starts by including two Marmalade header files. The IwGx.h file declares the functions and structures that make up Marmalade's IwGx API, which can be used for rendering both 2D and 3D graphics in the most efficient manner possible on the target device.

The s3e.h header file lets you access all the lower level functions that provide direct access to such things as device settings, touch screen input, sound support, and much more.

The main function is self-explanatory. It starts by calling Initialise, which will perform any setup required by the app and ends by calling Terminate, which will release any resources used by the app.

The main while loop starts after invoking Initialise. The exit condition is little more than a call to s3eDeviceCheckQuitRequest, which is a Marmalade function that checks to see if a request to close the app has been received, for example, when the user has closed the app or the operating system has requested it to shut down for some reason.

The main loop just calls the Update and Render functions continuously. The Update function will be used for things like detecting user input while Render is responsible for drawing the user interface.

The Initialise, Update, Render, and Terminate functions are all empty at the moment, but the app can be built and executed. If you want to give it a try in the Marmalade Windows Simulator, select the x86 Debug option from the Solution Configurations drop-down menu in the Visual Studio toolbar, press F7 to build the app, and F5 to execute it. You should see something similar to the screenshot below.

Step 2: Loading an Image

To display the user interface the app is going to need to have some images to render, so I'll now show you how to load a PNG image into memory and get it into a state where it can be rendered to the screen.

First, add the following line at the top of the main.cpp file, below the include statements at the top:

CIwTexture* gpTexture = NULL;

The CIwTexture class is used to represent a bitmapped image. In the above snippet, I'm declaring a global variable called gpTexture, which will be used to store a pointer to the image used in the app's user interface. The image file we'll be using is named ui.png.

The image can be loaded into memory and prepared for use by adding the following lines to the Initialise function.

// Initialise Marmalade modules
IwGxInit();

// Create a new CIwTexture and use it to load the GUI image file
gpTexture = new CIwTexture;
gpTexture->LoadFromFile("ui.png");
gpTexture->Upload();

The call to IwGxInit performs all the necessary initialization steps needed to allow the app to draw to the screen, which includes being able to load image files.

The gpTexture variable is used to store a pointer to a new instance of the CIwTexture class. A call to the LoadFromFile method with the file name of the image will load the PNG file into memory and convert it into a suitable format for rendering. The image file name is specified relative to the app's data folder, so you'll need to ensure the ui.png file is copied to this folder.

The call to the Upload method will upload the converted image data to the device's video memory, ready to be drawn on screen.

It's also important that we tidy up after ourselves, so when the app shuts down it should release any resources it may be using. Add the following to the Terminate function.

// Destroy texture instance
delete gpTexture;

// Terminate Marmalade modules
IwGxTerminate();

The above snippet first destroys the CIwTexture instance, representing the ui.png image, which will also release any hardware resources and memory used by the image. The call to IwGxTerminate releases any resources that were initially allocated by the call to IwGxInit in Initialise.

Step 3: Creating a Button Class

The user interface for this app is going to need some clickable buttons, so let's create a new class called Button that will implement this behavior. Open button.h file and enter the following code.

#ifndef BUTTON_H
#define BUTTON_H

#include "IwGx.h"

class Button
{
public:
    Button(CIwTexture* apTexture, int32 aX, int32 aY,
           int32 aWidth, int32 aHeight, int32 aU, int32 aV,
           int32 aUWidth, int32 aVWidth, bool aEnabled);
    ~Button();

    void Render();

private:
    CIwMaterial* mpMaterial;
    CIwSVec2 mTopLeft;
    CIwSVec2 mSize;
    CIwFVec2 mUV;
    CIwFVec2 mUVSize;
    bool mEnabled;
};

#endif

The constructor for this class is used to position and size the button on screen, and also to indicate which region of the source image should be displayed on the button. It also indicates whether this button should be enabled for user input.

The destructor will release any resources created by the constructor while the Render method will, unsurprisingly, draw the button on the screen.

Some more Marmalade classes are introduced for the member variables of the Button class. The CIwMaterial class is used by Marmalade to combine images with other rendering information, such as color data that might be required when rendering. The CIwSVec2 class is a two component vector where each component is a 16-bit signed integer. The CIwFVec2 class is another two component vector with each component being of type float.

Open button.cpp and add the following code snippet to implement the Button class.

#include "button.h"
#include "s3e.h"

Button::Button(CIwTexture* apTexture, int32 aX, int32 aY,
               int32 aWidth, int32 aHeight,
               int32 aU, int32 aV,
               int32 aUWidth, int32 aVHeight, bool aEnabled)
{
    mpMaterial = new CIwMaterial();
    mpMaterial->SetTexture(apTexture);

    float lTextureWidth = (float) apTexture->GetWidth();
    float lTextureHeight = (float) apTexture->GetHeight();

    mTopLeft.x = aX;
    mTopLeft.y = aY;
    mSize.x = aWidth;
    mSize.y = aHeight;
    mUV.x = (float) aU / lTextureWidth;
    mUV.y = (float) aV / lTextureHeight;
    mUVSize.x = (float) aUWidth / lTextureWidth;
    mUVSize.y = (float) aVHeight / lTextureHeight;

    mEnabled = aEnabled;
}

The constructor for the Button class starts by creating a new instance of CIwMaterial, which will be used to render the button image. Each Button instance has its own CIwMaterial instance as it makes it easier to change the button's color. Once the CIwMaterial instance is created, the CIwTexture instance passed into the constructor is set as its image.

The mTopLeft member variable is used to store the top left corner of the Button on the screen while mSize stores the width and height. These values are specified in pixels.

The mUV and mUVSize member variables store the top left corner and dimensions of the image region to be rendered. These are specified as a floating point fraction of the source image size, with (0, 0) being the top left corner of the image and (1, 1) being the bottom right corner.

The values passed into the constructor are specified as pixel offsets into the texture, so you need to convert these into fractional values by dividing by the pixel width or height of the source image. It is possible to find the image dimensions by calling the GetWidth and GetHeight methods on the CIwTexture class.

The next code snippet shows the class's destructor. As you can see, all it has to do is delete the CIwMaterial instance that was allocated in the constructor.

Button::~Button()
{
    delete mpMaterial;
}

The Render method will draw the Button on screen. It starts by checking the mEnabled member variable and sets the ambient color of the CIwMaterial, so that the Button is drawn at full brightness when enabled and a bit darker when disabled.  A call to IwGxSetMaterial tells Marmalade which CIwMaterial instance to draw with and IwGxDrawRectScreenSpace will cause the Button to be rendered.

void Button::Render()
{
    if (!mEnabled)
        mpMaterial->SetColAmbient(96, 96, 96, 255);
    else
        mpMaterial->SetColAmbient(255, 255, 255, 255);

    IwGxSetMaterial(mpMaterial);
    IwGxDrawRectScreenSpace(&mTopLeft, &mSize, &mUV, &mUVSize);
}

Step 4: Laying Out the User Interface

The user interface for the app is going to automatically adjust to the screen resolution of the device it is running on, but to make things a little simpler we're only going to support landscape orientation. The first step is to force landscape by entering the following into the app.icf file.

[S3E]
DispFixRot=LANDSCAPE
MemSize0=12000000

{OS=WINDOWS}
WinWidth=1280
WinHeight=800
{}

All settings in the app.icf file have a group associated with them. Square brackets are used to denote a group, so in this case the line [S3E] indicates the settings that follow are part of the S3E group, which is a group reserved by Marmalade for hardware related settings.

The DispFixRot setting will force the screen to always be in landscape orientation. The MemSize0 setting has also been added to increase the amount of RAM the app has available to it. When you add sound support later in this tutorial, the extra RAM will be needed to store the sound sample data.

The WinWidth and WinHeight settings are used to specify the dimensions of the windows used when running in the simulator. The {OS=WINDOWS} line ensures these settings are only used on the Windows simulator. The {} line disables this restriction so any settings following it become global settings again.

You can now start creating the elements of the user interface. Open the main.cpp file and start by adding the following snippet after the declaration of the gpTexture global variable.

enum ButtonIDs
{
    BUTTON_AUDIO_LABEL,
    BUTTON_AUDIO_OFF,
    BUTTON_AUDIO_MUSIC,
    BUTTON_AUDIO_SFX,
    BUTTON_AUDIO_SPEECH,
    BUTTON_FILTER_LABEL,
    BUTTON_FILTER_OFF,
    BUTTON_FILTER_MOVIE,
    BUTTON_FILTER_MUSIC,
    BUTTON_FILTER_GAME,
    BUTTON_FILTER_VOICE,
    BUTTON_COUNT
};

Button* gButton[BUTTON_COUNT];

bool gDolbySupported;

The ButtonIDs enumeration provides a convenient way of naming each of the user interface elements. The gButton array will store pointers to each of the Button instances in the user interface and the gDolbySupported boolean flag will be used to disable parts of the interface if the target device does not support the Dolby Audio API.

To create the required Button instances, add the following code to the end of the Initialise function.

// Check for Dolby Digital Plus support
gDolbySupported = false;

// Create our interface buttons
int32 lSize = IwGxGetScreenWidth() / 5;
int32 lGap = (int32) ((float) lSize * 0.1f);
lSize = (int32) ((float) lSize * 0.9f);

int32 lRowSize = IwGxGetScreenHeight() / 4;
int32 lTopRowX = (IwGxGetScreenWidth() - (4 * lSize) -
                 (3 * lGap)) / 2;
int32 lTopRowY = lRowSize - (lSize / 2);
int32 lBottomRowX = (IwGxGetScreenWidth() - (5 * lSize) -
                    (4 * lGap)) / 2;
int32 lBottomRowY = (3 * lRowSize) - (lSize / 2);
int32 lLabelWidth = (240 * lSize) / 160;
int32 lLabelHeight = (42 * lSize) / 160;
int32 lLabelX = (IwGxGetScreenWidth() - lLabelWidth) / 2;

gButton[BUTTON_AUDIO_LABEL] = new Button(gpTexture,
               lLabelX, lTopRowY - lLabelHeight - 10,
               lLabelWidth, lLabelHeight, 4, 408,
               240, 42, false);
gButton[BUTTON_AUDIO_OFF] = new Button(gpTexture,
               lTopRowX, lTopRowY, lSize, lSize, 347, 3,
               160, 160, true);
gButton[BUTTON_AUDIO_MUSIC] = new Button(gpTexture,
               lTopRowX + (lSize + lGap), lTopRowY,
               lSize, lSize, 175, 3, 160, 160, true);
gButton[BUTTON_AUDIO_SFX] = new Button(gpTexture,
               lTopRowX + (2 * (lSize + lGap)), lTopRowY,
               lSize, lSize, 2, 173, 160, 160, true);
gButton[BUTTON_AUDIO_SPEECH] = new Button(gpTexture,
               lTopRowX + (3 * (lSize + lGap)), lTopRowY,
               lSize, lSize, 174, 173, 160, 160, true);
gButton[BUTTON_FILTER_LABEL] = new Button(gpTexture,
               lLabelX, lBottomRowY - lLabelHeight - 10,
               lLabelWidth, lLabelHeight, 2, 353,
               240, 42, false);
gButton[BUTTON_FILTER_OFF] = new Button(gpTexture,
               lBottomRowX, lBottomRowY, lSize, lSize,
               347, 3, 160, 160, gDolbySupported);
gButton[BUTTON_FILTER_MOVIE] = new Button(gpTexture,
               lBottomRowX + (lSize + lGap), lBottomRowY,
               lSize, lSize, 2, 3, 160, 160, gDolbySupported);
gButton[BUTTON_FILTER_MUSIC] = new Button(gpTexture,
               lBottomRowX + (2 * (lSize + lGap)),
               lBottomRowY, lSize, lSize, 175, 3,
               160, 160, gDolbySupported);
gButton[BUTTON_FILTER_GAME] = new Button(gpTexture,
               lBottomRowX + (3 * (lSize + lGap)),
               lBottomRowY, lSize, lSize, 2, 173,
               160, 160, gDolbySupported);
gButton[BUTTON_FILTER_VOICE] = new Button(gpTexture,
               lBottomRowX + (4 * (lSize + lGap)), lBottomRowY,
               lSize, lSize, 174, 173,
               160, 160, gDolbySupported);

In this code block, we start by assuming the Dolby Audio API isn't supported by the user's device by setting gDolbySupported to false. Next, the IwGxGetScreenWidth and IwGxGetScreenHeight functions are used to infer the dimensions of the screen and suitable sizes and positions for the user interface elements are calculated. Finally, a number of Button instances are created, defining the user interface.

You may have noticed that the Button instances for controlling the current filter type use the gDolbySupported variable to indicate whether they should be enabled or not. I've cheated a little by using two disabled Button instances to draw some labels.

You've now created the user interface, but you should always ensure that you tidy up after yourself. Add the following code block at the start of the Terminate function to release the Button instances when the app shuts down.

// Destroy Button instances
for (uint32 i = 0; i < BUTTON_COUNT; i++)
{
    delete gButton[i];
}

If you were to run the app at this point, the user interface would be created and destroyed, but it still won't be rendered. You will need to add the following code snippet to the Render function before anything will be displayed on screen.

// Clear the screen to a pale blue
IwGxSetColClear(128, 224, 255, 0);
IwGxClear(IW_GX_COLOUR_BUFFER_F | IW_GX_DEPTH_BUFFER_F);

// Render the UI
for (uint32 i = 0; i < BUTTON_COUNT; i++)
{
    gButton[i]->Render();
}

// Finish rendering and display on screen
IwGxFlush();
IwGxSwapBuffers();

The above code snippet first clears the screen to light blue using calls to IwGxSetColClear and IwGxClear. Then, the user interface is drawn by calling the Render method on each of the Button instances. Finally, a call to IwGxFlush causes all render requests to be completed before IwGxSwapBuffers makes the user interface actually appear on screen.

If you build and run the app in the Marmalade Windows Simulator, you should see two rows of buttons, with the bottom row drawn darker as they are in a disabled state.


Step 5: Responding to User Input

Let's now make this app a little more interactive by tracking touch input from the user. To start, you'll need to add a new method to the Button class, so open button.h and add the following method prototypes.

void Update(uint32 aTouchState, int32 aX, int32 aY);
bool IsReleased();

You should also add the following additional private member variables to the Button class.

bool mPressed;
bool mDown;
bool mReleased;

Next, open button.cpp and add the following lines to the end of the class constructor to ensure the new member variables are initialized to sensible values.

mDown = false;
mPressed = false;
mReleased = false;

The next code block shows the implementations of the Update and IsReleased methods.

void Button::Update(uint32 aTouchState, int32 aX, int32 aY)
{
    if (!mEnabled)
        return;

    // Check if the touch position is within bounds of
    // this button
    aX -= mTopLeft.x;
    aY -= mTopLeft.y;

    bool lInBounds = (aX >= 0) && (aX < mSize.x) &&
                     (aY >= 0) && (aY < mSize.y);

    // Clear the released flag
    mReleased = false;

    // Check touch screen state
    if (aTouchState & S3E_POINTER_STATE_PRESSED)
    {
        // User has just touched the screen
        if (lInBounds)
        {
            mPressed = true;
            mDown = true;
        }
    }
    else if (aTouchState & S3E_POINTER_STATE_DOWN)
    {
        // If button has been pressed, check if user
        // is still touching inside it
        if (mPressed)
        {
            mDown = lInBounds;
        }
    }
    else if (aTouchState & S3E_POINTER_STATE_RELEASED)
    {
        // If user has released screen over a pressed
        // button, we set the release flag to true
        if (mPressed && mDown)
        {
            mReleased = true;
        }

        // Button is no longer pressed or down!
        mDown = false;
        mPressed = false;
    }
}

bool Button::IsReleased()
{
    return mReleased;
}

The Update method takes three parameters, the current touch status and the screen coordinates of the touch. You'll learn how to obtain this information shortly. The method first checks to see if the Button is disabled and immediately exits if it is. The screen coordinates passed to the method are then checked against the bounds of the Button to see if the Button is being touched.

The aTouchState parameter of the Update method is a bit mask comprised of three possible flags:

  • S3E_POINTER_STATE_PRESSED is set when the user has just touched the screen
  • S3E_POINTER_STATE_DOWN is set while the screen is being touched
  • S3E_POINTER_STATE_RELEASED is set when the user lifts its finger from the screen

The Update method uses the current value of aTouchState to update the internal member variables of the class accordingly.

The IsReleased method is trivial, it returns the current state of the mReleased variable.

We need to make one final change to the Button class. In the Render method, we draw the Button slightly darker while the user is pressing it. This visual feedback benefits the user experience of the application. Change the beginning of the Render method to the following:

if (!mEnabled)
    mpMaterial->SetColAmbient(96, 96, 96, 255);
else if (mDown)
    mpMaterial->SetColAmbient(192, 192, 192, 255);
else
    mpMaterial->SetColAmbient(255, 255, 255, 255);

With the Button class updated, you now just have to add some logic to detect touch input from the user. Open main.cpp again and add the following to the currently empty Update function:

// Allow device OS time to do its processing
s3eDeviceYield(0);

// Update pointer (actually touch screen!) inputs
s3ePointerUpdate();

// Read current touch screen inputs and use them to update Button states
uint32 lTouchState =
           s3ePointerGetState(S3E_POINTER_BUTTON_SELECT);
int32 x = s3ePointerGetX();
int32 y = s3ePointerGetY();
for (uint32 i = 0; i < BUTTON_COUNT; i++)
{
    gButton[i]->Update(lTouchState, x, y);
}

The call to s3eDeviceYield is vitally important in a Marmalade app as it allows the device's operating system time to handle any events, such as touch input, incoming calls, etc. The s3ePointerUpdate function takes a snapshot of the current touch screen status.

The current state of the first detected touch input is then found by calling s3ePointerGetState. It returns a value using the bit mask I described earlier. The s3ePointer functions are also used to detect mouse events on desktop operating systems. The value passed to s3ePointerGetState is S3E_POINTER_BUTTON_SELECT, which will return the status of the first detected touch event or the left mouse button, depending on the capabilities of the device the app is running on.

s3ePointerGetX and s3ePointerGetY return the screen coordinates of the touch. We then loop through the button instances and call Button::Update on each button, passing in the current touch screen status and touch coordinates.

The app is now capable of detecting user input and the Button instances will change color when they're pressed.

4. Adding Audio Playback

Step 1: Playing Compressed Audio Files

Playing back compressed audio files, such as MP3 files, is incredibly easy in Marmalade. In fact, it only takes a single line of code. Add the following code block to the end of the Update function in main.cpp.

// Check for button presses
if (gButton[BUTTON_AUDIO_MUSIC]->IsReleased())
{
    s3eAudioPlay("black-hole.mp3");
}

Whenever the user presses and releases the music note button on the top row of buttons, the app will call the s3eAudioPlay function, which will attempt to play the MP3 file called black-hole.mp3. This file must exist in the project data folder, so it can be located at runtime.

The black-hole.mp3 file was obtained from http://www.freesfx.co.uk and was composed by Craig Riley (SOCAN).

Step 2: Playing Uncompressed Sound Samples

Playing uncompressed sound samples is also possible in Marmalade. While it isn't quite as simple as playing a compressed sound file, it's more flexible as it allows you to play multiple sounds at a time, whereas most devices will only allow a single compressed audio track to be played back at any time.

Marmalade expects sound sample data to be in 16-bit signed PCM format and most audio editing software will allow you to save out files in this format using the WAV file format. However, Marmalade doesn't support the WAV file format directly, so for the purposes of this tutorial I have taken sound files saved in WAV format and removed the header from the file leaving only the sample data. For your own apps, you'd probably want to support WAV files directly, but that is beyond the scope of this tutorial.

I've added two sound files to the data folder called female-counting.raw and gun-battle.raw. The original WAV format files were obtained from http://soundbible.com and have been released under the Attribution 3.0 Creative Commons license.

In order to play a sampled sound effect, it is necessary to have the sound data in memory. I have created a Sound class that will take care of this for us. To implement this class, open up sound.h and add the following code block to it:

#ifndef SOUND_H
#define SOUND_H

#include "s3e.h"

class Sound
{
public:
    Sound(const char* apFileName);
    ~Sound();

    void Play();

private:
    int16* mpSoundData;
    uint32 mSamples;
};

#endif

Next, open sound.cpp and insert the following code block:

#include "sound.h"

Sound::Sound(const char* apFileName)
{
    // Attempt to open the sound effect file
    s3eFile* f = s3eFileOpen(apFileName, "rb");
    if (f)
    {
        // Seek to end of file to find its length
        s3eFileSeek(f, 0, S3E_FILESEEK_END);

        // Number of samples is file size divided by the
        // size of an int16
        mSamples = s3eFileTell(f) / sizeof(int16);
        s3eFileSeek(f, 0, S3E_FILESEEK_SET);

        // Allocate buffer for sound data
        mpSoundData = new int16[mSamples];

        // Read in sound data
        s3eFileRead(mpSoundData, sizeof(int16), mSamples, f);

        // Close the file
        s3eFileClose(f);
    }
    else
    {
        // File open failed, zero the member variables
        mpSoundData = NULL;
        mSamples = 0;
    }
}

Sound::~Sound()
{
    if (mpSoundData)
        delete[] mpSoundData;
}

void Sound::Play()
{
    if (mpSoundData)
    {
        int lChannel = s3eSoundGetFreeChannel();
        s3eSoundChannelPlay(lChannel, mpSoundData,
                            mSamples, 0, 0);
    }
}

The constructor takes the file name of a sound effect and finds the length of the file in bytes. An array of 16-bit signed integers is created, big enough to hold the entire sound, and the file is read into this buffer. The destructor deletes this buffer.

The Play method will actually start the sound sample playing. It does this by first asking Marmalade for a free sound channel with a call to s3eSoundGetFreeChannel.  The sound is then started on that channel by calling s3eSoundChannelPlay, passing in the channel number, start of the sound buffer, and the number of sound samples in the sound. The remaining two parameters indicate whether the sound sample should loop when it reaches the end and the offset into the sample data where subsequent loops should begin playing. By passing in zero for both of these parameters, the entire sound effect will loop continuously.

With the Sound class implemented, return to main.cpp and add some code to load and destroy the sound samples and to start the sounds playing when the user presses a button. Start by adding two new global variables after the declaration of the gpButton array.

Sound* gpGunBattleSound;
Sound* gpFemaleCountingSound;

Next, add the following to the end of the Initialise function. This code block will load the two sound files into memory and then set the default sound sample frequency to 44100Hz, which just so happens to be the frequency of both sounds used in the app.

// Load sound effects into memory
gpGunBattleSound = new Sound("gun-battle.raw");
gpFemaleCountingSound = new Sound("female-counting.raw");

// Configure default sample rate for s3eSound
s3eSoundSetInt(S3E_SOUND_DEFAULT_FREQ, 44100);

We also need to release the sound data on shut down. We do this by adding the following code block to the beginning of the Terminate function to destroy the Sound instances.

// Destroy sound effects
delete gpGunBattleSound;
delete gpFemaleCountingSound;

Finally, add the next code snippet to the end of the Update function, immediately after the end of the last if statement. This will start the sound effects playing in response to the user pressing the correct buttons.

else if (gButton[BUTTON_AUDIO_SFX]->IsReleased())
{
    if (gpGunBattleSound)
        gpGunBattleSound->Play();
}
else if (gButton[BUTTON_AUDIO_SPEECH]->IsReleased())
{
    if (gpFemaleCountingSound)
        gpFemaleCountingSound->Play();
}

Step 3: Stopping Audio Output

If you want to start a piece of audio playing, chances are you'll also eventually want to stop it playing. The code block below illustrates how to do this in Marmalade by having a button in the user interface stop all currently playing audio. Add the following block to the end of the if...else block at the very end of the Update function in main.cpp.

else if (gButton[BUTTON_AUDIO_OFF]->IsReleased())
{
    s3eAudioStop();
    s3eSoundStopAllChannels();
}

The call to s3eAudioStop will stop playback of any compressed audio track that is playing, while s3eSoundStopAllChannels will stop all uncompressed sampled sounds. It is possible to stop sampled sounds on a channel by channel basis, but for the purposes of this app it's fine to stop all channels that are currently active.

5. Integrating the Dolby Audio API

Step 1: Downloading the Dolby Audio API Marmalade Extension

Now that we have an app that can play some sounds, it's time to turn our attention to the Dolby Audio API. As you'll notice, adding support for the Dolby Audio API is incredibly simple and can be done in no more than five minutes.

You first need to obtain the Dolby Audio API Marmalade extension by visiting the Dolby Developer website. After creating a free developer account, you can download the Marmalade extension from the framework tab.

Step 2: Adding the Dolby Audio API to the Project

Extract the Dolby Audio API Marmalade extension archive to a folder on your development machine and find in the extracted Libraries folder for a folder named s3eDolbyAudio. Copy this folder and its contents into our project's DolbyTestApp folder, alongside the source and data folders.

To include the extension in your project, edit the DolbyTestApp.mkb file and add s3eDolbyAudio to the list of subprojects. If you then rebuild the project in Visual Studio, the MKB file will be reprocessed and the Dolby Audio API extension will be added to to the project.

Step 3: Initializing the Dolby Audio API

Before you can use the functionality of the Dolby Audio API, you must first check whether the device your app is running on supports Dolby Digital Plus. To implement the necessary checks, edit main.cpp and add the following #include at the top of the file.

#include "s3eDolbyAudio.h"

Next, declare a global variable named gDolbyInitialised after the declaration of gDolbySupported.

bool gDolbyInitialised;

To check whether Dolby Digital Plus is supported, you can add the following code block to the Initialise function, after the statement gDolbySupported = false;.

if (s3eDolbyAudioAvailable() == S3E_TRUE)
{
    if (s3eDolbyAudioSupported() == S3E_TRUE)
    {
        gDolbySupported = true;
    }
    s3eDeviceYield(0);
}

The first call to s3eDolbyAudioAvailable checks whether the Dolby Audio API extension is supported on the target platform. If the extension is available a call is made to s3eDolbyAudioSupported, which will return S3E_TRUE if the target device supports Dolby Digital Plus. If supported, the gDolbySupported flag is set true.

The call to s3eDeviceYield is to give the device time to perform background processing after doing the Dolby Digital Plus support test. Dolby recommend that you do not initialize Dolby Digital Plus immediately after checking whether it's supported, so the s3eDeviceYield call will help prevent issues during initialization.

At the end of the Initialise function, you can initialize Dolby Digital Plus by calling the s3eDolbyAudioInitialize function. Only if this function returns S3E_TRUE will the gDolbyInitialised flag be set true. The code you need to add is as follows:

// Initialise Dolby API
if (gDolbySupported)
{
    if (s3eDolbyAudioInitialize() == S3E_TRUE)
    {
        gDolbyInitialised = true;
    }
}

You should also shut down the Dolby Audio API when your program terminates, so add the following to the Terminate function, before the call to IwGxTerminate.

// Release resources used by Dolby API
if (gDolbySupported)
{
    s3eDolbyAudioRelease();
}

Step 4: Handling Suspending and Resuming

Since mobile phones and tablets are used for a plethora of tasks, it isn't uncommon for your app to be suspended, either because the user wants to perform a different task or an event such as an incoming call has occurred. Under these conditions the Dolby Audio API should be suspended so that it isn't active whilst your app is paused or running as a background task.

Marmalade allows us to set up some callback functions that will be triggered whenever the app loses or regains focus. To implement these, add the following code to the Initialise function immediately after the check for Dolby Digital Plus support.

// Initialise Pause/Resume callbacks
s3eDeviceRegister(S3E_DEVICE_PAUSE, AppSuspended, NULL);
s3eDeviceRegister(S3E_DEVICE_BACKGROUND, AppSuspended, NULL);
s3eDeviceRegister(S3E_DEVICE_UNPAUSE, AppResumed, NULL);
s3eDeviceRegister(S3E_DEVICE_FOREGROUND, AppResumed, NULL);

You should also remove these callback functions at shut down, so add the following lines to the Terminate function immediately after the delete gpTexture line.

// Disable Pause/Resume callbacks
s3eDeviceUnRegister(S3E_DEVICE_PAUSE, AppSuspended);
s3eDeviceUnRegister(S3E_DEVICE_BACKGROUND, AppSuspended);
s3eDeviceUnRegister(S3E_DEVICE_UNPAUSE, AppResumed);
s3eDeviceUnRegister(S3E_DEVICE_FOREGROUND, AppResumed);

Now you just need to implement the AppSuspended and AppResumed callback functions. Add this code after the declaration of the global variables at the top of main.cpp.

int32 AppSuspended(void* apSystemData, void* apUserData)
{
    if (gDolbyInitialised)
        s3eDolbyAudioSuspendSession();
    return 0;
}

int32 AppResumed(void* apSystemData, void* apUserData)
{
    if (gDolbyInitialised)
        s3eDolbyAudioRestartSession();
    return 0;
}

When the app is suspended or goes into background processing, the AppSuspended callback will be triggered, which calls s3eDolbyAudioSuspendSession if the gDolbyInitialised flag is true. When the app regains focus, AppResumed will be called, which invokes s3eDolbyAudioRestartSession if the Dolby Audio API has been initialized.

Step 5: Using the Dolby Audio API Filters

The final step to integrating the Dolby Audio API is to actually make use of the different audio filters it provides. There are four predefined filters available, which suit different types of audio output, movie, music, voice, and game.

Once the Dolby Audio API is active, pass S3E_TRUE to s3eDolbyAudioSetEnabled to ensure filtering support is switched on followed by a call to s3eDolbyAudioSetProfile. If you want to stop filtering you can do this with a call to s3eDolbyAudioSetEnabled, passing in S3E_FALSE.

Add the following code block to the end of the Update function to enable the bottom row of buttons to switch between the different filter types.

if (gButton[BUTTON_FILTER_OFF]->IsReleased())
{
    if (gDolbyInitialised)
    {
        s3eDolbyAudioSetEnabled(S3E_FALSE);
    }
}
else if (gButton[BUTTON_FILTER_MUSIC]->IsReleased())
{
    if (gDolbyInitialised)
    {
        s3eDolbyAudioSetEnabled(S3E_TRUE);
        s3eDolbyAudioSetProfile(MUSIC);
    }
}
else if (gButton[BUTTON_FILTER_MOVIE]->IsReleased())
{
    if (gDolbyInitialised)
    {
        s3eDolbyAudioSetEnabled(S3E_TRUE);
        s3eDolbyAudioSetProfile(MOVIE);
    }
}
else if (gButton[BUTTON_FILTER_GAME]->IsReleased())
{
    if (gDolbyInitialised)
    {
        s3eDolbyAudioSetEnabled(S3E_TRUE);
        s3eDolbyAudioSetProfile(GAME);
    }
}
else if (gButton[BUTTON_FILTER_VOICE]->IsReleased())
{
    if (gDolbyInitialised)
    {
        s3eDolbyAudioSetEnabled(S3E_TRUE);
        s3eDolbyAudioSetProfile(VOICE);
    }
}

6. Making a Device Build

Step 1: Including the Resource Files

Let's round off by getting the app running on a device. The first step is to ensure that the final deployment package contains all the necessary resource files. Edit DolbyTestApp.mkb and append the following code snippet to the end of the file.

assets
{
    [default]
    (data)
    black-hole.mp3
    female-counting.raw
    gun-battle.raw
    ui.png
}

deployments
{
    name="DolbyTestApp"
    caption="Dolby Test App"

    assets="default"
}

The assets section of an MKB file is used to list the resource files that need to be shipped with the executable in order for the app to run. The format is similar to the files section, using square brackets to make groupings of files and rounded brackets to indicate folder names where files can be found.

The deployments section lets you configure the final installation package. The name parameter lets us specify the file name that will be used for the package file and the caption parameter is used to declare the text that will appear beneath the app icon when it is installed on a device. The assets parameter references one of the groups defined in the assets section, so it is possible to switch between different asset sets should you need to, for example, if you want to create a full and lite version of your app.

Step 2: Making the Installation Package

To create an installation package for the device of your choosing, you must first compile the app source code for the type of processor used by the target device. In most cases, this will be an ARM chip, so you should select the GCC ARM Release option from the Solutions Configurations drop-down menu at the top of the Visual Studio toolbar.

Press F7 to build the app, followed by F5 to launch the Marmalade Deployment Tool.

The Marmalade Deployment Tool shows a wizard to create the installation packages. For example, if you want to create an Android build, which will also run on Kindle Fire devices, then you must first choose the build type, ARM GCC Release.

After clicking the Next Stage > button, you will be asked to choose which project configuration you wish to deploy. Check the Default configuration, which is the only configuration of our project, and click the Next Stage > button once more to see a list of platforms supported by your version of Marmalade. Tick the box next to Android and click the Next Stage > button again.

You can now choose what actions to take when creating the installation package. The drop-down menu lets you choose to just create the package, which means you'll need to manually install it on a device, create it and install it on a device connected via USB, or create, install, and run the app on a connected device. For the latter two options to work, you'll need the Android SDK installed on your development machine, because these options make use of the ADB tool, which is part of the Android SDK.

A discussion of how to set up an Android device for development use is beyond the scope of this tutorial, but you can read more about this topic on the Android Developer website.

Conclusion

As you can see, using the Dolby Audio API in a Marmalade app is a very simple process. Most of this tutorial was concerned with setting up the user interface or playing sounds rather than integrating the Dolby Audio API.

If you've written a game or any other kind of app in Marmalade that has rich audio output, then you really should consider adding support for Dolby Digital Plus to give your users the best possible audio experience.

2014-07-11T17:00:36.000Z2014-07-11T17:00:36.000ZSean Scaplehorn

Integrating the Dolby Audio API with Marmalade

$
0
0
tag:code.tutsplus.com,2005:PostPresenter/cms-21365
Final product image
What You'll Be Creating

Introduction

If you're developing a mobile app that has any kind of audio output, be it a game or any other kind of app, then you will want your audio to sound the best it possibly can on every device it is compatible with.

Some mobile devices support Dolby Digital Plus, in particular the Kindle Fire range but also a growing number of other Android devices. Dolby Digital Plus can dramatically improve the audio output of your app by applying filters that can boost certain parts of your audio output, for example music or voice. This may sound like a complicated thing to achieve, but luckily Dolby have provided an API that makes using this functionality incredibly simple.

In this tutorial, you will learn how you can develop an app using the Marmalade SDK that can take advantage of Dolby Digital Plus using the Dolby Audio API Marmalade extension. If you're only interested in integrating the Dolby Audio API into your Marmalade application, then head to the bottom of this article.

1. Overview

The app we'll be creating in this tutorial will provide a simple user interface containing a top row of buttons that can start and stop different kinds of audio, and a bottom row of buttons to set the type of audio filtering that needs to be applied.

We'll first create a new Marmalade project, and the folders and source files that will make up the project.

Next I'll explain how to implement the user interface by loading an image containing the required button images and drawing different parts of it on the screen. I will also illustrate how to use Marmalade to respond to user touches.

Once the user interface is up and running, I'll show you how to make Marmalade play two different types of audio, compressed audio, such as an MP3 file, and raw sound sample data.

Finally, I'll integrate the Dolby Audio API into the app and make use of the different audio filter types it provides.

Throughout this tutorial, I will assume you are developing on a machine running Windows and already have the Marmalade SDK and a version of Microsoft Visual Studio installed. Marmalade can also be used with Xcode on OS X and you should still find it easy to follow the steps of this tutorial if you are using a Mac for development.

2. Setting Up the Marmalade Project

Step 1: Creating the Source Files and Folder Structure

Create a top level folder called DolbyTestApp to contain the project files. Within this folder, create another folder called source. Let's also create the files we'll be needing in our project. In the source folder, create five empty files called button.cpp, button.h, main.cpp, sound.cpp, and sound.h.

Step 2: Creating the MKB File

Create an empty file called DolbyTestApp.mkb in the DolbyTestApp folder and open it in a code editor. The MKB file is the file used by Marmalade to configure the project. It brings together all your source and data files, and allows you to configure things like the icons used by your app when installed on the different platforms supported by Marmalade. Add the following to the DolbyTestApp.mkb file.

{
    [Source]
    (source)
    button.cpp
    button.h
    main.cpp
    sound.cpp
    sound.h
}

subprojects
{
    iwgeom
    iwgx
}

The files section of an MKB file is used to list all the source files needed by your project. It also allows you to apply arbitrary groupings to these files and reference files from more than one folder. Groups are defined using square brackets, so the line [Source] will create a group called Source. Using groups allows us to create organizational folders within the Visual Studio solution that we'll generate in the next step.

Rounded brackets are used to specify a folder in which Marmalade should look for source files. The line (source) instructs Marmalade to look in the source subfolder of our main project folder. The five source files for the project are listed after this line.

The subprojects section allows you to reference other source code modules that your project requires. This project will need the iwgeom and iwgx modules which are standard components provided by the Marmalade SDK.

Step 3: Creating a Visual Studio Project

You can now use the MKB file to create a Visual Studio solution that can be used to build and test the app. Double-click the MKB file in Windows File Explorer, which should automatically launch Visual Studio and open the solution.

If you take a look at the DolbyTestApp folder, you will see that a few new files have been created. There's a folder called build_dolbytestapp_vcxx, where the xx part of the folder name depends on the version of Visual Studio you're using. This folder is used by Marmalade to contain all files needed as part of the build process, including the Visual Studio solution file.

A data folder has also been created, which is where you should place any resources required by the app, such as graphics and audio files. Two files have also been automatically created for you in this folder:

  • app.icf: a configuration file that allows you to change app settings, such as the maximum amount of RAM the app can use
  • app.config.txt: used to define custom application specific parameters that can then be used in the app.icf file

For this tutorial you can safely ignore these files as no changes will need to be made to it.

3. Building the User Interface

Step 1: Implementing the Main Program Loop

Let's start writing the main program loop for our app. In Visual Studio open the Source folder in the Solution Explorer, double-click the main.cpp file, and add the following code snippet.

#include "IwGx.h"
#include "s3e.h"
#include "button.h"
#include "sound.h"

void Initialise()
{
}

void Terminate()
{
}

void Update()
{
}

void Render()
{
}

int main()
{
    Initialise();
    while (!s3eDeviceCheckQuitRequest())
    {
        Update();
        Render();
    }
    Terminate();
    return 0;
}

This code snippet starts by including two Marmalade header files. The IwGx.h file declares the functions and structures that make up Marmalade's IwGx API, which can be used for rendering both 2D and 3D graphics in the most efficient manner possible on the target device.

The s3e.h header file lets you access all the lower level functions that provide direct access to such things as device settings, touch screen input, sound support, and much more.

The main function is self-explanatory. It starts by calling Initialise, which will perform any setup required by the app and ends by calling Terminate, which will release any resources used by the app.

The main while loop starts after invoking Initialise. The exit condition is little more than a call to s3eDeviceCheckQuitRequest, which is a Marmalade function that checks to see if a request to close the app has been received, for example, when the user has closed the app or the operating system has requested it to shut down for some reason.

The main loop just calls the Update and Render functions continuously. The Update function will be used for things like detecting user input while Render is responsible for drawing the user interface.

The Initialise, Update, Render, and Terminate functions are all empty at the moment, but the app can be built and executed. If you want to give it a try in the Marmalade Windows Simulator, select the x86 Debug option from the Solution Configurations drop-down menu in the Visual Studio toolbar, press F7 to build the app, and F5 to execute it. You should see something similar to the screenshot below.

Step 2: Loading an Image

To display the user interface the app is going to need to have some images to render, so I'll now show you how to load a PNG image into memory and get it into a state where it can be rendered to the screen.

First, add the following line at the top of the main.cpp file, below the include statements at the top:

CIwTexture* gpTexture = NULL;

The CIwTexture class is used to represent a bitmapped image. In the above snippet, I'm declaring a global variable called gpTexture, which will be used to store a pointer to the image used in the app's user interface. The image file we'll be using is named ui.png.

The image can be loaded into memory and prepared for use by adding the following lines to the Initialise function.

// Initialise Marmalade modules
IwGxInit();

// Create a new CIwTexture and use it to load the GUI image file
gpTexture = new CIwTexture;
gpTexture->LoadFromFile("ui.png");
gpTexture->Upload();

The call to IwGxInit performs all the necessary initialization steps needed to allow the app to draw to the screen, which includes being able to load image files.

The gpTexture variable is used to store a pointer to a new instance of the CIwTexture class. A call to the LoadFromFile method with the file name of the image will load the PNG file into memory and convert it into a suitable format for rendering. The image file name is specified relative to the app's data folder, so you'll need to ensure the ui.png file is copied to this folder.

The call to the Upload method will upload the converted image data to the device's video memory, ready to be drawn on screen.

It's also important that we tidy up after ourselves, so when the app shuts down it should release any resources it may be using. Add the following to the Terminate function.

// Destroy texture instance
delete gpTexture;

// Terminate Marmalade modules
IwGxTerminate();

The above snippet first destroys the CIwTexture instance, representing the ui.png image, which will also release any hardware resources and memory used by the image. The call to IwGxTerminate releases any resources that were initially allocated by the call to IwGxInit in Initialise.

Step 3: Creating a Button Class

The user interface for this app is going to need some clickable buttons, so let's create a new class called Button that will implement this behavior. Open button.h file and enter the following code.

#ifndef BUTTON_H
#define BUTTON_H

#include "IwGx.h"

class Button
{
public:
    Button(CIwTexture* apTexture, int32 aX, int32 aY,
           int32 aWidth, int32 aHeight, int32 aU, int32 aV,
           int32 aUWidth, int32 aVWidth, bool aEnabled);
    ~Button();

    void Render();

private:
    CIwMaterial* mpMaterial;
    CIwSVec2 mTopLeft;
    CIwSVec2 mSize;
    CIwFVec2 mUV;
    CIwFVec2 mUVSize;
    bool mEnabled;
};

#endif

The constructor for this class is used to position and size the button on screen, and also to indicate which region of the source image should be displayed on the button. It also indicates whether this button should be enabled for user input.

The destructor will release any resources created by the constructor while the Render method will, unsurprisingly, draw the button on the screen.

Some more Marmalade classes are introduced for the member variables of the Button class. The CIwMaterial class is used by Marmalade to combine images with other rendering information, such as color data that might be required when rendering. The CIwSVec2 class is a two component vector where each component is a 16-bit signed integer. The CIwFVec2 class is another two component vector with each component being of type float.

Open button.cpp and add the following code snippet to implement the Button class.

#include "button.h"
#include "s3e.h"

Button::Button(CIwTexture* apTexture, int32 aX, int32 aY,
               int32 aWidth, int32 aHeight,
               int32 aU, int32 aV,
               int32 aUWidth, int32 aVHeight, bool aEnabled)
{
    mpMaterial = new CIwMaterial();
    mpMaterial->SetTexture(apTexture);

    float lTextureWidth = (float) apTexture->GetWidth();
    float lTextureHeight = (float) apTexture->GetHeight();

    mTopLeft.x = aX;
    mTopLeft.y = aY;
    mSize.x = aWidth;
    mSize.y = aHeight;
    mUV.x = (float) aU / lTextureWidth;
    mUV.y = (float) aV / lTextureHeight;
    mUVSize.x = (float) aUWidth / lTextureWidth;
    mUVSize.y = (float) aVHeight / lTextureHeight;

    mEnabled = aEnabled;
}

The constructor for the Button class starts by creating a new instance of CIwMaterial, which will be used to render the button image. Each Button instance has its own CIwMaterial instance as it makes it easier to change the button's color. Once the CIwMaterial instance is created, the CIwTexture instance passed into the constructor is set as its image.

The mTopLeft member variable is used to store the top left corner of the Button on the screen while mSize stores the width and height. These values are specified in pixels.

The mUV and mUVSize member variables store the top left corner and dimensions of the image region to be rendered. These are specified as a floating point fraction of the source image size, with (0, 0) being the top left corner of the image and (1, 1) being the bottom right corner.

The values passed into the constructor are specified as pixel offsets into the texture, so you need to convert these into fractional values by dividing by the pixel width or height of the source image. It is possible to find the image dimensions by calling the GetWidth and GetHeight methods on the CIwTexture class.

The next code snippet shows the class's destructor. As you can see, all it has to do is delete the CIwMaterial instance that was allocated in the constructor.

Button::~Button()
{
    delete mpMaterial;
}

The Render method will draw the Button on screen. It starts by checking the mEnabled member variable and sets the ambient color of the CIwMaterial, so that the Button is drawn at full brightness when enabled and a bit darker when disabled.  A call to IwGxSetMaterial tells Marmalade which CIwMaterial instance to draw with and IwGxDrawRectScreenSpace will cause the Button to be rendered.

void Button::Render()
{
    if (!mEnabled)
        mpMaterial->SetColAmbient(96, 96, 96, 255);
    else
        mpMaterial->SetColAmbient(255, 255, 255, 255);

    IwGxSetMaterial(mpMaterial);
    IwGxDrawRectScreenSpace(&mTopLeft, &mSize, &mUV, &mUVSize);
}

Step 4: Laying Out the User Interface

The user interface for the app is going to automatically adjust to the screen resolution of the device it is running on, but to make things a little simpler we're only going to support landscape orientation. The first step is to force landscape by entering the following into the app.icf file.

[S3E]
DispFixRot=LANDSCAPE
MemSize0=12000000

{OS=WINDOWS}
WinWidth=1280
WinHeight=800
{}

All settings in the app.icf file have a group associated with them. Square brackets are used to denote a group, so in this case the line [S3E] indicates the settings that follow are part of the S3E group, which is a group reserved by Marmalade for hardware related settings.

The DispFixRot setting will force the screen to always be in landscape orientation. The MemSize0 setting has also been added to increase the amount of RAM the app has available to it. When you add sound support later in this tutorial, the extra RAM will be needed to store the sound sample data.

The WinWidth and WinHeight settings are used to specify the dimensions of the windows used when running in the simulator. The {OS=WINDOWS} line ensures these settings are only used on the Windows simulator. The {} line disables this restriction so any settings following it become global settings again.

You can now start creating the elements of the user interface. Open the main.cpp file and start by adding the following snippet after the declaration of the gpTexture global variable.

enum ButtonIDs
{
    BUTTON_AUDIO_LABEL,
    BUTTON_AUDIO_OFF,
    BUTTON_AUDIO_MUSIC,
    BUTTON_AUDIO_SFX,
    BUTTON_AUDIO_SPEECH,
    BUTTON_FILTER_LABEL,
    BUTTON_FILTER_OFF,
    BUTTON_FILTER_MOVIE,
    BUTTON_FILTER_MUSIC,
    BUTTON_FILTER_GAME,
    BUTTON_FILTER_VOICE,
    BUTTON_COUNT
};

Button* gButton[BUTTON_COUNT];

bool gDolbySupported;

The ButtonIDs enumeration provides a convenient way of naming each of the user interface elements. The gButton array will store pointers to each of the Button instances in the user interface and the gDolbySupported boolean flag will be used to disable parts of the interface if the target device does not support the Dolby Audio API.

To create the required Button instances, add the following code to the end of the Initialise function.

// Check for Dolby Digital Plus support
gDolbySupported = false;

// Create our interface buttons
int32 lSize = IwGxGetScreenWidth() / 5;
int32 lGap = (int32) ((float) lSize * 0.1f);
lSize = (int32) ((float) lSize * 0.9f);

int32 lRowSize = IwGxGetScreenHeight() / 4;
int32 lTopRowX = (IwGxGetScreenWidth() - (4 * lSize) -
                 (3 * lGap)) / 2;
int32 lTopRowY = lRowSize - (lSize / 2);
int32 lBottomRowX = (IwGxGetScreenWidth() - (5 * lSize) -
                    (4 * lGap)) / 2;
int32 lBottomRowY = (3 * lRowSize) - (lSize / 2);
int32 lLabelWidth = (240 * lSize) / 160;
int32 lLabelHeight = (42 * lSize) / 160;
int32 lLabelX = (IwGxGetScreenWidth() - lLabelWidth) / 2;

gButton[BUTTON_AUDIO_LABEL] = new Button(gpTexture,
               lLabelX, lTopRowY - lLabelHeight - 10,
               lLabelWidth, lLabelHeight, 4, 408,
               240, 42, false);
gButton[BUTTON_AUDIO_OFF] = new Button(gpTexture,
               lTopRowX, lTopRowY, lSize, lSize, 347, 3,
               160, 160, true);
gButton[BUTTON_AUDIO_MUSIC] = new Button(gpTexture,
               lTopRowX + (lSize + lGap), lTopRowY,
               lSize, lSize, 175, 3, 160, 160, true);
gButton[BUTTON_AUDIO_SFX] = new Button(gpTexture,
               lTopRowX + (2 * (lSize + lGap)), lTopRowY,
               lSize, lSize, 2, 173, 160, 160, true);
gButton[BUTTON_AUDIO_SPEECH] = new Button(gpTexture,
               lTopRowX + (3 * (lSize + lGap)), lTopRowY,
               lSize, lSize, 174, 173, 160, 160, true);
gButton[BUTTON_FILTER_LABEL] = new Button(gpTexture,
               lLabelX, lBottomRowY - lLabelHeight - 10,
               lLabelWidth, lLabelHeight, 2, 353,
               240, 42, false);
gButton[BUTTON_FILTER_OFF] = new Button(gpTexture,
               lBottomRowX, lBottomRowY, lSize, lSize,
               347, 3, 160, 160, gDolbySupported);
gButton[BUTTON_FILTER_MOVIE] = new Button(gpTexture,
               lBottomRowX + (lSize + lGap), lBottomRowY,
               lSize, lSize, 2, 3, 160, 160, gDolbySupported);
gButton[BUTTON_FILTER_MUSIC] = new Button(gpTexture,
               lBottomRowX + (2 * (lSize + lGap)),
               lBottomRowY, lSize, lSize, 175, 3,
               160, 160, gDolbySupported);
gButton[BUTTON_FILTER_GAME] = new Button(gpTexture,
               lBottomRowX + (3 * (lSize + lGap)),
               lBottomRowY, lSize, lSize, 2, 173,
               160, 160, gDolbySupported);
gButton[BUTTON_FILTER_VOICE] = new Button(gpTexture,
               lBottomRowX + (4 * (lSize + lGap)), lBottomRowY,
               lSize, lSize, 174, 173,
               160, 160, gDolbySupported);

In this code block, we start by assuming the Dolby Audio API isn't supported by the user's device by setting gDolbySupported to false. Next, the IwGxGetScreenWidth and IwGxGetScreenHeight functions are used to infer the dimensions of the screen and suitable sizes and positions for the user interface elements are calculated. Finally, a number of Button instances are created, defining the user interface.

You may have noticed that the Button instances for controlling the current filter type use the gDolbySupported variable to indicate whether they should be enabled or not. I've cheated a little by using two disabled Button instances to draw some labels.

You've now created the user interface, but you should always ensure that you tidy up after yourself. Add the following code block at the start of the Terminate function to release the Button instances when the app shuts down.

// Destroy Button instances
for (uint32 i = 0; i < BUTTON_COUNT; i++)
{
    delete gButton[i];
}

If you were to run the app at this point, the user interface would be created and destroyed, but it still won't be rendered. You will need to add the following code snippet to the Render function before anything will be displayed on screen.

// Clear the screen to a pale blue
IwGxSetColClear(128, 224, 255, 0);
IwGxClear(IW_GX_COLOUR_BUFFER_F | IW_GX_DEPTH_BUFFER_F);

// Render the UI
for (uint32 i = 0; i < BUTTON_COUNT; i++)
{
    gButton[i]->Render();
}

// Finish rendering and display on screen
IwGxFlush();
IwGxSwapBuffers();

The above code snippet first clears the screen to light blue using calls to IwGxSetColClear and IwGxClear. Then, the user interface is drawn by calling the Render method on each of the Button instances. Finally, a call to IwGxFlush causes all render requests to be completed before IwGxSwapBuffers makes the user interface actually appear on screen.

If you build and run the app in the Marmalade Windows Simulator, you should see two rows of buttons, with the bottom row drawn darker as they are in a disabled state.


Step 5: Responding to User Input

Let's now make this app a little more interactive by tracking touch input from the user. To start, you'll need to add a new method to the Button class, so open button.h and add the following method prototypes.

void Update(uint32 aTouchState, int32 aX, int32 aY);
bool IsReleased();

You should also add the following additional private member variables to the Button class.

bool mPressed;
bool mDown;
bool mReleased;

Next, open button.cpp and add the following lines to the end of the class constructor to ensure the new member variables are initialized to sensible values.

mDown = false;
mPressed = false;
mReleased = false;

The next code block shows the implementations of the Update and IsReleased methods.

void Button::Update(uint32 aTouchState, int32 aX, int32 aY)
{
    if (!mEnabled)
        return;

    // Check if the touch position is within bounds of
    // this button
    aX -= mTopLeft.x;
    aY -= mTopLeft.y;

    bool lInBounds = (aX >= 0) && (aX < mSize.x) &&
                     (aY >= 0) && (aY < mSize.y);

    // Clear the released flag
    mReleased = false;

    // Check touch screen state
    if (aTouchState & S3E_POINTER_STATE_PRESSED)
    {
        // User has just touched the screen
        if (lInBounds)
        {
            mPressed = true;
            mDown = true;
        }
    }
    else if (aTouchState & S3E_POINTER_STATE_DOWN)
    {
        // If button has been pressed, check if user
        // is still touching inside it
        if (mPressed)
        {
            mDown = lInBounds;
        }
    }
    else if (aTouchState & S3E_POINTER_STATE_RELEASED)
    {
        // If user has released screen over a pressed
        // button, we set the release flag to true
        if (mPressed && mDown)
        {
            mReleased = true;
        }

        // Button is no longer pressed or down!
        mDown = false;
        mPressed = false;
    }
}

bool Button::IsReleased()
{
    return mReleased;
}

The Update method takes three parameters, the current touch status and the screen coordinates of the touch. You'll learn how to obtain this information shortly. The method first checks to see if the Button is disabled and immediately exits if it is. The screen coordinates passed to the method are then checked against the bounds of the Button to see if the Button is being touched.

The aTouchState parameter of the Update method is a bit mask comprised of three possible flags:

  • S3E_POINTER_STATE_PRESSED is set when the user has just touched the screen
  • S3E_POINTER_STATE_DOWN is set while the screen is being touched
  • S3E_POINTER_STATE_RELEASED is set when the user lifts its finger from the screen

The Update method uses the current value of aTouchState to update the internal member variables of the class accordingly.

The IsReleased method is trivial, it returns the current state of the mReleased variable.

We need to make one final change to the Button class. In the Render method, we draw the Button slightly darker while the user is pressing it. This visual feedback benefits the user experience of the application. Change the beginning of the Render method to the following:

if (!mEnabled)
    mpMaterial->SetColAmbient(96, 96, 96, 255);
else if (mDown)
    mpMaterial->SetColAmbient(192, 192, 192, 255);
else
    mpMaterial->SetColAmbient(255, 255, 255, 255);

With the Button class updated, you now just have to add some logic to detect touch input from the user. Open main.cpp again and add the following to the currently empty Update function:

// Allow device OS time to do its processing
s3eDeviceYield(0);

// Update pointer (actually touch screen!) inputs
s3ePointerUpdate();

// Read current touch screen inputs and use them to update Button states
uint32 lTouchState =
           s3ePointerGetState(S3E_POINTER_BUTTON_SELECT);
int32 x = s3ePointerGetX();
int32 y = s3ePointerGetY();
for (uint32 i = 0; i < BUTTON_COUNT; i++)
{
    gButton[i]->Update(lTouchState, x, y);
}

The call to s3eDeviceYield is vitally important in a Marmalade app as it allows the device's operating system time to handle any events, such as touch input, incoming calls, etc. The s3ePointerUpdate function takes a snapshot of the current touch screen status.

The current state of the first detected touch input is then found by calling s3ePointerGetState. It returns a value using the bit mask I described earlier. The s3ePointer functions are also used to detect mouse events on desktop operating systems. The value passed to s3ePointerGetState is S3E_POINTER_BUTTON_SELECT, which will return the status of the first detected touch event or the left mouse button, depending on the capabilities of the device the app is running on.

s3ePointerGetX and s3ePointerGetY return the screen coordinates of the touch. We then loop through the button instances and call Button::Update on each button, passing in the current touch screen status and touch coordinates.

The app is now capable of detecting user input and the Button instances will change color when they're pressed.

4. Adding Audio Playback

Step 1: Playing Compressed Audio Files

Playing back compressed audio files, such as MP3 files, is incredibly easy in Marmalade. In fact, it only takes a single line of code. Add the following code block to the end of the Update function in main.cpp.

// Check for button presses
if (gButton[BUTTON_AUDIO_MUSIC]->IsReleased())
{
    s3eAudioPlay("black-hole.mp3");
}

Whenever the user presses and releases the music note button on the top row of buttons, the app will call the s3eAudioPlay function, which will attempt to play the MP3 file called black-hole.mp3. This file must exist in the project data folder, so it can be located at runtime.

The black-hole.mp3 file was obtained from http://www.freesfx.co.uk and was composed by Craig Riley (SOCAN).

Step 2: Playing Uncompressed Sound Samples

Playing uncompressed sound samples is also possible in Marmalade. While it isn't quite as simple as playing a compressed sound file, it's more flexible as it allows you to play multiple sounds at a time, whereas most devices will only allow a single compressed audio track to be played back at any time.

Marmalade expects sound sample data to be in 16-bit signed PCM format and most audio editing software will allow you to save out files in this format using the WAV file format. However, Marmalade doesn't support the WAV file format directly, so for the purposes of this tutorial I have taken sound files saved in WAV format and removed the header from the file leaving only the sample data. For your own apps, you'd probably want to support WAV files directly, but that is beyond the scope of this tutorial.

I've added two sound files to the data folder called female-counting.raw and gun-battle.raw. The original WAV format files were obtained from http://soundbible.com and have been released under the Attribution 3.0 Creative Commons license.

In order to play a sampled sound effect, it is necessary to have the sound data in memory. I have created a Sound class that will take care of this for us. To implement this class, open up sound.h and add the following code block to it:

#ifndef SOUND_H
#define SOUND_H

#include "s3e.h"

class Sound
{
public:
    Sound(const char* apFileName);
    ~Sound();

    void Play();

private:
    int16* mpSoundData;
    uint32 mSamples;
};

#endif

Next, open sound.cpp and insert the following code block:

#include "sound.h"

Sound::Sound(const char* apFileName)
{
    // Attempt to open the sound effect file
    s3eFile* f = s3eFileOpen(apFileName, "rb");
    if (f)
    {
        // Seek to end of file to find its length
        s3eFileSeek(f, 0, S3E_FILESEEK_END);

        // Number of samples is file size divided by the
        // size of an int16
        mSamples = s3eFileTell(f) / sizeof(int16);
        s3eFileSeek(f, 0, S3E_FILESEEK_SET);

        // Allocate buffer for sound data
        mpSoundData = new int16[mSamples];

        // Read in sound data
        s3eFileRead(mpSoundData, sizeof(int16), mSamples, f);

        // Close the file
        s3eFileClose(f);
    }
    else
    {
        // File open failed, zero the member variables
        mpSoundData = NULL;
        mSamples = 0;
    }
}

Sound::~Sound()
{
    if (mpSoundData)
        delete[] mpSoundData;
}

void Sound::Play()
{
    if (mpSoundData)
    {
        int lChannel = s3eSoundGetFreeChannel();
        s3eSoundChannelPlay(lChannel, mpSoundData,
                            mSamples, 0, 0);
    }
}

The constructor takes the file name of a sound effect and finds the length of the file in bytes. An array of 16-bit signed integers is created, big enough to hold the entire sound, and the file is read into this buffer. The destructor deletes this buffer.

The Play method will actually start the sound sample playing. It does this by first asking Marmalade for a free sound channel with a call to s3eSoundGetFreeChannel.  The sound is then started on that channel by calling s3eSoundChannelPlay, passing in the channel number, start of the sound buffer, and the number of sound samples in the sound. The remaining two parameters indicate whether the sound sample should loop when it reaches the end and the offset into the sample data where subsequent loops should begin playing. By passing in zero for both of these parameters, the entire sound effect will loop continuously.

With the Sound class implemented, return to main.cpp and add some code to load and destroy the sound samples and to start the sounds playing when the user presses a button. Start by adding two new global variables after the declaration of the gpButton array.

Sound* gpGunBattleSound;
Sound* gpFemaleCountingSound;

Next, add the following to the end of the Initialise function. This code block will load the two sound files into memory and then set the default sound sample frequency to 44100Hz, which just so happens to be the frequency of both sounds used in the app.

// Load sound effects into memory
gpGunBattleSound = new Sound("gun-battle.raw");
gpFemaleCountingSound = new Sound("female-counting.raw");

// Configure default sample rate for s3eSound
s3eSoundSetInt(S3E_SOUND_DEFAULT_FREQ, 44100);

We also need to release the sound data on shut down. We do this by adding the following code block to the beginning of the Terminate function to destroy the Sound instances.

// Destroy sound effects
delete gpGunBattleSound;
delete gpFemaleCountingSound;

Finally, add the next code snippet to the end of the Update function, immediately after the end of the last if statement. This will start the sound effects playing in response to the user pressing the correct buttons.

else if (gButton[BUTTON_AUDIO_SFX]->IsReleased())
{
    if (gpGunBattleSound)
        gpGunBattleSound->Play();
}
else if (gButton[BUTTON_AUDIO_SPEECH]->IsReleased())
{
    if (gpFemaleCountingSound)
        gpFemaleCountingSound->Play();
}

Step 3: Stopping Audio Output

If you want to start a piece of audio playing, chances are you'll also eventually want to stop it playing. The code block below illustrates how to do this in Marmalade by having a button in the user interface stop all currently playing audio. Add the following block to the end of the if...else block at the very end of the Update function in main.cpp.

else if (gButton[BUTTON_AUDIO_OFF]->IsReleased())
{
    s3eAudioStop();
    s3eSoundStopAllChannels();
}

The call to s3eAudioStop will stop playback of any compressed audio track that is playing, while s3eSoundStopAllChannels will stop all uncompressed sampled sounds. It is possible to stop sampled sounds on a channel by channel basis, but for the purposes of this app it's fine to stop all channels that are currently active.

5. Integrating the Dolby Audio API

Step 1: Downloading the Dolby Audio API Marmalade Extension

Now that we have an app that can play some sounds, it's time to turn our attention to the Dolby Audio API. As you'll notice, adding support for the Dolby Audio API is incredibly simple and can be done in no more than five minutes.

You first need to obtain the Dolby Audio API Marmalade extension by visiting the Dolby Developer website. After creating a free developer account, you can download the Marmalade extension from the framework tab.

Step 2: Adding the Dolby Audio API to the Project

Extract the Dolby Audio API Marmalade extension archive to a folder on your development machine and find in the extracted Libraries folder for a folder named s3eDolbyAudio. Copy this folder and its contents into our project's DolbyTestApp folder, alongside the source and data folders.

To include the extension in your project, edit the DolbyTestApp.mkb file and add s3eDolbyAudio to the list of subprojects. If you then rebuild the project in Visual Studio, the MKB file will be reprocessed and the Dolby Audio API extension will be added to to the project.

Step 3: Initializing the Dolby Audio API

Before you can use the functionality of the Dolby Audio API, you must first check whether the device your app is running on supports Dolby Digital Plus. To implement the necessary checks, edit main.cpp and add the following #include at the top of the file.

#include "s3eDolbyAudio.h"

Next, declare a global variable named gDolbyInitialised after the declaration of gDolbySupported.

bool gDolbyInitialised;

To check whether Dolby Digital Plus is supported, you can add the following code block to the Initialise function, after the statement gDolbySupported = false;.

if (s3eDolbyAudioAvailable() == S3E_TRUE)
{
    if (s3eDolbyAudioSupported() == S3E_TRUE)
    {
        gDolbySupported = true;
    }
    s3eDeviceYield(0);
}

The first call to s3eDolbyAudioAvailable checks whether the Dolby Audio API extension is supported on the target platform. If the extension is available a call is made to s3eDolbyAudioSupported, which will return S3E_TRUE if the target device supports Dolby Digital Plus. If supported, the gDolbySupported flag is set true.

The call to s3eDeviceYield is to give the device time to perform background processing after doing the Dolby Digital Plus support test. Dolby recommend that you do not initialize Dolby Digital Plus immediately after checking whether it's supported, so the s3eDeviceYield call will help prevent issues during initialization.

At the end of the Initialise function, you can initialize Dolby Digital Plus by calling the s3eDolbyAudioInitialize function. Only if this function returns S3E_TRUE will the gDolbyInitialised flag be set true. The code you need to add is as follows:

// Initialise Dolby API
if (gDolbySupported)
{
    if (s3eDolbyAudioInitialize() == S3E_TRUE)
    {
        gDolbyInitialised = true;
    }
}

You should also shut down the Dolby Audio API when your program terminates, so add the following to the Terminate function, before the call to IwGxTerminate.

// Release resources used by Dolby API
if (gDolbySupported)
{
    s3eDolbyAudioRelease();
}

Step 4: Handling Suspending and Resuming

Since mobile phones and tablets are used for a plethora of tasks, it isn't uncommon for your app to be suspended, either because the user wants to perform a different task or an event such as an incoming call has occurred. Under these conditions the Dolby Audio API should be suspended so that it isn't active whilst your app is paused or running as a background task.

Marmalade allows us to set up some callback functions that will be triggered whenever the app loses or regains focus. To implement these, add the following code to the Initialise function immediately after the check for Dolby Digital Plus support.

// Initialise Pause/Resume callbacks
s3eDeviceRegister(S3E_DEVICE_PAUSE, AppSuspended, NULL);
s3eDeviceRegister(S3E_DEVICE_BACKGROUND, AppSuspended, NULL);
s3eDeviceRegister(S3E_DEVICE_UNPAUSE, AppResumed, NULL);
s3eDeviceRegister(S3E_DEVICE_FOREGROUND, AppResumed, NULL);

You should also remove these callback functions at shut down, so add the following lines to the Terminate function immediately after the delete gpTexture line.

// Disable Pause/Resume callbacks
s3eDeviceUnRegister(S3E_DEVICE_PAUSE, AppSuspended);
s3eDeviceUnRegister(S3E_DEVICE_BACKGROUND, AppSuspended);
s3eDeviceUnRegister(S3E_DEVICE_UNPAUSE, AppResumed);
s3eDeviceUnRegister(S3E_DEVICE_FOREGROUND, AppResumed);

Now you just need to implement the AppSuspended and AppResumed callback functions. Add this code after the declaration of the global variables at the top of main.cpp.

int32 AppSuspended(void* apSystemData, void* apUserData)
{
    if (gDolbyInitialised)
        s3eDolbyAudioSuspendSession();
    return 0;
}

int32 AppResumed(void* apSystemData, void* apUserData)
{
    if (gDolbyInitialised)
        s3eDolbyAudioRestartSession();
    return 0;
}

When the app is suspended or goes into background processing, the AppSuspended callback will be triggered, which calls s3eDolbyAudioSuspendSession if the gDolbyInitialised flag is true. When the app regains focus, AppResumed will be called, which invokes s3eDolbyAudioRestartSession if the Dolby Audio API has been initialized.

Step 5: Using the Dolby Audio API Filters

The final step to integrating the Dolby Audio API is to actually make use of the different audio filters it provides. There are four predefined filters available, which suit different types of audio output, movie, music, voice, and game.

Once the Dolby Audio API is active, pass S3E_TRUE to s3eDolbyAudioSetEnabled to ensure filtering support is switched on followed by a call to s3eDolbyAudioSetProfile. If you want to stop filtering you can do this with a call to s3eDolbyAudioSetEnabled, passing in S3E_FALSE.

Add the following code block to the end of the Update function to enable the bottom row of buttons to switch between the different filter types.

if (gButton[BUTTON_FILTER_OFF]->IsReleased())
{
    if (gDolbyInitialised)
    {
        s3eDolbyAudioSetEnabled(S3E_FALSE);
    }
}
else if (gButton[BUTTON_FILTER_MUSIC]->IsReleased())
{
    if (gDolbyInitialised)
    {
        s3eDolbyAudioSetEnabled(S3E_TRUE);
        s3eDolbyAudioSetProfile(MUSIC);
    }
}
else if (gButton[BUTTON_FILTER_MOVIE]->IsReleased())
{
    if (gDolbyInitialised)
    {
        s3eDolbyAudioSetEnabled(S3E_TRUE);
        s3eDolbyAudioSetProfile(MOVIE);
    }
}
else if (gButton[BUTTON_FILTER_GAME]->IsReleased())
{
    if (gDolbyInitialised)
    {
        s3eDolbyAudioSetEnabled(S3E_TRUE);
        s3eDolbyAudioSetProfile(GAME);
    }
}
else if (gButton[BUTTON_FILTER_VOICE]->IsReleased())
{
    if (gDolbyInitialised)
    {
        s3eDolbyAudioSetEnabled(S3E_TRUE);
        s3eDolbyAudioSetProfile(VOICE);
    }
}

6. Making a Device Build

Step 1: Including the Resource Files

Let's round off by getting the app running on a device. The first step is to ensure that the final deployment package contains all the necessary resource files. Edit DolbyTestApp.mkb and append the following code snippet to the end of the file.

assets
{
    [default]
    (data)
    black-hole.mp3
    female-counting.raw
    gun-battle.raw
    ui.png
}

deployments
{
    name="DolbyTestApp"
    caption="Dolby Test App"

    assets="default"
}

The assets section of an MKB file is used to list the resource files that need to be shipped with the executable in order for the app to run. The format is similar to the files section, using square brackets to make groupings of files and rounded brackets to indicate folder names where files can be found.

The deployments section lets you configure the final installation package. The name parameter lets us specify the file name that will be used for the package file and the caption parameter is used to declare the text that will appear beneath the app icon when it is installed on a device. The assets parameter references one of the groups defined in the assets section, so it is possible to switch between different asset sets should you need to, for example, if you want to create a full and lite version of your app.

Step 2: Making the Installation Package

To create an installation package for the device of your choosing, you must first compile the app source code for the type of processor used by the target device. In most cases, this will be an ARM chip, so you should select the GCC ARM Release option from the Solutions Configurations drop-down menu at the top of the Visual Studio toolbar.

Press F7 to build the app, followed by F5 to launch the Marmalade Deployment Tool.

The Marmalade Deployment Tool shows a wizard to create the installation packages. For example, if you want to create an Android build, which will also run on Kindle Fire devices, then you must first choose the build type, ARM GCC Release.

After clicking the Next Stage > button, you will be asked to choose which project configuration you wish to deploy. Check the Default configuration, which is the only configuration of our project, and click the Next Stage > button once more to see a list of platforms supported by your version of Marmalade. Tick the box next to Android and click the Next Stage > button again.

You can now choose what actions to take when creating the installation package. The drop-down menu lets you choose to just create the package, which means you'll need to manually install it on a device, create it and install it on a device connected via USB, or create, install, and run the app on a connected device. For the latter two options to work, you'll need the Android SDK installed on your development machine, because these options make use of the ADB tool, which is part of the Android SDK.

A discussion of how to set up an Android device for development use is beyond the scope of this tutorial, but you can read more about this topic on the Android Developer website.

Conclusion

As you can see, using the Dolby Audio API in a Marmalade app is a very simple process. Most of this tutorial was concerned with setting up the user interface or playing sounds rather than integrating the Dolby Audio API.

If you've written a game or any other kind of app in Marmalade that has rich audio output, then you really should consider adding support for Dolby Digital Plus to give your users the best possible audio experience.

2014-07-11T17:00:36.000Z2014-07-11T17:00:36.000ZSean Scaplehorn

An Introduction to Swift: Part 1

$
0
0

At WWDC 2014, Apple has introduced one of the biggest updates to iOS since 2008 from a developer's point of view. They introduced HomeKit, HealthKit, CloudKit, and Extensions, just to name a few. But the biggest surprise out of WWDC 2014 was the introduction of a brand new programming language, Swift.

Swift is a wonderful programming language that has been built from the ground up to be efficient and safe. It uses the same APIs that Objective-C does. Or, what you can do in Objective-C, you can do in Swift. It also introduces some new concepts longtime programmers will appreciate and some of which I will cover in this introductory series on Swift.

In this series, I'm going to assume you're already familiar with Objective-C. In the first article of this series, I talk about Swift's philosophy, file structure, and syntax. In the second article, I zoom in on more advanced aspects of Swift's syntax, such as optionals and memory management. Hang on to your hats, folks, it's going to be a doozy.

1. Philosophy

To better understand Swift, Apple has been conditioning us with structural improvements in Objective-C over the last few years. Objective-C improvements like code blocks, literal array and dictionary definitions, and ARC (Automatic Reference Counting) are but a few things Apple added to Objective-C that ease the transition to Swift.

One important pillar of Swift's philosophy is code initialization. All objects and variables  when defined in Swift must be initialized in code. An uninitialized object or variable will result in a compile time error in Swift. This ensures an object or variable always has a value. There's one special case in Swift for when an initial value cannot be defined. In this special case, your variable is called an optional. We will cover optionals in the second part of this series.

Another important pillar is branch completeness. All conditional branches, be it if or switch/case, must cover all conditions. This way, no code can fall through without it being covered. Missing a condition in your branch statement will be caught and generate a compile time error.

One last element of Swift's philosophy is its preference to constants over variables. Swift defines variables and constants in the following manner:

let someConstant : String = "This is a constant"
var someVariable : String = "This is a variable"

In the above example, the let keyword is used to define a constant while the var keyword defines a variable. By making the definition of constants this easy, Apple encourages the use of constants whenever possible. This leads to safer code in a multithreaded environment and better code optimization as the compiler knows a constant's value won't change.

Now, there's much more to Swift than a few syntax and formatting enhancements. Swift was built from the ground up to fix many common C/C++, and inherently Objective-C, sources of crashes. Issues like:

  • out-of-bound indexes in arrays
  • uninitialized data
  • unchecked return types
  • unchecked pointer access
  • implicit fall through 
  • goto errors

As someone who programs for both iOS and Android, I know first-hand how much more fun coding for the iOS platform really is with Cocoa and UIKit. This series will show you how much more fun coding can be by adding Swift to the mix.

2. File Structure

In Objective-C, we have header files (.h) and implementation files (.m). This is something Objective-C inherited from the C language.

In Swift, a class is defined in a single implementation file (.swift) that includes all the definitions and implementations of the class. This is reminiscent of other languages like Java and C#.

Gone is the need for juggling header files and adding the pesky #IFNDEF at the top of header files.

3. Syntax

The first thing you'll notice about Swift is the disappearance of the semicolon at the end of each statement. In Swift, each line is considered a statement and we don't have to add a semicolon at the end of each line.

I emphasize have to, because nothing stops you from adding semicolons at the end of your statements. I will continue to add semicolons at the end of each statement as I think it increases readability. Also, it's really difficult to get rid of the habit of adding semicolons like Cocoa developers have done for years.

Another important change in Swift is that curly braces are mandatory for if statements. That means no more Heartbleed bugs.

Syntax can be a complex subject to write about. Swift has a lot of subtleties that can take very long to go over, but that isn't the goal of this article. For brevity, I will concentrate on what changes an Objective-C developer would notice.

4. Similarities with Objective-C

I'll start off by showing you three code snippets that illustrate some of the similarities with Objective-C. It will help you in your understanding of the Swift language.

// Objective-C
for (int index = 0; index < 5; i++) {
     NSLog(@"%d",index);
}

// Swift
for index in 1..<5 {
    plrintln("\(index)");
}
// Objective-C
switch(index) {
    case 0:
        break;
    case 1:
        break;
    default:
        break;
}

// Swift
switch(index) {
    case 0:
    
    case 1:

    default:
}

// no break statement
// Objective-C
if (index == 0) {

}

// Swift
if index == 0 {

}

// parentheses are optional
// curly braces are required

Objective-C programmers will find that Swift has the same branch and iteration statements you're already familiar with, such as if/else,  for loops, for..in loops, and switch statements.

Swift includes two range operators, ..< and ..., to specify a range of values. In the above for loop, we use the half-closed range operator, ..<, to specify a range of values that includes 1, 2, 3, and 4, but it excludes 5. The other range operator is the closed range operator, .... It specifies a range of values that includes the value on both sides of the closed range operator. For example, 1...5. specifies a range of values from 1 through 5, including 5.

5. Defining Variables and Constants

Let's revisit the example I showed you earlier.

let someConstant : String = "This is a constant";
var someVariable : String = "This is a variable";

In Swift, we define constants using the let keyword and variables using the var keyword. The colon, : ,is a marker to define types. In the above example, we're creating a constant and a variable of type String.

We're also initializing the constant and the variable with a string. In Swift, strings are defined just like C strings in Objective-C, they're not preceded by an @ symbol.

Objective-C is a strongly typed language, which means that the type of a variable or parameter must always be specified. Swift is also a strongly typed language, but Swift is a bit smarter as the compiler will infer a variable's type. The compiler also makes sure that no incorrect casting of variables occurs without your expressed knowledge and intervention.

If we rewrite the above example letting type inference do its work, then the code snippet looks as follows:

let someConstant = "This is a constant";
var someVariable = "This is a variable";
let someInt = 1;
let someFloat = 1.0;

This is much better and the code is much cleaner. The compiler is smart enough to understand that someInt is an Int and someFloat is a Double.

6. Strings

One way to get an idea of the strength of a language is by exploring how it handles string manipulation. Objective-C has a lot of functions and features that let us handle strings, better than most languages, but they tend to be verbose and confusing from time to time.

Let's start with an Objective-C example. To concatenate two strings in Objective-C, we do the following:

NSString *string = @"Hello ";
NSString *greeting = [string stringByAppendingString:@"World!"];

In Swift, to append a string to another string, we use the + operator. It's that simple.

let string = "Hello "
let greeting = string + "World!"

Strings in Swift are Unicode, which means that we can write:

let string = "你好世界"

We can iterate the characters of a string using a for..in statement as shown in the following example. We can use a for..in loop to iterate Unicode strings too. It's really that cool.

let str = "Hello";
for char in str {
    println(char);
}

// outputs
// H
// e
// l
// l
// o

One last thing I'd like to cover about strings before moving on is string interpolation. In Objective-C, if we want to output a string with variables, we invoke [NSString stringWithFormat:]. In Swift, variables can be embedded. Take a look at the following example.

let x = 4;
let y = 5;

println( "\(x) x \(y) = \(x*y)")

// outputs
// 4 x 5 = 20

To use string interpolation, you wrap the variable or function call in parentheses and put a backslash in front of it, \( expression).

7. Arrays & Dictionaries

Array& Dictionary

As an Objective-C programmer, you're already familiar with arrays and dictionaries. Swift also has collection classes and adds a few additional features to them.

How do you use collections in Swift? In Swift, they're called Array and Dictionary. Declaring an array in Swift is similar to declaring an array literal in Objective-C, using a set of square brackets, [ ], but without an @ symbol preceding them.

let someArray: String[] = ["one","two","three"];
var someOtherArray: String[] = ["alpha","beta","gamma"];

The same goes for dictionaries. However, instead of using curly braces, you use square brackets.

let someDictionary:Dictionary<String, Int> = ["one":1, "two":2, "three":3];
var someOtherDictionary:Dictionary<String, Int> = ["cats":1, "dogs":4, "mice":3];

Mutability

If an Array object is equivalent to an NSArray object and a Dictionary object is equivalent to an NSDictionary object, then how do we create mutable arrays and dictionaries in Swift?

The answer is very simple, declare the object as a variable. In other words, in the previous example someArray is the equivalent of an NSArray instance and someOtherArray that of an NSMutableArray instance. This applies to dictionaries too. In the previous example, someDictionary is the equivalent of an NSDictionary instance and someOtherDictionary that of an NSMutableDictionary instance. That's neat, right?

While Objective-C arrays and dictionaries can only contain objects, in Swift, collections can contain objects as well as primitive data types, such as integers and floats. Another important difference with Objective-C is that collections in Swift are typed, explicitly or through type inference at compile time. By specifying the type of the objects in a collection, Swift adds extra safety to these collections.

Even though we can omit a variable's type when declaring it, it doesn't alter the fact that the compiler will assign types to the objects in a collection. Using type inference helps to keep the code legible and light.

We can redeclare the Array and Dictionary objects we declared earlier as follows:

let someArray = ["one","two","three"];
var someOtherArray = ["alpha","beta","gamma"];
let someDictionary = ["one":1, "two":2, "three":3];
var someOtherDictionary = ["cats":1, "dogs":4, "mice":3];

The compiler will inspect the collections during their initialization and infer the correct type. In other words, it understands that someArray and someOtherArray are a collection of String objects and someDictionary and someOtherDictionary are dictionaries with keys of type String and values of type Int.

Collection Manipulation

Adding an object or another array to an array is very similar to string concatenation in that we also use the + operator.

var array = ["one","two","three"]; // mutable array
array += "four"; // add element to array
array += ["five","six"]; // add array to array

Manipulating dictionaries is just as easy.

var dictionary = ["cat": 2,"dog":4,"snake":8]; // mutable dictionary
dictionary["lion"] = 7; // add element to dictionary
dictionary += ["bear":1,"mouse":6]; // add dictionary to dictionary

Typed Collections

Earlier, I mentioned that collections in Swift are typed, but what does that mean? If we define a collection that contains String objects, you can only add String objects to that collection. Failing to do so will result in an error.

Let's look at an example to clarify this. We define a collection of Car objects. The following code snippet shows the Car class definition and a mutable array cars, containing three Car instances.

// Car class
class Car {
    var speed = 0.0
    func accelerate(by: Double = 1.0) -> Bool {
        speed += by;
        return true;
    }
}

var cars = [ Car(), Car(), Car()];

Behind the scenes, the compiler will infer the type of the array. If we want to add a new Car instance to the collection, we can simply use the + operator as shown below.

cars += Car();

However, adding an object of a different type will result in an error.

cars += "Some String"; // this will cause a compiler error

This has the added advantage of type safety for fetching objects from collections. It also eliminates the need for casting collection objects before using them.

In our example, a Car object has an accelerate method. Because a collection is typed, we can grab an item from the array and immediately invoke a method on the object, in one line of code. We don't need to worry about the element's type since the collection only contains Car objects.

cars[0].accelerate(by: 2.0);

To accomplish the same thing in Objective-C with the same level of safety, we'd need to write the following:

id pointer = cars[0];
if([pointer isKindOfClass:[Car class]]) {
    Car* car = (Car*) pointer;
    [car accelerateBy:2.0];
}

Finally, to iterate an array, we use a for..in loop:

for car in cars {
    car.accelerate(by:2.0);
}

To iterate a dictionary, we also use a for..in loop:

for (key,value) in someDictionary {
    println("Key \(key) has value \(value)"
}

As you can see, typed collections are a powerful feature of the Swift language.

Conclusion

 We've already learned quite a bit about the Swift language and you should take the time to let it sink in. I recommend that you download Xcode 6 as soon as possible and start applying what you've learned in this article in Xcode's new Playground feature. Playgrounds let you play with Swift in real time.

That's it for this article. In the next instalment of this series, we take a look at tuples, functions, closures, classes and last but not least, optionals. You'll also learn how memory management works in Swift. Stay tuned.

2014-07-14T18:41:57.082Z2014-07-14T18:41:57.082ZMichael Musallam

An Introduction to Swift: Part 1

$
0
0
tag:code.tutsplus.com,2005:PostPresenter/cms-21389

At WWDC 2014, Apple has introduced one of the biggest updates to iOS since 2008 from a developer's point of view. They introduced HomeKit, HealthKit, CloudKit, and Extensions, just to name a few. But the biggest surprise out of WWDC 2014 was the introduction of a brand new programming language, Swift.

Swift is a wonderful programming language that has been built from the ground up to be efficient and safe. It uses the same APIs that Objective-C does. Or, what you can do in Objective-C, you can do in Swift. It also introduces some new concepts longtime programmers will appreciate and some of which I will cover in this introductory series on Swift.

In this series, I'm going to assume you're already familiar with Objective-C. In the first article of this series, I talk about Swift's philosophy, file structure, and syntax. In the second article, I zoom in on more advanced aspects of Swift's syntax, such as optionals and memory management. Hang on to your hats, folks, it's going to be a doozy.

1. Philosophy

To better understand Swift, Apple has been conditioning us with structural improvements in Objective-C over the last few years. Objective-C improvements like code blocks, literal array and dictionary definitions, and ARC (Automatic Reference Counting) are but a few things Apple added to Objective-C that ease the transition to Swift.

One important pillar of Swift's philosophy is code initialization. All objects and variables  when defined in Swift must be initialized in code. An uninitialized object or variable will result in a compile time error in Swift. This ensures an object or variable always has a value. There's one special case in Swift for when an initial value cannot be defined. In this special case, your variable is called an optional. We will cover optionals in the second part of this series.

Another important pillar is branch completeness. All conditional branches, be it if or switch/case, must cover all conditions. This way, no code can fall through without it being covered. Missing a condition in your branch statement will be caught and generate a compile time error.

One last element of Swift's philosophy is its preference to constants over variables. Swift defines variables and constants in the following manner:

let someConstant : String = "This is a constant"
var someVariable : String = "This is a variable"

In the above example, the let keyword is used to define a constant while the var keyword defines a variable. By making the definition of constants this easy, Apple encourages the use of constants whenever possible. This leads to safer code in a multithreaded environment and better code optimization as the compiler knows a constant's value won't change.

Now, there's much more to Swift than a few syntax and formatting enhancements. Swift was built from the ground up to fix many common C/C++, and inherently Objective-C, sources of crashes. Issues like:

  • out-of-bound indexes in arrays
  • uninitialized data
  • unchecked return types
  • unchecked pointer access
  • implicit fall through 
  • goto errors

As someone who programs for both iOS and Android, I know first-hand how much more fun coding for the iOS platform really is with Cocoa and UIKit. This series will show you how much more fun coding can be by adding Swift to the mix.

2. File Structure

In Objective-C, we have header files (.h) and implementation files (.m). This is something Objective-C inherited from the C language.

In Swift, a class is defined in a single implementation file (.swift) that includes all the definitions and implementations of the class. This is reminiscent of other languages like Java and C#.

Gone is the need for juggling header files and adding the pesky #IFNDEF at the top of header files.

3. Syntax

The first thing you'll notice about Swift is the disappearance of the semicolon at the end of each statement. In Swift, each line is considered a statement and we don't have to add a semicolon at the end of each line.

I emphasize have to, because nothing stops you from adding semicolons at the end of your statements. I will continue to add semicolons at the end of each statement as I think it increases readability. Also, it's really difficult to get rid of the habit of adding semicolons like Cocoa developers have done for years.

Another important change in Swift is that curly braces are mandatory for if statements. That means no more Heartbleed bugs.

Syntax can be a complex subject to write about. Swift has a lot of subtleties that can take very long to go over, but that isn't the goal of this article. For brevity, I will concentrate on what changes an Objective-C developer would notice.

4. Similarities with Objective-C

I'll start off by showing you three code snippets that illustrate some of the similarities with Objective-C. It will help you in your understanding of the Swift language.

// Objective-C
for (int index = 0; index < 5; i++) {
     NSLog(@"%d",index);
}

// Swift
for index in 1..<5 {
    plrintln("\(index)");
}
// Objective-C
switch(index) {
    case 0:
        break;
    case 1:
        break;
    default:
        break;
}

// Swift
switch(index) {
    case 0:
    
    case 1:

    default:
}

// no break statement
// Objective-C
if (index == 0) {

}

// Swift
if index == 0 {

}

// parentheses are optional
// curly braces are required

Objective-C programmers will find that Swift has the same branch and iteration statements you're already familiar with, such as if/else,  for loops, for..in loops, and switch statements.

Swift includes two range operators, ..< and ..., to specify a range of values. In the above for loop, we use the half-closed range operator, ..<, to specify a range of values that includes 1, 2, 3, and 4, but it excludes 5. The other range operator is the closed range operator, .... It specifies a range of values that includes the value on both sides of the closed range operator. For example, 1...5. specifies a range of values from 1 through 5, including 5.

5. Defining Variables and Constants

Let's revisit the example I showed you earlier.

let someConstant : String = "This is a constant";
var someVariable : String = "This is a variable";

In Swift, we define constants using the let keyword and variables using the var keyword. The colon, : ,is a marker to define types. In the above example, we're creating a constant and a variable of type String.

We're also initializing the constant and the variable with a string. In Swift, strings are defined just like C strings in Objective-C, they're not preceded by an @ symbol.

Objective-C is a strongly typed language, which means that the type of a variable or parameter must always be specified. Swift is also a strongly typed language, but Swift is a bit smarter as the compiler will infer a variable's type. The compiler also makes sure that no incorrect casting of variables occurs without your expressed knowledge and intervention.

If we rewrite the above example letting type inference do its work, then the code snippet looks as follows:

let someConstant = "This is a constant";
var someVariable = "This is a variable";
let someInt = 1;
let someFloat = 1.0;

This is much better and the code is much cleaner. The compiler is smart enough to understand that someInt is an Int and someFloat is a Double.

6. Strings

One way to get an idea of the strength of a language is by exploring how it handles string manipulation. Objective-C has a lot of functions and features that let us handle strings, better than most languages, but they tend to be verbose and confusing from time to time.

Let's start with an Objective-C example. To concatenate two strings in Objective-C, we do the following:

NSString *string = @"Hello ";
NSString *greeting = [string stringByAppendingString:@"World!"];

In Swift, to append a string to another string, we use the + operator. It's that simple.

let string = "Hello "
let greeting = string + "World!"

Strings in Swift are Unicode, which means that we can write:

let string = "你好世界"

We can iterate the characters of a string using a for..in statement as shown in the following example. We can use a for..in loop to iterate Unicode strings too. It's really that cool.

let str = "Hello";
for char in str {
    println(char);
}

// outputs
// H
// e
// l
// l
// o

One last thing I'd like to cover about strings before moving on is string interpolation. In Objective-C, if we want to output a string with variables, we invoke [NSString stringWithFormat:]. In Swift, variables can be embedded. Take a look at the following example.

let x = 4;
let y = 5;

println( "\(x) x \(y) = \(x*y)")

// outputs
// 4 x 5 = 20

To use string interpolation, you wrap the variable or function call in parentheses and put a backslash in front of it, \( expression).

7. Arrays & Dictionaries

Array& Dictionary

As an Objective-C programmer, you're already familiar with arrays and dictionaries. Swift also has collection classes and adds a few additional features to them.

How do you use collections in Swift? In Swift, they're called Array and Dictionary. Declaring an array in Swift is similar to declaring an array literal in Objective-C, using a set of square brackets, [ ], but without an @ symbol preceding them.

let someArray: String[] = ["one","two","three"];
var someOtherArray: String[] = ["alpha","beta","gamma"];

The same goes for dictionaries. However, instead of using curly braces, you use square brackets.

let someDictionary:Dictionary<String, Int> = ["one":1, "two":2, "three":3];
var someOtherDictionary:Dictionary<String, Int> = ["cats":1, "dogs":4, "mice":3];

Mutability

If an Array object is equivalent to an NSArray object and a Dictionary object is equivalent to an NSDictionary object, then how do we create mutable arrays and dictionaries in Swift?

The answer is very simple, declare the object as a variable. In other words, in the previous example someArray is the equivalent of an NSArray instance and someOtherArray that of an NSMutableArray instance. This applies to dictionaries too. In the previous example, someDictionary is the equivalent of an NSDictionary instance and someOtherDictionary that of an NSMutableDictionary instance. That's neat, right?

While Objective-C arrays and dictionaries can only contain objects, in Swift, collections can contain objects as well as primitive data types, such as integers and floats. Another important difference with Objective-C is that collections in Swift are typed, explicitly or through type inference at compile time. By specifying the type of the objects in a collection, Swift adds extra safety to these collections.

Even though we can omit a variable's type when declaring it, it doesn't alter the fact that the compiler will assign types to the objects in a collection. Using type inference helps to keep the code legible and light.

We can redeclare the Array and Dictionary objects we declared earlier as follows:

let someArray = ["one","two","three"];
var someOtherArray = ["alpha","beta","gamma"];
let someDictionary = ["one":1, "two":2, "three":3];
var someOtherDictionary = ["cats":1, "dogs":4, "mice":3];

The compiler will inspect the collections during their initialization and infer the correct type. In other words, it understands that someArray and someOtherArray are a collection of String objects and someDictionary and someOtherDictionary are dictionaries with keys of type String and values of type Int.

Collection Manipulation

Adding an object or another array to an array is very similar to string concatenation in that we also use the + operator.

var array = ["one","two","three"]; // mutable array
array += "four"; // add element to array
array += ["five","six"]; // add array to array

Manipulating dictionaries is just as easy.

var dictionary = ["cat": 2,"dog":4,"snake":8]; // mutable dictionary
dictionary["lion"] = 7; // add element to dictionary
dictionary += ["bear":1,"mouse":6]; // add dictionary to dictionary

Typed Collections

Earlier, I mentioned that collections in Swift are typed, but what does that mean? If we define a collection that contains String objects, you can only add String objects to that collection. Failing to do so will result in an error.

Let's look at an example to clarify this. We define a collection of Car objects. The following code snippet shows the Car class definition and a mutable array cars, containing three Car instances.

// Car class
class Car {
    var speed = 0.0
    func accelerate(by: Double = 1.0) -> Bool {
        speed += by;
        return true;
    }
}

var cars = [ Car(), Car(), Car()];

Behind the scenes, the compiler will infer the type of the array. If we want to add a new Car instance to the collection, we can simply use the + operator as shown below.

cars += Car();

However, adding an object of a different type will result in an error.

cars += "Some String"; // this will cause a compiler error

This has the added advantage of type safety for fetching objects from collections. It also eliminates the need for casting collection objects before using them.

In our example, a Car object has an accelerate method. Because a collection is typed, we can grab an item from the array and immediately invoke a method on the object, in one line of code. We don't need to worry about the element's type since the collection only contains Car objects.

cars[0].accelerate(by: 2.0);

To accomplish the same thing in Objective-C with the same level of safety, we'd need to write the following:

id pointer = cars[0];
if([pointer isKindOfClass:[Car class]]) {
    Car* car = (Car*) pointer;
    [car accelerateBy:2.0];
}

Finally, to iterate an array, we use a for..in loop:

for car in cars {
    car.accelerate(by:2.0);
}

To iterate a dictionary, we also use a for..in loop:

for (key,value) in someDictionary {
    println("Key \(key) has value \(value)"
}

As you can see, typed collections are a powerful feature of the Swift language.

Conclusion

 We've already learned quite a bit about the Swift language and you should take the time to let it sink in. I recommend that you download Xcode 6 as soon as possible and start applying what you've learned in this article in Xcode's new Playground feature. Playgrounds let you play with Swift in real time.

That's it for this article. In the next instalment of this series, we take a look at tuples, functions, closures, classes and last but not least, optionals. You'll also learn how memory management works in Swift. Stay tuned.

2014-07-14T18:41:57.082Z2014-07-14T18:41:57.082ZMichael Musallam

Create a Plane Fighting Game in Corona: More Gameplay

$
0
0
Final product image
What You'll Be Creating

Introduction

In the previous tutorial of this series, we started implementing the game's gameplay and already managed to get the plane moving around on the screen. In this tutorial, we'll continue implementing the gameplay. Let's dive right in with the startTimers function.

1. startTimers

As its name indicates, the startTimers function starts the timers. Add the following code to gamelevel.lua.

function startTimers()

end

Invoke this function in the enterScene method as shown below.

function scene:enterScene( event )
    local planeSound = audio.loadStream("planesound.mp3")
    planeSoundChannel = audio.play( planeSound, {loops=-1} )
    Runtime:addEventListener("enterFrame", gameLoop)
    startTimers()
end

2. firePlayerBullet

The firePlayerBullet function creates a bullet for the player.

function firePlayerBullet()
    local tempBullet = display.newImage("bullet.png",(player.x+playerWidth/2) - bulletWidth,player.y-bulletHeight)
    table.insert(playerBullets,tempBullet);
    planeGroup:insert(tempBullet)
end 

Here we use the Display object's newImage method to create the bullet. We position it in such a way that it's in the center of the plane on the x axis and at the very top of the plane on the y axis. The bullet is then inserted into the playerBullets table for later reference and also into the planeGroup.

3. Calling firePlayerBullet

We need to call the firePlayerBullet function periodically to make sure the player's plane is automatically firing bullets. Add the following code snippet in the startTimers function.

function startTimers()
    firePlayerBulletTimer = timer.performWithDelay(2000, firePlayerBullet ,-1)
end

As its name indicates, the Timer's performWithDelay method calls a specified function after a period of time has passed. The time is in milliseconds, so here we are calling the firePlayerBullet function every two seconds. By passing -1 as the third argument, the timer will repeat forever.

If you test the game now, you should see that every two seconds a bullet appears. However, they aren't moving yet. We will take care of that in the next few steps.

4.movePlayerBullets

In movePlayerBullets, we loop through the playerBullets table and change the y coordinate of every bullet. We first check to make sure the playerBullets table has bullets in it. The # before playerBullets is called the length operator and it returns the length of the object it is called upon. It's useful to know that the # operator also works on strings.

function movePlayerBullets()
    if(#playerBullets > 0) then
        for i=1,#playerBullets do
            playerBullets[i]. y = playerBullets[i].y - 7
	end
   end
end

We need to invoke movePlayerBullets in the gameLoop function as shown below.

function gameLoop()
    --SNIP--
    numberOfTicks = numberOfTicks + 1
    movePlayer()
    movePlayerBullets()
end

5. checkPlayerBulletsOutOfBounds

When a bullet goes off-screen, it is no longer relevant to the game. However, they're still part of the playerBullets table and continue to move like any other bullet in the table. This is a waste of resources and, if the game were to go on for a very long time, it would result in hundreds or thousands of unused objects.

To overcome this, we monitor the bullets and, once they move off-screen, we remove them from the playerBullets table as well as from from the display. Take a look at the implementation of checkPlayerBulletsOutOfBounds.

function checkPlayerBulletsOutOfBounds()
 if(#playerBullets > 0) then
     for i=#playerBullets,1,-1 do
            if(playerBullets[i].y < -18) then
		playerBullets[i]:removeSelf()
		playerBullets[i] = nil
		table.remove(playerBullets,i)
            end
       end
     end
end

It's important to note that we are looping through the playerBullets table in backwards. If we loop through the table forwards, then, when we remove one of the bullets, it would throw the looping index off and causing an error. By looping over the table in reverse order, the last bullet is already processed. The removeSelf method  removes the display object and frees its memory. As a best practice, you should set any objects to nil after calling removeSelf.

We invoke this function in the gameLoop function.

function gameLoop()
    --SNIP--
    movePlayer()
    movePlayerBullets()
    checkPlayerBulletsOutOfBounds()
end

If you want to see if this function is working properly, you can temporarily insert a print("Removing Bullet") statement immediately after setting the display object to nil.

6. generateIsland

To make the game more interesting, we generate an island every so often, and move it down the screen to give the appearance of the plane flying over the islands. Add the following code snippet for the generateIsland function.

function generateIsland()
    local tempIsland = display.newImage("island1.png", math.random(0,display.contentWidth - islandWidth),-islandHeight)
    table.insert(islands,tempIsland)
    islandGroup:insert( tempIsland )
end

We make use of the newImage method once again and position the island by setting a negative value for the islandHeight. For the x position, we use the math.random method  to generate a number between 0 and the display's contentWidth minus the islandWidth. The reason we subtract the width of the island is to make sure the island is completely on the screen. If we wouldn't subtract the island's width, there would be a chance that part of the island wouldn't be on the screen.

We need to start a timer to generate an island every so often. Add the following snippet to the startTimers function we created earlier. As you can see, we are generating an island every five seconds. In the next step, we'll make the islands move.

function startTimers()
    firePlayerBulletTimer =    timer.performWithDelay(2000, firePlayerBullet ,-1)
    generateIslandTimer = timer.performWithDelay( 5000, generateIsland ,-1)
end

7.moveIslands

The implementation of moveIslands is nearly identical to the movePlayerBullets function. We check if the islands table contains any islands and, if it does, we loop through it and move each island a little bit.

function moveIslands()
if(#islands > 0) then
    for i=1, #islands do
        islands[i].y = islands[i].y + 3
   end
  end
end

8.checkIslandsOutOfBounds

Just like we check if the player's bullets have moved off-screen, we check if any of the islands had moved off-screen. The implementation of checkIslandsOutOfBounds should therefore look familiar to you. We check if the islands y position is greater than display.contentHeight and if it is, we know the island has moved off-screen and can therefore be removed.

function  checkIslandsOutOfBounds() 
    if(#islands > 0) then
        for i=#islands,1,-1 do
            if(islands[i].y > display.contentHeight) then
 	        islands[i]:removeSelf()
 		islands[i] = nil
 		table.remove(islands,i)
 	end
      end
  end
end

9. generateFreeLife

Every so often, the player has a chance to get a free life. We first generate a free life image and if the player collides with the image they get an extra life. The player can have a maximum of six lives.

function generateFreeLife ()
    if(numberOfLives >= 6) then
	return
   end
   local freeLife = display.newImage("newlife.png", math.random(0,display.contentWidth - 40), 0);
   table.insert(freeLifes,freeLife)
   planeGroup:insert(freeLife)
end 

If the player already has six lives, we do nothing by returning early from the function. If not, we create a new life image and add it to the screen. Similar to how we positioned the islands earlier, we set the image at a negative y position and generate a random value for the image's x position. We then insert it into the freeLifes table to be able to reference it later.

We need to call this function every so often. Add the following snippet to the startTimers function.

function startTimers()
    firePlayerBulletTimer =    timer.performWithDelay(2000, firePlayerBullet ,-1)
    generateIslandTimer = timer.performWithDelay( 5000, generateIsland ,-1)
    generateFreeLifeTimer = timer.performWithDelay(7000,generateFreeLife, - 1)
end

10.moveFreeLives

The implementation of moveFreeLifes should look familiar. We are looping through the freeLifes table and move every image in it.

function moveFreeLifes()
    if(#freeLifes > 0) then
	for i=1,#freeLifes do
	    freeLifes[i].y = freeLifes[i].y  +5
	end
   end
end

All we need to do is call moveFreeLifes in the gameLoop function.

function gameLoop()
    --SNIP--
    checkIslandsOutOfBounds()
    moveFreeLifes()
end

11. checkFreeLifesOutOfBounds

The following code snippet should also look familiar to you by now. We check if any of the images in the freeLifes table have moved off-screen and remove the ones that have.

function  checkFreeLifesOutOfBounds() 
    if(#freeLifes > 0) then
        for i=#freeLifes,1,-1 do
            if(freeLifes[i].y > display.contentHeight) then
 	        freeLifes[i]:removeSelf()
 		freeLifes[i] = nil
 		table.remove(freeLifes,i)
 	   end
 	end
     end
 end

We call this function in the gameLoop function.

function gameLoop()
    --SNIP--
    checkIslandsOutOfBounds()
    moveFreeLifes()
    checkFreeLifesOutOfBounds()
end

12. hasCollided

We need to be able to tell when game objects collide with each other, such as the player's plane and the free life images, the bullets and the planes, etc. While Corona offers a very robust physics engine that can easily handle collisions between display objects for us, doing so adds a bit of overhead with the calculations the engine has to do every frame.

For the purposes of this game, we will be using a simple bounding box collision detection system. What this function does, is make sure the rectangles or bounding boxes around two objects don't overlap. If they do, the objects are colliding. This logic is implemented in the hasCollided function.

function hasCollided( obj1, obj2 )
   if ( obj1 == nil ) then 
      return false
   end
   if ( obj2 == nil ) then  
      return false
   end

   local left = obj1.contentBounds.xMin <= obj2.contentBounds.xMin and obj1.contentBounds.xMax >= obj2.contentBounds.xMin
   local right = obj1.contentBounds.xMin >= obj2.contentBounds.xMin and obj1.contentBounds.xMin <= obj2.contentBounds.xMax
   local up = obj1.contentBounds.yMin <= obj2.contentBounds.yMin and obj1.contentBounds.yMax >= obj2.contentBounds.yMin
   local down = obj1.contentBounds.yMin >= obj2.contentBounds.yMin and obj1.contentBounds.yMin <= obj2.contentBounds.yMax

   return (left or right) and (up or down)
end

I found this code snippet on the CoronaLabs website. It works really well, because the game objects in our game are rectangular. If you're working with object that aren't rectangular, then you better take advantage of Corona's physics engine as its collision detection is very well optimized for this.

13. checkPlayerCollidesWithFreeLife

We want to check if the player's plane has collided with a free life object. If it has, then we award the player a free life.

function checkPlayerCollidesWithFreeLife() 
    if(#freeLifes > 0) then
        for i=#freeLifes,1,-1 do
	     if(hasCollided(freeLifes[i], player)) then
	        freeLifes[i]:removeSelf()
		freeLifes[i] = nil
		table.remove(freeLifes, i)
		numberOfLives = numberOfLives + 1
		hideLives()
		showLives()
	    end
	end
    end
end

In the checkPlayerCollidesWithFreeLife function, we loop through the freeLives table backwards for the same reason I described earlier. We call the hasCollided function and pass in the current image and the player's plane. If the two object collide, we remove the free life image, increment the numberOfLives variable, and call the hideLives and showLives function.

We invoke this function in the gameLoop function.

function gameLoop()
    --SNIP--
    moveFreeLifes()
    checkFreeLifesOutOfBounds()
    checkPlayerCollidesWithFreeLife()
end

14. hideLives

The hideLives function loops through the livesImages table and sets the isVisible property of each life image to false.

function hideLives()
    for i=1, 6 do
        livesImages[i].isVisible = false
   end
end

15. showLives

The showLives function loops through the livesImages table and sets each image's isVisible property to true.

function showLives()
    for i=1, numberOfLives do
        livesImages[i].isVisible = true;
    end
end

Conclusion

This brings the third part of this series to a close. In the next and final installment of this series, we will create enemy planes and finalized the game's gameplay. Thanks for reading and see you there.

2014-07-16T16:55:38.000Z2014-07-16T16:55:38.000ZJames Tyner

Create a Plane Fighting Game in Corona: More Gameplay

$
0
0
tag:code.tutsplus.com,2005:PostPresenter/cms-21347
Final product image
What You'll Be Creating

Introduction

In the previous tutorial of this series, we started implementing the game's gameplay and already managed to get the plane moving around on the screen. In this tutorial, we'll continue implementing the gameplay. Let's dive right in with the startTimers function.

1. startTimers

As its name indicates, the startTimers function starts the timers. Add the following code to gamelevel.lua.

function startTimers()

end

Invoke this function in the enterScene method as shown below.

function scene:enterScene( event )
    local planeSound = audio.loadStream("planesound.mp3")
    planeSoundChannel = audio.play( planeSound, {loops=-1} )
    Runtime:addEventListener("enterFrame", gameLoop)
    startTimers()
end

2. firePlayerBullet

The firePlayerBullet function creates a bullet for the player.

function firePlayerBullet()
    local tempBullet = display.newImage("bullet.png",(player.x+playerWidth/2) - bulletWidth,player.y-bulletHeight)
    table.insert(playerBullets,tempBullet);
    planeGroup:insert(tempBullet)
end 

Here we use the Display object's newImage method to create the bullet. We position it in such a way that it's in the center of the plane on the x axis and at the very top of the plane on the y axis. The bullet is then inserted into the playerBullets table for later reference and also into the planeGroup.

3. Calling firePlayerBullet

We need to call the firePlayerBullet function periodically to make sure the player's plane is automatically firing bullets. Add the following code snippet in the startTimers function.

function startTimers()
    firePlayerBulletTimer = timer.performWithDelay(2000, firePlayerBullet ,-1)
end

As its name indicates, the Timer's performWithDelay method calls a specified function after a period of time has passed. The time is in milliseconds, so here we are calling the firePlayerBullet function every two seconds. By passing -1 as the third argument, the timer will repeat forever.

If you test the game now, you should see that every two seconds a bullet appears. However, they aren't moving yet. We will take care of that in the next few steps.

4.movePlayerBullets

In movePlayerBullets, we loop through the playerBullets table and change the y coordinate of every bullet. We first check to make sure the playerBullets table has bullets in it. The # before playerBullets is called the length operator and it returns the length of the object it is called upon. It's useful to know that the # operator also works on strings.

function movePlayerBullets()
    if(#playerBullets > 0) then
        for i=1,#playerBullets do
            playerBullets[i]. y = playerBullets[i].y - 7
	end
   end
end

We need to invoke movePlayerBullets in the gameLoop function as shown below.

function gameLoop()
    --SNIP--
    numberOfTicks = numberOfTicks + 1
    movePlayer()
    movePlayerBullets()
end

5. checkPlayerBulletsOutOfBounds

When a bullet goes off-screen, it is no longer relevant to the game. However, they're still part of the playerBullets table and continue to move like any other bullet in the table. This is a waste of resources and, if the game were to go on for a very long time, it would result in hundreds or thousands of unused objects.

To overcome this, we monitor the bullets and, once they move off-screen, we remove them from the playerBullets table as well as from from the display. Take a look at the implementation of checkPlayerBulletsOutOfBounds.

function checkPlayerBulletsOutOfBounds()
 if(#playerBullets > 0) then
     for i=#playerBullets,1,-1 do
            if(playerBullets[i].y < -18) then
		playerBullets[i]:removeSelf()
		playerBullets[i] = nil
		table.remove(playerBullets,i)
            end
       end
     end
end

It's important to note that we are looping through the playerBullets table in backwards. If we loop through the table forwards, then, when we remove one of the bullets, it would throw the looping index off and causing an error. By looping over the table in reverse order, the last bullet is already processed. The removeSelf method  removes the display object and frees its memory. As a best practice, you should set any objects to nil after calling removeSelf.

We invoke this function in the gameLoop function.

function gameLoop()
    --SNIP--
    movePlayer()
    movePlayerBullets()
    checkPlayerBulletsOutOfBounds()
end

If you want to see if this function is working properly, you can temporarily insert a print("Removing Bullet") statement immediately after setting the display object to nil.

6. generateIsland

To make the game more interesting, we generate an island every so often, and move it down the screen to give the appearance of the plane flying over the islands. Add the following code snippet for the generateIsland function.

function generateIsland()
    local tempIsland = display.newImage("island1.png", math.random(0,display.contentWidth - islandWidth),-islandHeight)
    table.insert(islands,tempIsland)
    islandGroup:insert( tempIsland )
end

We make use of the newImage method once again and position the island by setting a negative value for the islandHeight. For the x position, we use the math.random method  to generate a number between 0 and the display's contentWidth minus the islandWidth. The reason we subtract the width of the island is to make sure the island is completely on the screen. If we wouldn't subtract the island's width, there would be a chance that part of the island wouldn't be on the screen.

We need to start a timer to generate an island every so often. Add the following snippet to the startTimers function we created earlier. As you can see, we are generating an island every five seconds. In the next step, we'll make the islands move.

function startTimers()
    firePlayerBulletTimer =    timer.performWithDelay(2000, firePlayerBullet ,-1)
    generateIslandTimer = timer.performWithDelay( 5000, generateIsland ,-1)
end

7.moveIslands

The implementation of moveIslands is nearly identical to the movePlayerBullets function. We check if the islands table contains any islands and, if it does, we loop through it and move each island a little bit.

function moveIslands()
if(#islands > 0) then
    for i=1, #islands do
        islands[i].y = islands[i].y + 3
   end
  end
end

8.checkIslandsOutOfBounds

Just like we check if the player's bullets have moved off-screen, we check if any of the islands had moved off-screen. The implementation of checkIslandsOutOfBounds should therefore look familiar to you. We check if the islands y position is greater than display.contentHeight and if it is, we know the island has moved off-screen and can therefore be removed.

function  checkIslandsOutOfBounds() 
    if(#islands > 0) then
        for i=#islands,1,-1 do
            if(islands[i].y > display.contentHeight) then
 	        islands[i]:removeSelf()
 		islands[i] = nil
 		table.remove(islands,i)
 	end
      end
  end
end

9. generateFreeLife

Every so often, the player has a chance to get a free life. We first generate a free life image and if the player collides with the image they get an extra life. The player can have a maximum of six lives.

function generateFreeLife ()
    if(numberOfLives >= 6) then
	return
   end
   local freeLife = display.newImage("newlife.png", math.random(0,display.contentWidth - 40), 0);
   table.insert(freeLifes,freeLife)
   planeGroup:insert(freeLife)
end 

If the player already has six lives, we do nothing by returning early from the function. If not, we create a new life image and add it to the screen. Similar to how we positioned the islands earlier, we set the image at a negative y position and generate a random value for the image's x position. We then insert it into the freeLifes table to be able to reference it later.

We need to call this function every so often. Add the following snippet to the startTimers function.

function startTimers()
    firePlayerBulletTimer =    timer.performWithDelay(2000, firePlayerBullet ,-1)
    generateIslandTimer = timer.performWithDelay( 5000, generateIsland ,-1)
    generateFreeLifeTimer = timer.performWithDelay(7000,generateFreeLife, - 1)
end

10.moveFreeLives

The implementation of moveFreeLifes should look familiar. We are looping through the freeLifes table and move every image in it.

function moveFreeLifes()
    if(#freeLifes > 0) then
	for i=1,#freeLifes do
	    freeLifes[i].y = freeLifes[i].y  +5
	end
   end
end

All we need to do is call moveFreeLifes in the gameLoop function.

function gameLoop()
    --SNIP--
    checkIslandsOutOfBounds()
    moveFreeLifes()
end

11. checkFreeLifesOutOfBounds

The following code snippet should also look familiar to you by now. We check if any of the images in the freeLifes table have moved off-screen and remove the ones that have.

function  checkFreeLifesOutOfBounds() 
    if(#freeLifes > 0) then
        for i=#freeLifes,1,-1 do
            if(freeLifes[i].y > display.contentHeight) then
 	        freeLifes[i]:removeSelf()
 		freeLifes[i] = nil
 		table.remove(freeLifes,i)
 	   end
 	end
     end
 end

We call this function in the gameLoop function.

function gameLoop()
    --SNIP--
    checkIslandsOutOfBounds()
    moveFreeLifes()
    checkFreeLifesOutOfBounds()
end

12. hasCollided

We need to be able to tell when game objects collide with each other, such as the player's plane and the free life images, the bullets and the planes, etc. While Corona offers a very robust physics engine that can easily handle collisions between display objects for us, doing so adds a bit of overhead with the calculations the engine has to do every frame.

For the purposes of this game, we will be using a simple bounding box collision detection system. What this function does, is make sure the rectangles or bounding boxes around two objects don't overlap. If they do, the objects are colliding. This logic is implemented in the hasCollided function.

function hasCollided( obj1, obj2 )
   if ( obj1 == nil ) then 
      return false
   end
   if ( obj2 == nil ) then  
      return false
   end

   local left = obj1.contentBounds.xMin <= obj2.contentBounds.xMin and obj1.contentBounds.xMax >= obj2.contentBounds.xMin
   local right = obj1.contentBounds.xMin >= obj2.contentBounds.xMin and obj1.contentBounds.xMin <= obj2.contentBounds.xMax
   local up = obj1.contentBounds.yMin <= obj2.contentBounds.yMin and obj1.contentBounds.yMax >= obj2.contentBounds.yMin
   local down = obj1.contentBounds.yMin >= obj2.contentBounds.yMin and obj1.contentBounds.yMin <= obj2.contentBounds.yMax

   return (left or right) and (up or down)
end

I found this code snippet on the CoronaLabs website. It works really well, because the game objects in our game are rectangular. If you're working with object that aren't rectangular, then you better take advantage of Corona's physics engine as its collision detection is very well optimized for this.

13. checkPlayerCollidesWithFreeLife

We want to check if the player's plane has collided with a free life object. If it has, then we award the player a free life.

function checkPlayerCollidesWithFreeLife() 
    if(#freeLifes > 0) then
        for i=#freeLifes,1,-1 do
	     if(hasCollided(freeLifes[i], player)) then
	        freeLifes[i]:removeSelf()
		freeLifes[i] = nil
		table.remove(freeLifes, i)
		numberOfLives = numberOfLives + 1
		hideLives()
		showLives()
	    end
	end
    end
end

In the checkPlayerCollidesWithFreeLife function, we loop through the freeLives table backwards for the same reason I described earlier. We call the hasCollided function and pass in the current image and the player's plane. If the two object collide, we remove the free life image, increment the numberOfLives variable, and call the hideLives and showLives function.

We invoke this function in the gameLoop function.

function gameLoop()
    --SNIP--
    moveFreeLifes()
    checkFreeLifesOutOfBounds()
    checkPlayerCollidesWithFreeLife()
end

14. hideLives

The hideLives function loops through the livesImages table and sets the isVisible property of each life image to false.

function hideLives()
    for i=1, 6 do
        livesImages[i].isVisible = false
   end
end

15. showLives

The showLives function loops through the livesImages table and sets each image's isVisible property to true.

function showLives()
    for i=1, numberOfLives do
        livesImages[i].isVisible = true;
    end
end

Conclusion

This brings the third part of this series to a close. In the next and final installment of this series, we will create enemy planes and finalized the game's gameplay. Thanks for reading and see you there.

2014-07-16T16:55:38.000Z2014-07-16T16:55:38.000ZJames Tyner

Build A Custom Launcher on Android

$
0
0
Final product image
What You'll Be Creating

Introduction

In its most basic form, a launcher is an application that does the following:

  • it represents the home screen of a device
  • it lists and launches applications that are installed on the device

In other words, it is the application that shows up when you press the home button. Unless you've already installed a custom launcher, you are currently using the default launcher that comes with your Android installation. A lot of device manufacturers have their own default, custom launchers that conform to their proprietary look and feel, for example, Samsung TouchWiz and HTC Sense.

In this tutorial, we are going to create a simple launcher with a basic user interface. It will have two screens:

  • a home screen showing the device's wallpaper
  • a screen showing the icons and details of the applications installed on the device

1. Requirements

You need to have the following installed and configured on your development machine:

  • Android SDK and platform tools
  • Eclipse IDE 3.7.2 or higher with the ADT plugin
  • an emulator or Android device running Android 2.2 or higher

You can download the SDK and platform tools the Android developer portal.

2. Project Setup

Launch Eclipse and create a new Android application project. I'm naming the application SimpleLauncher, but you can name it anything you want. Make sure you use a unique package. The lowest SDK version our launcher supports is Froyo and the target SDK is Jelly Bean.

Since we don't want to create an Activity, deselect Create Activity. Click Finish to continue.

3. Project Manifest

The next step is modifying the AndroidManifest.xml file by adding two activities. The first Activity displays the home screen. Let's name it HomeActivity as shown below.

<activity
    android:name="ah.hathi.simplelauncher.HomeActivity"
    android:label="Simple Launcher Home"
    android:theme="@android:style/Theme.Wallpaper.NoTitleBar.Fullscreen"
    android:launchMode="singleTask"
    android:stateNotNeeded="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.HOME" /><category android:name="android.intent.category.DEFAULT" /></intent-filter>         </activity>

By adding the categories android.intent.category.HOME and android.intent.category.DEFAULT to the intent-filter group, the associated Activity behaves like a launcher and shows up as an option when you press the device's home button.

We also need to set the launchMode to singleTask to make sure that only one instance of this Activity is held by the system at any time. To show the user's wallpaper, set the theme to Theme.Wallpaper.NoTitleBar.FullScreen.

The second Activity we need to add displays the applications that are installed on the user's device. It's also responsible for launching applications. We don't need any special configuration for this Activity. Name it AppsListActivity.

<activity
    android:name="ah.hathi.simplelauncher.AppsListActivity"
    android:theme="@android:style/Theme.NoTitleBar.Fullscreen">            </activity>

4. Activity Layouts

Create an XML file for the HomeActivity class in the project's res/layout folder and name it activity_home.xml. The layout has a single Button that responds to click events. Clicking the button takes the user from the home screen to the list of applications.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".HomeActivity" ><Button
        android:id="@+id/apps_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_marginRight="10dp"
        android:layout_marginTop="10dp"
        android:text="Show Apps"
        android:onClick="showApps"
        /></RelativeLayout>

Next, create an XML file for the AppsListActivity class in the project's res/layout folder and name it activity_apps_list.xml. The layout contains a ListView that takes up the entire screen.

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" ><ListView
        android:id="@+id/apps_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent">        </ListView>    </LinearLayout>

Finally, create a third XML file in the same location and name it list_item.xml. This file defines the layout of an item in the ListView. Each list view item represents an application installed on the user's device. It shows the application's icon, label, and package name. We display the application icon using an ImageView instance and TextView instances for the label and package name.

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp"><ImageView
        android:id="@+id/item_app_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"        
        /><TextView 
        android:id="@+id/item_app_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/item_app_icon"
        android:paddingLeft="10dp"
        /><TextView 
        android:id="@+id/item_app_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/item_app_label"
        android:layout_toRightOf="@+id/item_app_icon"
        android:paddingLeft="10dp"
        /></RelativeLayout>

5. Implementing the Activity Classes

HomeActivity

With the layouts of the application created, it's time to create the two Activity classes. When creating the two classes, make sure the name of each class matches the one you specified in the project's manifest file earlier.

Create a new class namedHomeActivity and set android.app.Activity as its superclass.

package ah.hathi.simplelauncher;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class HomeActivity extends Activity {
    
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_home);
	}
	
	public void showApps(View v){
		Intent i = new Intent(this, AppsListActivity.class);
		startActivity(i);
	}
}

In the class's onCreate method, we invoke setContentView, passing in the layout we created earlier. You may remember that we added a button to the activity_home layout that triggers a method named showApps. We now need to implement that method in the HomeActivity class. The implementation is pretty simple, we create an Intent for the AppsListActivity class and start it.

AppsListActivity

Create another Activity class named AppsListActivity and set android.app.Activity as its superclass. In the class's onCreate method, we invoke setContentView, passing in the activity_apps_list layout we created earlier.

package ah.hathi.simplelauncher;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class AppsListActivity extends Activity {
    
	@Override
	protected void onCreate(Bundle savedInstanceState) {	
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_apps_list);
	}
		
}

Even though our launcher isn't finished yet, you can save and run your application at this point. When you press the device's home button, you should see a pop-up asking you which launcher you'd like to use.

If you choose Simple Launcher Home, you should see your new home screen with a single button in the top right corner of the screen. You should also see your device's current wallpaper.

Go back to Eclipse and create a class named AppDetail that will contain the details of an application, its package name, label, and application icon. The interface is pretty basic as you can see below.

package ah.hathi.simplelauncher;

import android.graphics.drawable.Drawable;

public class AppDetail {
        CharSequence label;
        CharSequence name;
        Drawable icon;
}

6. Fetching Applications

In the loadApps method of the AppsListActivity class, we use the queryIntentActivities method of the PackageManager class to fetch all the Intents that have a category of Intent.CATEGORY_LAUNCHER. The query returns a list of the applications that can be launched by a launcher. We loop through the results of the query and add each item to a list named apps. Take a look at the following code snippet for clarification.

private PackageManager manager;
private List<AppDetail> apps;	
private void loadApps(){
	manager = getPackageManager();
	apps = new ArrayList<AppDetail>();
	Intent i = new Intent(Intent.ACTION_MAIN, null);
	i.addCategory(Intent.CATEGORY_LAUNCHER);
	List<ResolveInfo> availableActivities = manager.queryIntentActivities(i, 0);
	for(ResolveInfo ri:availableActivities){
		AppDetail app = new AppDetail();
		app.label = ri.loadLabel(manager);
		app.name = ri.activityInfo.packageName;
		app.icon = ri.activityInfo.loadIcon(manager);
		apps.add(app);
	}
}

7. Displaying the List of Applications

With the apps variable containing all the details we need, we can show the list of applications using the ListView class. We create a simple ArrayAdapter and override its getView method to render the list's items. We then associate the ListView with the adapter.

private ListView list;    
private void loadListView(){
	list = (ListView)findViewById(R.id.apps_list);
	ArrayAdapter<AppDetail> adapter = new ArrayAdapter<AppDetail>(this, 
			R.layout.list_item, 
			apps) {
		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			if(convertView == null){
				convertView = getLayoutInflater().inflate(R.layout.list_item, null);
			}
			ImageView appIcon = (ImageView)convertView.findViewById(R.id.item_app_icon);
			appIcon.setImageDrawable(apps.get(position).icon);
			TextView appLabel = (TextView)convertView.findViewById(R.id.item_app_label);
			appLabel.setText(apps.get(position).label);
			TextView appName = (TextView)convertView.findViewById(R.id.item_app_name);
			appName.setText(apps.get(position).name);
			return convertView;
		}
	};
	list.setAdapter(adapter);			
}

8. Listening for Clicks

When the user clicks an item in the ListView, the corresponding application should be launched by our launcher. We use the getLaunchIntentForPackage method of the PackageManager class to create an Intent with which we start the application. Take a look at the following code snippet.

private void addClickListener(){    	
	list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
		@Override
		public void onItemClick(AdapterView<?> av, View v, int pos,
				long id) {
			Intent i = manager.getLaunchIntentForPackage(apps.get(pos).name.toString());
			AppsListActivity.this.startActivity(i);
		}
	});
}

9. Putting It All Together

To make everything work together, we need to invoke loadApps, loadListView, and addClickListener in the onCreate method of the AppsListActivity class as shown below.

protected void onCreate(Bundle savedInstanceState) {	
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_apps_list);
	loadApps();
	loadListView();
	addClickListener();
}

Build and run your application once more to see the result. You should now be able to see the applications that can be launched when you click the button on the home screen of our launcher. Click on an item to launch the corresponding application.

Conclusion

You now have your own custom launcher. It's very basic, but you can add all the customizations you want. If you want to dig deeper into custom launchers, I encourage you to take a look at the sample applications on the Android Developer Portal.

2014-07-18T15:25:52.000Z2014-07-18T15:25:52.000ZAshraff Hathibelagal

Build A Custom Launcher on Android

$
0
0
tag:code.tutsplus.com,2005:PostPresenter/cms-21358
Final product image
What You'll Be Creating

Introduction

In its most basic form, a launcher is an application that does the following:

  • it represents the home screen of a device
  • it lists and launches applications that are installed on the device

In other words, it is the application that shows up when you press the home button. Unless you've already installed a custom launcher, you are currently using the default launcher that comes with your Android installation. A lot of device manufacturers have their own default, custom launchers that conform to their proprietary look and feel, for example, Samsung TouchWiz and HTC Sense.

In this tutorial, we are going to create a simple launcher with a basic user interface. It will have two screens:

  • a home screen showing the device's wallpaper
  • a screen showing the icons and details of the applications installed on the device

1. Requirements

You need to have the following installed and configured on your development machine:

  • Android SDK and platform tools
  • Eclipse IDE 3.7.2 or higher with the ADT plugin
  • an emulator or Android device running Android 2.2 or higher

You can download the SDK and platform tools the Android developer portal.

2. Project Setup

Launch Eclipse and create a new Android application project. I'm naming the application SimpleLauncher, but you can name it anything you want. Make sure you use a unique package. The lowest SDK version our launcher supports is Froyo and the target SDK is Jelly Bean.

Since we don't want to create an Activity, deselect Create Activity. Click Finish to continue.

3. Project Manifest

The next step is modifying the AndroidManifest.xml file by adding two activities. The first Activity displays the home screen. Let's name it HomeActivity as shown below.

<activity
    android:name="ah.hathi.simplelauncher.HomeActivity"
    android:label="Simple Launcher Home"
    android:theme="@android:style/Theme.Wallpaper.NoTitleBar.Fullscreen"
    android:launchMode="singleTask"
    android:stateNotNeeded="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.HOME" /><category android:name="android.intent.category.DEFAULT" /></intent-filter>         </activity>

By adding the categories android.intent.category.HOME and android.intent.category.DEFAULT to the intent-filter group, the associated Activity behaves like a launcher and shows up as an option when you press the device's home button.

We also need to set the launchMode to singleTask to make sure that only one instance of this Activity is held by the system at any time. To show the user's wallpaper, set the theme to Theme.Wallpaper.NoTitleBar.FullScreen.

The second Activity we need to add displays the applications that are installed on the user's device. It's also responsible for launching applications. We don't need any special configuration for this Activity. Name it AppsListActivity.

<activity
    android:name="ah.hathi.simplelauncher.AppsListActivity"
    android:theme="@android:style/Theme.NoTitleBar.Fullscreen">            </activity>

4. Activity Layouts

Create an XML file for the HomeActivity class in the project's res/layout folder and name it activity_home.xml. The layout has a single Button that responds to click events. Clicking the button takes the user from the home screen to the list of applications.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".HomeActivity" ><Button
        android:id="@+id/apps_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_marginRight="10dp"
        android:layout_marginTop="10dp"
        android:text="Show Apps"
        android:onClick="showApps"
        /></RelativeLayout>

Next, create an XML file for the AppsListActivity class in the project's res/layout folder and name it activity_apps_list.xml. The layout contains a ListView that takes up the entire screen.

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" ><ListView
        android:id="@+id/apps_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent">        </ListView>    </LinearLayout>

Finally, create a third XML file in the same location and name it list_item.xml. This file defines the layout of an item in the ListView. Each list view item represents an application installed on the user's device. It shows the application's icon, label, and package name. We display the application icon using an ImageView instance and TextView instances for the label and package name.

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp"><ImageView
        android:id="@+id/item_app_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"        
        /><TextView 
        android:id="@+id/item_app_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/item_app_icon"
        android:paddingLeft="10dp"
        /><TextView 
        android:id="@+id/item_app_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/item_app_label"
        android:layout_toRightOf="@+id/item_app_icon"
        android:paddingLeft="10dp"
        /></RelativeLayout>

5. Implementing the Activity Classes

HomeActivity

With the layouts of the application created, it's time to create the two Activity classes. When creating the two classes, make sure the name of each class matches the one you specified in the project's manifest file earlier.

Create a new class namedHomeActivity and set android.app.Activity as its superclass.

package ah.hathi.simplelauncher;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class HomeActivity extends Activity {
    
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_home);
	}
	
	public void showApps(View v){
		Intent i = new Intent(this, AppsListActivity.class);
		startActivity(i);
	}
}

In the class's onCreate method, we invoke setContentView, passing in the layout we created earlier. You may remember that we added a button to the activity_home layout that triggers a method named showApps. We now need to implement that method in the HomeActivity class. The implementation is pretty simple, we create an Intent for the AppsListActivity class and start it.

AppsListActivity

Create another Activity class named AppsListActivity and set android.app.Activity as its superclass. In the class's onCreate method, we invoke setContentView, passing in the activity_apps_list layout we created earlier.

package ah.hathi.simplelauncher;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class AppsListActivity extends Activity {
    
	@Override
	protected void onCreate(Bundle savedInstanceState) {	
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_apps_list);
	}
		
}

Even though our launcher isn't finished yet, you can save and run your application at this point. When you press the device's home button, you should see a pop-up asking you which launcher you'd like to use.

If you choose Simple Launcher Home, you should see your new home screen with a single button in the top right corner of the screen. You should also see your device's current wallpaper.

Go back to Eclipse and create a class named AppDetail that will contain the details of an application, its package name, label, and application icon. The interface is pretty basic as you can see below.

package ah.hathi.simplelauncher;

import android.graphics.drawable.Drawable;

public class AppDetail {
        CharSequence label;
        CharSequence name;
        Drawable icon;
}

6. Fetching Applications

In the loadApps method of the AppsListActivity class, we use the queryIntentActivities method of the PackageManager class to fetch all the Intents that have a category of Intent.CATEGORY_LAUNCHER. The query returns a list of the applications that can be launched by a launcher. We loop through the results of the query and add each item to a list named apps. Take a look at the following code snippet for clarification.

private PackageManager manager;
private List<AppDetail> apps;	
private void loadApps(){
	manager = getPackageManager();
	apps = new ArrayList<AppDetail>();
	Intent i = new Intent(Intent.ACTION_MAIN, null);
	i.addCategory(Intent.CATEGORY_LAUNCHER);
	List<ResolveInfo> availableActivities = manager.queryIntentActivities(i, 0);
	for(ResolveInfo ri:availableActivities){
		AppDetail app = new AppDetail();
		app.label = ri.loadLabel(manager);
		app.name = ri.activityInfo.packageName;
		app.icon = ri.activityInfo.loadIcon(manager);
		apps.add(app);
	}
}

7. Displaying the List of Applications

With the apps variable containing all the details we need, we can show the list of applications using the ListView class. We create a simple ArrayAdapter and override its getView method to render the list's items. We then associate the ListView with the adapter.

private ListView list;    
private void loadListView(){
	list = (ListView)findViewById(R.id.apps_list);
	ArrayAdapter<AppDetail> adapter = new ArrayAdapter<AppDetail>(this, 
			R.layout.list_item, 
			apps) {
		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			if(convertView == null){
				convertView = getLayoutInflater().inflate(R.layout.list_item, null);
			}
			ImageView appIcon = (ImageView)convertView.findViewById(R.id.item_app_icon);
			appIcon.setImageDrawable(apps.get(position).icon);
			TextView appLabel = (TextView)convertView.findViewById(R.id.item_app_label);
			appLabel.setText(apps.get(position).label);
			TextView appName = (TextView)convertView.findViewById(R.id.item_app_name);
			appName.setText(apps.get(position).name);
			return convertView;
		}
	};
	list.setAdapter(adapter);			
}

8. Listening for Clicks

When the user clicks an item in the ListView, the corresponding application should be launched by our launcher. We use the getLaunchIntentForPackage method of the PackageManager class to create an Intent with which we start the application. Take a look at the following code snippet.

private void addClickListener(){    	
	list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
		@Override
		public void onItemClick(AdapterView<?> av, View v, int pos,
				long id) {
			Intent i = manager.getLaunchIntentForPackage(apps.get(pos).name.toString());
			AppsListActivity.this.startActivity(i);
		}
	});
}

9. Putting It All Together

To make everything work together, we need to invoke loadApps, loadListView, and addClickListener in the onCreate method of the AppsListActivity class as shown below.

protected void onCreate(Bundle savedInstanceState) {	
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_apps_list);
	loadApps();
	loadListView();
	addClickListener();
}

Build and run your application once more to see the result. You should now be able to see the applications that can be launched when you click the button on the home screen of our launcher. Click on an item to launch the corresponding application.

Conclusion

You now have your own custom launcher. It's very basic, but you can add all the customizations you want. If you want to dig deeper into custom launchers, I encourage you to take a look at the sample applications on the Android Developer Portal.

2014-07-18T15:25:52.000Z2014-07-18T15:25:52.000ZAshraff Hathibelagal

An Introduction to Xamarin: Part 3

$
0
0

1. Before Getting Started

Hopefully to this point, you have been following along with these introductory tutorials on what Xamarin is and how to use it. If you have, then you are primed and ready for the next step in your Xamarin training, cross-platform applications. If not, then head back and take a look at the previous tutorials to get a better grasp on how this fantastic technology works. Once you have that basic knowledge, you can come back and follow along.

2. Enter the Misconception

Several years ago, when many of the cross-platform solutions started to emerge, so did the misconception of Write Once Run Anywhere or WORA. Many developers started to salivate at the thought of being able to write their applications in a single language and have it run on various mobile platforms. The idea isn't incorrect, it merely was slightly misinformed in the beginning.

The dream of WORA comes true when you are considering the common logic of your application. What is considered common logic? Nearly anything that doesn't have to do with the user interface. This could mean your domain objects or models, data access layer, service layer, etc. All of these components are kept separate from the user interface. They are the pieces of the puzzle that make your application important to users.

The dreams of WORA start to break down when you begin looking at the cosmetic aspects of your app. At this point you start dealing with different SDKs with different ways of describing user interface elements. A button is no longer just a button. A button is now a Button object described using XAML and C# (among other possible languages) on Windows Phone. A button is a Button object described in AXML and Java on Android. A button is a UIButton instance described in a proprietary XML format and Objective-C on iOS.

Given the tools that you have at your disposal, the best you can do is create a separation of these user interface components from your functional code. This all starts to change with the introduction of Xamarin.Forms in the recent release of Xamarin 3, but we'll cover that in another tutorial.

When you want to create a cross-platform app in the Xamarin ecosystem, you will generally go with the formula of creating at least n+1 projects for each app that targets n platforms. Why at least? Why n+1? The n+1 is the easy part. The generally accepted way is to divide your application up into separate projects like this:

  • App.Core
  • App.iOS (Xamarin.iOS project)
  • App.Android (Xamarin.Android project)

You will want to create a specific project for each platform. In your case you would create a Xamarin.iOS project and a Xamarin.Android project. Best practices would tell you to append the name of the platform onto the end of the project name just to make the breakdown more obvious. The third project is the +1, the project in which you keep all of your platform independent code that can be reused by other projects in your solution or in other solutions.

The at least part is more of a personal preference. Depending on how you typically like to break up your code, the third project (Core) could either be one class library or several. It is completely up to you. That is where you get to flex your creative muscle. But it isn't just as easy as creating a new class library project and banging out some code. If it were that easy, everyone would be doing it. Luckily, you have some tried and true options for sharing this code.

3. Sharing Code

If you want to write code in C# and share it across multiple platforms in a Xamarin project, you have three basic options:

  • File Linking
  • Portable Class Libraries
  • Shared Projects

As Xamarin continues to evolve, File Linking tends to fall off the map, but we'll discuss every option separately to let you choose the most appropriate one.

File Linking

File linking is a viable option in Xamarin Studio and has been around in Visual Studio for quite some time. It's an option that allows you to write code in one physical file and have all other projects have access to that one file. This means that any changes you make to the linked file, will cause changes in the original file. This is because there's really only one file.

The opposite approach of file linking is to create copies of the original file in any other project in your solution that needs access to that particular code. That way, when you make changes to these files in any of the projects, the changes are restricted to that physical file and not the original.

How To Link Files

The process of linking files to different projects in your solution is very similar, whether you're using Xamarin Studio or Visual Studio. Let's start by creating a new class library project in either IDE.

For this example, name your first project SourceLibrary. Within this class library, create a new class and name it User. Replace the class's implementation with the following:

public class User
{
	public string FirstName { get; set; }
	public string LastName { get; set; }
}

You now have a basic class that represents a User within your application. If you wanted to share that concept with another project within your solution, you could add a link. First, you need to create a new class library project within your solution. Let's name this one LinkLibrary. After creating the project, you can add a link to your User class by right-clicking your LinkLibrary project and selecting Add Files in Xamarin Studio or Add Existing Files in Visual Studio. Navigate to the file you want to link to in the Open File dialog box. When you click the Open button in Xamarin Studio you will be presented with a dialog box that looks like this:

To add a link, select the last option, Add a link to the file, and click OK. You have now successfully created a link to the original file.

In Visual Studio, when you find the file you want to link to in the Open File dialog, you'll notice that there's a little drop-down menu on the right edge of the Add button. If you click that menu, you'll be presented with a couple of options:

  • Add, the default option that creates a copy of this file in the current project
  • Add As Link, creates a link to the original file in the current project

After clicking Add As Link, Visual Studio will create a link to the User file in your LinkLibrary project. Once you create a link to a file, you will see that linked file in your current solution with a strange icon in the corner to signify that it's a linked file.

Now, edit the User class in your LinkLibrary project to look like this.

public class User
{
	public string FirstName { get; set; }
	public string LastName { get; set; }
	public string UserName { get; set; }
}

After saving the changes and navigating back to the User class in your SourceLibrary project, you should see that both versions have changed.

This may seem like a lot of work. Why not create all this shared logic in a typical class library project and add references to this assembly from your platform specific assemblies? The answer is that you can't.

When you create an iOS or Android application from the Xamarin.iOS or Xamarin.Android project templates, you're very limited to the assemblies that you can reference. The assemblies that are created during this process are restricted in the functionality and libraries that are supported. The problem is that, by default, the typical class library project contains access to way more functionality than the Xamarin libraries support. Fear not, there are some workarounds.

Portable Class Libraries

Portable Class Libraries or PCLs are the next evolution in creating cross-platform code in the world of .NET. PCLs are stripped down versions of class library projects that only contain bare-bones functionality capable of being referenced and run, without modification, in several different types of projects. The list of supported platforms and features continues to grow. To get more detail on PCLs and to see the supported platforms and features, take a look at the Portable Class Library page on MSDN.

Generally speaking, the way that PCLs work is by creating all of your shared logic in a PCL project and then adding references to that project from your platform specific projects. The only difference in this process from creating a normal class library project is one extra step.

Creating a PCL

Start by creating a new project in Xamarin Studio or Visual Studio, select Class Library as the project template, and name the project TypicalLibrary. Next, add a new project to your solution. Select Class Library (Portable) as the template and name it PortableLibrary. Depending on your IDE, this process may differ slightly.

If you create your new Portable Class Library project in Visual Studio, a dialog similar to the one below will appear.

Depending on your version of Visual Studio, .NET Framework, and Xamarin tools installed, you may see different options, but the concepts are the same. You have the choice of selecting the different platforms you want to support. The combinations of platforms you choose here are only limited by your imagination. Just be sure to only select the platforms that you need. The more platforms you select, the more limited the range of features you can access within your PCL.

If you're using Xamarin Studio, you won't see the above dialog box. Xamarin Studio will set similar options for you by default. To change the supported platforms, right-click the PortableLibrary project and select Options. In the Project Options dialog, select General under the Build section. You should see a set of options similar to those shown below.

Since this is merely a demonstration, you can select any of the platforms that you like. To prove the point though, make sure that you have at least selected Xamarin.Android as one of the supported platforms.

Finally, add another project to your solution. Select the Android Application template and name the project BasicAndroidApplication. To use the functionality you've created in a class library, add a reference to it from your Android application. If you're using Xamarin Studio, you do this by right-clicking the References folder under your BasicAndroidApplication project and selecting Edit References. In the Edit References dialog, select the Projects tab to reference an assembly within your current solution. This is what you'll see:

You are able to select the PCL (PortableLibrary), but as you can see the normal class library (TypicalLibrary) is incompatible.

Attempting this exercise in Visual Studio will ultimately end in the same place, but Visual Studio is going to be a little less obvious. It will let you add a reference to the normal class library, but under the BasicAndroidApplication Refereces you'll see the TypicalLibrary listed with a warning icon.

When you build the project, you'll receive a warning in the Error List similar to the one shown below.

Even though this may seem a little nicer than Xamarin Studio just slamming the door in your face, but this will cause other issues down the road when you try to test and debug your application. You'll run into errors and other problems that may make debugging difficult, so you'll definitely want to be aware of these types of warnings when using a PCL.

Using PCLs

When using PCLs in your solution, there are very few differences with using normal class libraries. During development, when you want to use any classes defined within your PCL, you simply add a reference to it and away you go. During the build process, the output of a PCL is a DLL file, just like a normal class library.

When you create and reference PCLs in your project, that physical DLL file is included with the application, so the primary application assembly always has access to it. This may come as no surprise to you, but it is a distinguishing characteristic when comparing it with the third option, which we'll discuss now.

Shared Projects

Shared Projects and support for their corresponding project templates were added in Xamarin Studio 5 and Visual Studio 2013 Update 2. While the name is rather unassuming, these projects work quite differently than their class library and PCL cousins. These projects don't have the limitations that a PCL does. They have full access to built-in libraries and assemblies in the .NET Framework, with the exception that you're not able to add additional references and components to these projects.

Creating a Shared Project

Start by creating a new Solution/Project in Xamarin Studio or Visual Studio. Select C# as your language and select the Shared Project template. If you're using Visual Studio and you don't see this as an option, then you'll need to download and install the Shared Project Reference Manager extension. Once the installation is complete and you've restarted Visual Studio, you should see this option.

After creating the project, you'll notice that it's a bit empty. There are no Resources or Components folders. There are no Properties or default classes created. You just get a blank slate.

You'll also notice that there are fewer properties when you right-click your project and select Properties from the contextual menu.

The only choice you have for changing options in a Shared Project is the Root Namespace. The Root Namespace is the default namespace given to all classes created within your Shared Project so you don't have to manually update them.

Support for Compiler Directives

One of the important aspects of Shared Projects is their support for compiler directives. This is one of the reasons they fit so well into cross-platform projects. There are a number of directives that are included in Xamarin and Windows Phone projects.

  • __MOBILE__, defined in both Xamarin.iOS and Xamarin.Android projects
  • __IOS__, defined in Xamarin.iOS projects
  • __ANDROID__, defined in Xamarin.Android projects
  • WINDOWS_PHONE, defined in Windows Phone projects
  • SILVERLIGHT, defined in Windows Phone projects

In addition to these generic directives, Xamarin.Android provides a number of specific directives to target a specific version of the Android SDK. It's not uncommon to see a directive similar to __ANDROID_11__, for example. These directives can be used in conditional preprocessor statements to let the compiler know to only attempt to compile the code within them if the special directives are defined. Take a look at the following example.

public class User
{
#if __ANDROID__
    public string FirstName { get; set; }
#endif

    public string LastName { get; set; }
}

This may be a bit of a contrived example, but it serves a purpose. When the compiler comes across this block of code in a Xamarin.Android project, the User class will have both the FirstName and LastName properties. If you were building this in a Xamarin.iOS project, the User class would contain only the LastName property.

This process of conditional compilation comes in handy when dealing with platform specific differences. These differences could come into play when using device or platform specific features such as local storage. With directives, you have the ability to write platform specific code in one file as opposed to multiple files across multiple projects. Very nice.

Using Shared Projects

When you use Shared Projects in an application, there are a few things you need to look out for. You may have noticed when looking at the properties associated with a Shared Project that there were no choices for selecting a Target framework or Output type. That's because these little beauties aren't actually compiled in their own assemblies. Code that is written in a Shared Project is compiled into any project that references it.

What happens if you have a Shared Project in your solution that isn't referenced by any other projects? Nothing. The code is not compiled and subsequently not included anywhere. The same will happen if you add a reference to a Xamarin.iOS project that points to a Shared Project that contains the User class above. Since the __ANDROID__ directive is not defined within Xamarin.iOS, the FirstName property will not be compiled into the Xamarin.iOS project.

4. Sharing Code Guidelines

Based on the above strategies to share code among cross-platform applications, you have some decisions to make. The following discussion outlines the pros and cons of each strategy and presents a number of guidelines to help you with this decision. Keep in mind that there is no right or best strategy. Any of these options works, but one may be slightly more desirable over another.

File Linking

File Linking is the process of creating logical links from one project to a physical file in another project or directory.

Pros

  • only one physical file to maintain
  • code changes only need to be made once

Cons

  • changes made to linked files will effect all code referencing them
  • linking multiple files and directories is a multistep process

Portable Class Libraries

A PCL is a specialized class library project that can be included and run on several different platforms without modification.

Pros

  • generates an assembly that can be referenced by other projects
  • all refactoring and testing can be done in one location

Cons

  • cannot reference any non-PCL or platform specific assemblies
  • only includes a subset of features of the .NET Framework

Shared Projects

A Shared Project is a specialized project template enabling you to write code that is directly compiled into any project that references it. It allows the use of preprocessor directives to include all platform specific code to live in the same project.

Pros

  • can use directives to target specific platforms in a single file
  • have access to other referenced assemblies projects referencing them

Cons

  • there is no output file so code cannot be shared with projects outside the solution
  • refactorings inside inactive compiler directives will not update the project


Rules of Thumb

With the evolution of Xamarin and the introduction of the Shared Project strategy, the File Linking option has all but fallen to the wayside. That doesn't mean you can't or shouldn't use this option, it simply means that in most circumstances the Portable Class Library or Shared Project strategies may be the best fit. Here are simple scenarios for determining which one to go with.

  • Portable Class Library, creating a cross platform application in which the shared code is not platform specific and is intended to be shared or reused outside the current solution
  • Shared Project, creating a cross-platform application with code that can be platform specific and only belong in your solution and is not intended to be shared or reused outside that context

5. Let's Build An App

That's enough theory for now. Let's get write some code. In the previous two introductory tutorials on Xamarin, you created the same RSS reader. While that may have seemed redundant at the time, we did it for two reasons.

  1. show the similarities in creating Xamarin.iOS and Xamarin.Android applications
  2. lead up to merging common code in a single cross-platform application

Project Setup

To create this version of your application, you will follow the basic n+1 guideline. Start by creating a new iOS project using the Empty Project template, naming the project XamFeed.iOS and the solution XamFeed.

Next, add a new Android project to your solution using the Android Application project template. Name the new project XamFeed.Android.

Now it's time for the +1. You can choose any of the code sharing options described in this tutorial to create this application. Since the code that's going to be shared between the iOS and Android applications isn't specific to any one platform and may be useful to other projects and applications, I will take the Portable Class Library route. Add a new C# project to your solution using the Portable Library template and name it XamFeed.Core.

If you're using Visual Studio, you'll see a dialog box that lets you select the supported platforms. If you're using Xamarin Studio, you'll need to look into the project properties for the XamFeed.Core project. You can select just about any combination of platforms as long as you include .NET Framework 4.5 or laterXamarin.iOS, and Xamarin.Android, and make sure not to select Windows Phone 8 and Windows Phone Silverlight 8. The reason you can't choose those two platforms is because they don't support the HttpClient class that we'll use in our application.

You can delete the default class file in the XamFeed.Core library, because we won't be needing it. Finally, be sure to add a reference to the XamFeed.Core project in both the XamFeed.iOS and XamFeed.Android projects. You are now ready to add some code.

Adding Shared Code

We'll start by creating the domain object, RssItem. This class will contain four properties. You can add more if you like.

  • Title, the name of the article
  • PubDate, the date the article was published
  • Creator, the author of the article
  • Link, the URL of the article

Add a new class to your XamFeed.Core project named RssItem. Replace the default implementation with the following:

public class RssItem
{
    public string Title { get; set; }
    public string PubDate { get; set; }
    public string Creator { get; set; }
    public string Link { get; set; }
}

Create another class and name it FeedRetriever. The task of this class will be fetching an array of RssItem objects from a given URL. Once you've created this class, add a new method to it with the following implementation:

public async Task<RssItem[]> GetItems(string feedUrl) 
{
	using (var client = new HttpClient())
	{
		var xmlFeed = await client.GetStringAsync(feedUrl);
		var doc = XDocument.Parse(xmlFeed);
		XNamespace dc = "http://purl.org/dc/elements/1.1/";

		var items = (from item in doc.Descendants("item")
			select new RssItem
			{
				Title = item.Element("title").Value,
				PubDate = item.Element("pubDate").Value,
				Creator = item.Element(dc + "creator").Value,
				Link = item.Element("link").Value
			}).ToArray();

		return items;
	}
}

To get this method to compile into your project, you'll need to add four using statements to the top of the FeedRetriever file:

  • using System.Linq;
  • using System.Net.Http;
  • using System.Xml.Linq;
  • using System.Threading.Tasks;

You now have a handy little class that can be used to retrieve the articles from a given RSS feed URL. This functionality will be added to the iOS and Android applications. Let's start with the Android application.

XamFeed.Android

Putting the RssItem objects into a list requires a custom adapter. In the XamFeed.Android project, create a new class, name it FeedAdapter, and replace it's implementation with the following:

public class FeedAdapter : BaseAdapter<RssItem>
{
    private RssItem[] _items;
    private Activity _context;
    public FeedAdapter( Activity context, RssItem[] items) : base()
    {
        _context = context;
        _items = items;
    }
    public override RssItem this[int position]
    {
        get { return _items[position]; }
    }
    public override int Count
    {
        get { return _items.Count(); }
    }
    public override long GetItemId(int position)
    {
        return position;
    }
    public override View GetView(int position, View convertView, ViewGroup parent)
    {
        var view = convertView;
        if (view == null)
        {
            view = _context.LayoutInflater.Inflate(Android.Resource.Layout.SimpleListItem2, null);
        }
        view.FindViewById<TextView>(Android.Resource.Id.Text1).Text = _items[position].Title;
        view.FindViewById<TextView>(Android.Resource.Id.Text2).Text = string.Format("{0} on {1}", _items[position].Creator, _items[position].PubDate);
        return view;
    }
}

Depending on how you created this class, you may have to add some or all of the following using statements to the class. Double-check that they're all included.

  • using Android.Widget;
  • using XamFeed.Core;
  • using Android.App;
  • using Android.Views;
  • using Android.OS;
  • using System.Linq;

You now have a mechanism for retrieving RssItem objects and an Adapter to display them. All you need is a ListView. Replace the contents of Main.axml (Resources > layout) with the following:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"><ListView
        android:id="@+id/TutsFeedListView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" /></LinearLayout>

The above layout adds a ListView to the screen, so you can populate it with data. All that's left for us to do is wiring up the MainActivity class to the layout, populate the list with RssItem objects, open each of the links when one of the items is tapped by the user. To accomplish this, you only need to modify the contents of MainActivity.cs as shown below.

[Activity(Label = "XamFeed", MainLauncher = true, Icon = "@drawable/icon")]
public class FeedActivity : ListActivity
{
	private FeedRetriever _retriever;
	private RssItem[] _items;

	protected async override void OnCreate(Bundle bundle)
	{
		base.OnCreate(bundle);          
		_retriever = new FeedRetriever ();

		_items = await _retriever.GetItems ("http://blog.xamarin.com/feed");

		ListAdapter = new FeedAdapter (this, _items);

	}

	protected override void OnListItemClick(ListView l, View v, int position, long id)
	{
		base.OnListItemClick(l, v, position, id);
		var uri = global::Android.Net.Uri.Parse (_items [position].Link);

		var intent = new Intent (Intent.ActionView, uri);
		StartActivity(intent);
	}
}

You will have to add a using statement to include the XamFeed.Core namespace to get this to compile.

There you have it. You now have a fully functional Android application that is utilizing core functionality that resides in a Portable Class Library. Now it's time to turn our attention to the XamFeed.iOS project.

XamFeed.iOS

The basic process of getting the iOS project up and running is going to be similar to the Android project. We'll use the FeedRetriever class to get the RssItem objects and then write custom user interface code to display it to the user. To accomplish this, we'll create a view controller containing a list of RssItem objects and set that view controller as the root view controller of the application.

Start by adding a new iOS Table View Controller to the XamFeed.iOS project and name it RssItemView. This results in the creation of the RssItemViewController, RssItemViewSource, and RssItemViewCell classes.

These classes make up the table view that will display the RssItem objects. Replace the default implementation of the RssItemViewController class with the following:

public class RssItemViewController : UITableViewController
{
	private FeedRetriever _retriever;
	private RssItem[] _items;
	public RssItemViewController () : base ()
	{
		_retriever = new FeedRetriever ();
	}

	public override void DidReceiveMemoryWarning ()
	{
		// Releases the view if it doesn't have a superview.
		base.DidReceiveMemoryWarning ();

		// Release any cached data, images, etc that aren't in use.
	}

	public async override void ViewDidLoad ()
	{
		base.ViewDidLoad ();
		_items = await _retriever.GetItems ("http://blog.xamarin.com/feed");
		// Register the TableView's data source
		TableView.Source = new RssItemViewSource (_items);
		TableView.ReloadData ();

	}
}

In the ViewDidLoad method, we use the FeedRetriever class to get the RssItem objects from the Xamarin blog, assign them to the RssItemViewSource object, and call the ReloadData method on the TableView instance. This method ensures that the table view is refreshed after it is shown, so that the data appears on the screen.

Next, update the RssItemViewSource class to handle the RssItem[] that we're passing to it. To do that, replace the default implementation with the following:

public class RssItemViewSource : UITableViewSource
{
	private RssItem[] _items;

	public RssItemViewSource (RssItem[] items)
	{
		_items = items;
	}

	public override int NumberOfSections (UITableView tableView)
	{
		// TODO: return the actual number of sections
		return 1;
	}

	public override int RowsInSection (UITableView tableview, int section)
	{
		// TODO: return the actual number of items in the section
		return _items.Length;
	}

	public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
	{
		var cell = tableView.DequeueReusableCell (RssItemViewCell.Key) as RssItemViewCell;
		if (cell == null)
			cell = new RssItemViewCell ();

		// TODO: populate the cell with the appropriate data based on the indexPath

		cell.TextLabel.Text = _items[indexPath.Row].Title;
		cell.DetailTextLabel.Text = string.Format ("{0} on {1}", _items [indexPath.Row].Creator, _items [indexPath.Row].PubDate);

		return cell;
	}

	public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
	{
		var item = _items [indexPath.Row];

		var url = new NSUrl (item.Link);
		UIApplication.SharedApplication.OpenUrl (url);
	}
}

Most of the work here is done in the GetCell method. This is where we either create a new cell if one doesn't exist or reuse one that has gone out of view. Once we have a cell, we populate it with the Title, Creator, and PubDate properties of the corresponding RssItem object.

Lastly, we need to define the look of the cells within the UITableView. For that, we use the RssItemViewCell class. Replace the default implementation with the following:

public class RssItemViewCell : UITableViewCell
{
	public static readonly NSString Key = new NSString ("FeedItemViewControllerCell");

	public RssItemViewCell () : base (UITableViewCellStyle.Subtitle, Key)
	{
		// TODO: add subviews to the ContentView, set various colors, etc.
		TextLabel.Text = "TextLabel";
	}
}

You may have noticed that it looks almost exactly like the default implementation with only one minor change, we pass a UITableViewCell.Subtitle to the base constructor. This gives each cell two text fields that you can populate.

We now have everything we need to show the list of RssItem objects to the user. There's only one problem. We haven't added the view of the view controller to the view hierarchy of our application. This is easy to fix though. Update the FinishedLaunching method in the AppDelegate class as shown below.

public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
	// create a new window instance based on the screen size
	window = new UIWindow (UIScreen.MainScreen.Bounds);
	var controller = new RssItemViewController ();
	controller.View.BackgroundColor = UIColor.White;
	controller.Title = "Xamarin Feeds";

	var navController = new UINavigationController (controller);
	window.RootViewController = navController;
	// If you have defined a root view controller, set it here:
	// window.RootViewController = myViewController;
	
	// make the window visible
	window.MakeKeyAndVisible ();
	
	return true;
}

We first create a new instance of the RssItemViewController and set a couple of its properties. We then create a new UINavigationViewController instance, passing it the view controller we created a moment ago. Finally, we set the new navigation controller as the RootViewController of the UIWindow instance. That's it.

We now have an Android and an iOS application that displays the RSS feed of the Xamarin blog. You can run these applications one at a time and see them work individually, or you can run them both in one debugging session. To do this, you just need to go into your XamFeed solution properties or options, depending on your IDE. In here, you can choose which projects you want to start up when you run the application. In your solution, select both XamFeed.Android and XamFeed.iOS.

Save the changes and now, when you run your application, both the iOS Simulator and Android Emulator start up and run your application.

Conclusion

Congratulations. You've successfully created a cross-platform mobile application with a single code base. It's important to understand that creating these applications doesn't necessarily coincide with the Write Once Run Anywhere mindset. There will always be customization needed. Luckily, with a combination of Xamarin and some very cool code sharing strategies, you can get very close. If you simply start to think of your mobile apps in terms of platform specific code and platform agnostic code, you are well on your way.

2014-07-21T16:45:37.000Z2014-07-21T16:45:37.000ZDerek Jensen

An Introduction to Xamarin: Part 3

$
0
0
tag:code.tutsplus.com,2005:PostPresenter/cms-21435

1. Before Getting Started

Hopefully to this point, you have been following along with these introductory tutorials on what Xamarin is and how to use it. If you have, then you are primed and ready for the next step in your Xamarin training, cross-platform applications. If not, then head back and take a look at the previous tutorials to get a better grasp on how this fantastic technology works. Once you have that basic knowledge, you can come back and follow along.

2. Enter the Misconception

Several years ago, when many of the cross-platform solutions started to emerge, so did the misconception of Write Once Run Anywhere or WORA. Many developers started to salivate at the thought of being able to write their applications in a single language and have it run on various mobile platforms. The idea isn't incorrect, it merely was slightly misinformed in the beginning.

The dream of WORA comes true when you are considering the common logic of your application. What is considered common logic? Nearly anything that doesn't have to do with the user interface. This could mean your domain objects or models, data access layer, service layer, etc. All of these components are kept separate from the user interface. They are the pieces of the puzzle that make your application important to users.

The dreams of WORA start to break down when you begin looking at the cosmetic aspects of your app. At this point you start dealing with different SDKs with different ways of describing user interface elements. A button is no longer just a button. A button is now a Button object described using XAML and C# (among other possible languages) on Windows Phone. A button is a Button object described in AXML and Java on Android. A button is a UIButton instance described in a proprietary XML format and Objective-C on iOS.

Given the tools that you have at your disposal, the best you can do is create a separation of these user interface components from your functional code. This all starts to change with the introduction of Xamarin.Forms in the recent release of Xamarin 3, but we'll cover that in another tutorial.

When you want to create a cross-platform app in the Xamarin ecosystem, you will generally go with the formula of creating at least n+1 projects for each app that targets n platforms. Why at least? Why n+1? The n+1 is the easy part. The generally accepted way is to divide your application up into separate projects like this:

  • App.Core
  • App.iOS (Xamarin.iOS project)
  • App.Android (Xamarin.Android project)

You will want to create a specific project for each platform. In your case you would create a Xamarin.iOS project and a Xamarin.Android project. Best practices would tell you to append the name of the platform onto the end of the project name just to make the breakdown more obvious. The third project is the +1, the project in which you keep all of your platform independent code that can be reused by other projects in your solution or in other solutions.

The at least part is more of a personal preference. Depending on how you typically like to break up your code, the third project (Core) could either be one class library or several. It is completely up to you. That is where you get to flex your creative muscle. But it isn't just as easy as creating a new class library project and banging out some code. If it were that easy, everyone would be doing it. Luckily, you have some tried and true options for sharing this code.

3. Sharing Code

If you want to write code in C# and share it across multiple platforms in a Xamarin project, you have three basic options:

  • File Linking
  • Portable Class Libraries
  • Shared Projects

As Xamarin continues to evolve, File Linking tends to fall off the map, but we'll discuss every option separately to let you choose the most appropriate one.

File Linking

File linking is a viable option in Xamarin Studio and has been around in Visual Studio for quite some time. It's an option that allows you to write code in one physical file and have all other projects have access to that one file. This means that any changes you make to the linked file, will cause changes in the original file. This is because there's really only one file.

The opposite approach of file linking is to create copies of the original file in any other project in your solution that needs access to that particular code. That way, when you make changes to these files in any of the projects, the changes are restricted to that physical file and not the original.

How To Link Files

The process of linking files to different projects in your solution is very similar, whether you're using Xamarin Studio or Visual Studio. Let's start by creating a new class library project in either IDE.

For this example, name your first project SourceLibrary. Within this class library, create a new class and name it User. Replace the class's implementation with the following:

public class User
{
	public string FirstName { get; set; }
	public string LastName { get; set; }
}

You now have a basic class that represents a User within your application. If you wanted to share that concept with another project within your solution, you could add a link. First, you need to create a new class library project within your solution. Let's name this one LinkLibrary. After creating the project, you can add a link to your User class by right-clicking your LinkLibrary project and selecting Add Files in Xamarin Studio or Add Existing Files in Visual Studio. Navigate to the file you want to link to in the Open File dialog box. When you click the Open button in Xamarin Studio you will be presented with a dialog box that looks like this:

To add a link, select the last option, Add a link to the file, and click OK. You have now successfully created a link to the original file.

In Visual Studio, when you find the file you want to link to in the Open File dialog, you'll notice that there's a little drop-down menu on the right edge of the Add button. If you click that menu, you'll be presented with a couple of options:

  • Add, the default option that creates a copy of this file in the current project
  • Add As Link, creates a link to the original file in the current project

After clicking Add As Link, Visual Studio will create a link to the User file in your LinkLibrary project. Once you create a link to a file, you will see that linked file in your current solution with a strange icon in the corner to signify that it's a linked file.

Now, edit the User class in your LinkLibrary project to look like this.

public class User
{
	public string FirstName { get; set; }
	public string LastName { get; set; }
	public string UserName { get; set; }
}

After saving the changes and navigating back to the User class in your SourceLibrary project, you should see that both versions have changed.

This may seem like a lot of work. Why not create all this shared logic in a typical class library project and add references to this assembly from your platform specific assemblies? The answer is that you can't.

When you create an iOS or Android application from the Xamarin.iOS or Xamarin.Android project templates, you're very limited to the assemblies that you can reference. The assemblies that are created during this process are restricted in the functionality and libraries that are supported. The problem is that, by default, the typical class library project contains access to way more functionality than the Xamarin libraries support. Fear not, there are some workarounds.

Portable Class Libraries

Portable Class Libraries or PCLs are the next evolution in creating cross-platform code in the world of .NET. PCLs are stripped down versions of class library projects that only contain bare-bones functionality capable of being referenced and run, without modification, in several different types of projects. The list of supported platforms and features continues to grow. To get more detail on PCLs and to see the supported platforms and features, take a look at the Portable Class Library page on MSDN.

Generally speaking, the way that PCLs work is by creating all of your shared logic in a PCL project and then adding references to that project from your platform specific projects. The only difference in this process from creating a normal class library project is one extra step.

Creating a PCL

Start by creating a new project in Xamarin Studio or Visual Studio, select Class Library as the project template, and name the project TypicalLibrary. Next, add a new project to your solution. Select Class Library (Portable) as the template and name it PortableLibrary. Depending on your IDE, this process may differ slightly.

If you create your new Portable Class Library project in Visual Studio, a dialog similar to the one below will appear.

Depending on your version of Visual Studio, .NET Framework, and Xamarin tools installed, you may see different options, but the concepts are the same. You have the choice of selecting the different platforms you want to support. The combinations of platforms you choose here are only limited by your imagination. Just be sure to only select the platforms that you need. The more platforms you select, the more limited the range of features you can access within your PCL.

If you're using Xamarin Studio, you won't see the above dialog box. Xamarin Studio will set similar options for you by default. To change the supported platforms, right-click the PortableLibrary project and select Options. In the Project Options dialog, select General under the Build section. You should see a set of options similar to those shown below.

Since this is merely a demonstration, you can select any of the platforms that you like. To prove the point though, make sure that you have at least selected Xamarin.Android as one of the supported platforms.

Finally, add another project to your solution. Select the Android Application template and name the project BasicAndroidApplication. To use the functionality you've created in a class library, add a reference to it from your Android application. If you're using Xamarin Studio, you do this by right-clicking the References folder under your BasicAndroidApplication project and selecting Edit References. In the Edit References dialog, select the Projects tab to reference an assembly within your current solution. This is what you'll see:

You are able to select the PCL (PortableLibrary), but as you can see the normal class library (TypicalLibrary) is incompatible.

Attempting this exercise in Visual Studio will ultimately end in the same place, but Visual Studio is going to be a little less obvious. It will let you add a reference to the normal class library, but under the BasicAndroidApplication Refereces you'll see the TypicalLibrary listed with a warning icon.

When you build the project, you'll receive a warning in the Error List similar to the one shown below.

Even though this may seem a little nicer than Xamarin Studio just slamming the door in your face, but this will cause other issues down the road when you try to test and debug your application. You'll run into errors and other problems that may make debugging difficult, so you'll definitely want to be aware of these types of warnings when using a PCL.

Using PCLs

When using PCLs in your solution, there are very few differences with using normal class libraries. During development, when you want to use any classes defined within your PCL, you simply add a reference to it and away you go. During the build process, the output of a PCL is a DLL file, just like a normal class library.

When you create and reference PCLs in your project, that physical DLL file is included with the application, so the primary application assembly always has access to it. This may come as no surprise to you, but it is a distinguishing characteristic when comparing it with the third option, which we'll discuss now.

Shared Projects

Shared Projects and support for their corresponding project templates were added in Xamarin Studio 5 and Visual Studio 2013 Update 2. While the name is rather unassuming, these projects work quite differently than their class library and PCL cousins. These projects don't have the limitations that a PCL does. They have full access to built-in libraries and assemblies in the .NET Framework, with the exception that you're not able to add additional references and components to these projects.

Creating a Shared Project

Start by creating a new Solution/Project in Xamarin Studio or Visual Studio. Select C# as your language and select the Shared Project template. If you're using Visual Studio and you don't see this as an option, then you'll need to download and install the Shared Project Reference Manager extension. Once the installation is complete and you've restarted Visual Studio, you should see this option.

After creating the project, you'll notice that it's a bit empty. There are no Resources or Components folders. There are no Properties or default classes created. You just get a blank slate.

You'll also notice that there are fewer properties when you right-click your project and select Properties from the contextual menu.

The only choice you have for changing options in a Shared Project is the Root Namespace. The Root Namespace is the default namespace given to all classes created within your Shared Project so you don't have to manually update them.

Support for Compiler Directives

One of the important aspects of Shared Projects is their support for compiler directives. This is one of the reasons they fit so well into cross-platform projects. There are a number of directives that are included in Xamarin and Windows Phone projects.

  • __MOBILE__, defined in both Xamarin.iOS and Xamarin.Android projects
  • __IOS__, defined in Xamarin.iOS projects
  • __ANDROID__, defined in Xamarin.Android projects
  • WINDOWS_PHONE, defined in Windows Phone projects
  • SILVERLIGHT, defined in Windows Phone projects

In addition to these generic directives, Xamarin.Android provides a number of specific directives to target a specific version of the Android SDK. It's not uncommon to see a directive similar to __ANDROID_11__, for example. These directives can be used in conditional preprocessor statements to let the compiler know to only attempt to compile the code within them if the special directives are defined. Take a look at the following example.

public class User
{
#if __ANDROID__
    public string FirstName { get; set; }
#endif

    public string LastName { get; set; }
}

This may be a bit of a contrived example, but it serves a purpose. When the compiler comes across this block of code in a Xamarin.Android project, the User class will have both the FirstName and LastName properties. If you were building this in a Xamarin.iOS project, the User class would contain only the LastName property.

This process of conditional compilation comes in handy when dealing with platform specific differences. These differences could come into play when using device or platform specific features such as local storage. With directives, you have the ability to write platform specific code in one file as opposed to multiple files across multiple projects. Very nice.

Using Shared Projects

When you use Shared Projects in an application, there are a few things you need to look out for. You may have noticed when looking at the properties associated with a Shared Project that there were no choices for selecting a Target framework or Output type. That's because these little beauties aren't actually compiled in their own assemblies. Code that is written in a Shared Project is compiled into any project that references it.

What happens if you have a Shared Project in your solution that isn't referenced by any other projects? Nothing. The code is not compiled and subsequently not included anywhere. The same will happen if you add a reference to a Xamarin.iOS project that points to a Shared Project that contains the User class above. Since the __ANDROID__ directive is not defined within Xamarin.iOS, the FirstName property will not be compiled into the Xamarin.iOS project.

4. Sharing Code Guidelines

Based on the above strategies to share code among cross-platform applications, you have some decisions to make. The following discussion outlines the pros and cons of each strategy and presents a number of guidelines to help you with this decision. Keep in mind that there is no right or best strategy. Any of these options works, but one may be slightly more desirable over another.

File Linking

File Linking is the process of creating logical links from one project to a physical file in another project or directory.

Pros

  • only one physical file to maintain
  • code changes only need to be made once

Cons

  • changes made to linked files will effect all code referencing them
  • linking multiple files and directories is a multistep process

Portable Class Libraries

A PCL is a specialized class library project that can be included and run on several different platforms without modification.

Pros

  • generates an assembly that can be referenced by other projects
  • all refactoring and testing can be done in one location

Cons

  • cannot reference any non-PCL or platform specific assemblies
  • only includes a subset of features of the .NET Framework

Shared Projects

A Shared Project is a specialized project template enabling you to write code that is directly compiled into any project that references it. It allows the use of preprocessor directives to include all platform specific code to live in the same project.

Pros

  • can use directives to target specific platforms in a single file
  • have access to other referenced assemblies projects referencing them

Cons

  • there is no output file so code cannot be shared with projects outside the solution
  • refactorings inside inactive compiler directives will not update the project


Rules of Thumb

With the evolution of Xamarin and the introduction of the Shared Project strategy, the File Linking option has all but fallen to the wayside. That doesn't mean you can't or shouldn't use this option, it simply means that in most circumstances the Portable Class Library or Shared Project strategies may be the best fit. Here are simple scenarios for determining which one to go with.

  • Portable Class Library, creating a cross platform application in which the shared code is not platform specific and is intended to be shared or reused outside the current solution
  • Shared Project, creating a cross-platform application with code that can be platform specific and only belong in your solution and is not intended to be shared or reused outside that context

5. Let's Build An App

That's enough theory for now. Let's get write some code. In the previous two introductory tutorials on Xamarin, you created the same RSS reader. While that may have seemed redundant at the time, we did it for two reasons.

  1. show the similarities in creating Xamarin.iOS and Xamarin.Android applications
  2. lead up to merging common code in a single cross-platform application

Project Setup

To create this version of your application, you will follow the basic n+1 guideline. Start by creating a new iOS project using the Empty Project template, naming the project XamFeed.iOS and the solution XamFeed.

Next, add a new Android project to your solution using the Android Application project template. Name the new project XamFeed.Android.

Now it's time for the +1. You can choose any of the code sharing options described in this tutorial to create this application. Since the code that's going to be shared between the iOS and Android applications isn't specific to any one platform and may be useful to other projects and applications, I will take the Portable Class Library route. Add a new C# project to your solution using the Portable Library template and name it XamFeed.Core.

If you're using Visual Studio, you'll see a dialog box that lets you select the supported platforms. If you're using Xamarin Studio, you'll need to look into the project properties for the XamFeed.Core project. You can select just about any combination of platforms as long as you include .NET Framework 4.5 or laterXamarin.iOS, and Xamarin.Android, and make sure not to select Windows Phone 8 and Windows Phone Silverlight 8. The reason you can't choose those two platforms is because they don't support the HttpClient class that we'll use in our application.

You can delete the default class file in the XamFeed.Core library, because we won't be needing it. Finally, be sure to add a reference to the XamFeed.Core project in both the XamFeed.iOS and XamFeed.Android projects. You are now ready to add some code.

Adding Shared Code

We'll start by creating the domain object, RssItem. This class will contain four properties. You can add more if you like.

  • Title, the name of the article
  • PubDate, the date the article was published
  • Creator, the author of the article
  • Link, the URL of the article

Add a new class to your XamFeed.Core project named RssItem. Replace the default implementation with the following:

public class RssItem
{
    public string Title { get; set; }
    public string PubDate { get; set; }
    public string Creator { get; set; }
    public string Link { get; set; }
}

Create another class and name it FeedRetriever. The task of this class will be fetching an array of RssItem objects from a given URL. Once you've created this class, add a new method to it with the following implementation:

public async Task<RssItem[]> GetItems(string feedUrl) 
{
	using (var client = new HttpClient())
	{
		var xmlFeed = await client.GetStringAsync(feedUrl);
		var doc = XDocument.Parse(xmlFeed);
		XNamespace dc = "http://purl.org/dc/elements/1.1/";

		var items = (from item in doc.Descendants("item")
			select new RssItem
			{
				Title = item.Element("title").Value,
				PubDate = item.Element("pubDate").Value,
				Creator = item.Element(dc + "creator").Value,
				Link = item.Element("link").Value
			}).ToArray();

		return items;
	}
}

To get this method to compile into your project, you'll need to add four using statements to the top of the FeedRetriever file:

  • using System.Linq;
  • using System.Net.Http;
  • using System.Xml.Linq;
  • using System.Threading.Tasks;

You now have a handy little class that can be used to retrieve the articles from a given RSS feed URL. This functionality will be added to the iOS and Android applications. Let's start with the Android application.

XamFeed.Android

Putting the RssItem objects into a list requires a custom adapter. In the XamFeed.Android project, create a new class, name it FeedAdapter, and replace it's implementation with the following:

public class FeedAdapter : BaseAdapter<RssItem>
{
    private RssItem[] _items;
    private Activity _context;
    public FeedAdapter( Activity context, RssItem[] items) : base()
    {
        _context = context;
        _items = items;
    }
    public override RssItem this[int position]
    {
        get { return _items[position]; }
    }
    public override int Count
    {
        get { return _items.Count(); }
    }
    public override long GetItemId(int position)
    {
        return position;
    }
    public override View GetView(int position, View convertView, ViewGroup parent)
    {
        var view = convertView;
        if (view == null)
        {
            view = _context.LayoutInflater.Inflate(Android.Resource.Layout.SimpleListItem2, null);
        }
        view.FindViewById<TextView>(Android.Resource.Id.Text1).Text = _items[position].Title;
        view.FindViewById<TextView>(Android.Resource.Id.Text2).Text = string.Format("{0} on {1}", _items[position].Creator, _items[position].PubDate);
        return view;
    }
}

Depending on how you created this class, you may have to add some or all of the following using statements to the class. Double-check that they're all included.

  • using Android.Widget;
  • using XamFeed.Core;
  • using Android.App;
  • using Android.Views;
  • using Android.OS;
  • using System.Linq;

You now have a mechanism for retrieving RssItem objects and an Adapter to display them. All you need is a ListView. Replace the contents of Main.axml (Resources > layout) with the following:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"><ListView
        android:id="@+id/TutsFeedListView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" /></LinearLayout>

The above layout adds a ListView to the screen, so you can populate it with data. All that's left for us to do is wiring up the MainActivity class to the layout, populate the list with RssItem objects, open each of the links when one of the items is tapped by the user. To accomplish this, you only need to modify the contents of MainActivity.cs as shown below.

[Activity(Label = "XamFeed", MainLauncher = true, Icon = "@drawable/icon")]
public class FeedActivity : ListActivity
{
	private FeedRetriever _retriever;
	private RssItem[] _items;

	protected async override void OnCreate(Bundle bundle)
	{
		base.OnCreate(bundle);          
		_retriever = new FeedRetriever ();

		_items = await _retriever.GetItems ("http://blog.xamarin.com/feed");

		ListAdapter = new FeedAdapter (this, _items);

	}

	protected override void OnListItemClick(ListView l, View v, int position, long id)
	{
		base.OnListItemClick(l, v, position, id);
		var uri = global::Android.Net.Uri.Parse (_items [position].Link);

		var intent = new Intent (Intent.ActionView, uri);
		StartActivity(intent);
	}
}

You will have to add a using statement to include the XamFeed.Core namespace to get this to compile.

There you have it. You now have a fully functional Android application that is utilizing core functionality that resides in a Portable Class Library. Now it's time to turn our attention to the XamFeed.iOS project.

XamFeed.iOS

The basic process of getting the iOS project up and running is going to be similar to the Android project. We'll use the FeedRetriever class to get the RssItem objects and then write custom user interface code to display it to the user. To accomplish this, we'll create a view controller containing a list of RssItem objects and set that view controller as the root view controller of the application.

Start by adding a new iOS Table View Controller to the XamFeed.iOS project and name it RssItemView. This results in the creation of the RssItemViewController, RssItemViewSource, and RssItemViewCell classes.

These classes make up the table view that will display the RssItem objects. Replace the default implementation of the RssItemViewController class with the following:

public class RssItemViewController : UITableViewController
{
	private FeedRetriever _retriever;
	private RssItem[] _items;
	public RssItemViewController () : base ()
	{
		_retriever = new FeedRetriever ();
	}

	public override void DidReceiveMemoryWarning ()
	{
		// Releases the view if it doesn't have a superview.
		base.DidReceiveMemoryWarning ();

		// Release any cached data, images, etc that aren't in use.
	}

	public async override void ViewDidLoad ()
	{
		base.ViewDidLoad ();
		_items = await _retriever.GetItems ("http://blog.xamarin.com/feed");
		// Register the TableView's data source
		TableView.Source = new RssItemViewSource (_items);
		TableView.ReloadData ();

	}
}

In the ViewDidLoad method, we use the FeedRetriever class to get the RssItem objects from the Xamarin blog, assign them to the RssItemViewSource object, and call the ReloadData method on the TableView instance. This method ensures that the table view is refreshed after it is shown, so that the data appears on the screen.

Next, update the RssItemViewSource class to handle the RssItem[] that we're passing to it. To do that, replace the default implementation with the following:

public class RssItemViewSource : UITableViewSource
{
	private RssItem[] _items;

	public RssItemViewSource (RssItem[] items)
	{
		_items = items;
	}

	public override int NumberOfSections (UITableView tableView)
	{
		// TODO: return the actual number of sections
		return 1;
	}

	public override int RowsInSection (UITableView tableview, int section)
	{
		// TODO: return the actual number of items in the section
		return _items.Length;
	}

	public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
	{
		var cell = tableView.DequeueReusableCell (RssItemViewCell.Key) as RssItemViewCell;
		if (cell == null)
			cell = new RssItemViewCell ();

		// TODO: populate the cell with the appropriate data based on the indexPath

		cell.TextLabel.Text = _items[indexPath.Row].Title;
		cell.DetailTextLabel.Text = string.Format ("{0} on {1}", _items [indexPath.Row].Creator, _items [indexPath.Row].PubDate);

		return cell;
	}

	public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
	{
		var item = _items [indexPath.Row];

		var url = new NSUrl (item.Link);
		UIApplication.SharedApplication.OpenUrl (url);
	}
}

Most of the work here is done in the GetCell method. This is where we either create a new cell if one doesn't exist or reuse one that has gone out of view. Once we have a cell, we populate it with the Title, Creator, and PubDate properties of the corresponding RssItem object.

Lastly, we need to define the look of the cells within the UITableView. For that, we use the RssItemViewCell class. Replace the default implementation with the following:

public class RssItemViewCell : UITableViewCell
{
	public static readonly NSString Key = new NSString ("FeedItemViewControllerCell");

	public RssItemViewCell () : base (UITableViewCellStyle.Subtitle, Key)
	{
		// TODO: add subviews to the ContentView, set various colors, etc.
		TextLabel.Text = "TextLabel";
	}
}

You may have noticed that it looks almost exactly like the default implementation with only one minor change, we pass a UITableViewCell.Subtitle to the base constructor. This gives each cell two text fields that you can populate.

We now have everything we need to show the list of RssItem objects to the user. There's only one problem. We haven't added the view of the view controller to the view hierarchy of our application. This is easy to fix though. Update the FinishedLaunching method in the AppDelegate class as shown below.

public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
	// create a new window instance based on the screen size
	window = new UIWindow (UIScreen.MainScreen.Bounds);
	var controller = new RssItemViewController ();
	controller.View.BackgroundColor = UIColor.White;
	controller.Title = "Xamarin Feeds";

	var navController = new UINavigationController (controller);
	window.RootViewController = navController;
	// If you have defined a root view controller, set it here:
	// window.RootViewController = myViewController;
	
	// make the window visible
	window.MakeKeyAndVisible ();
	
	return true;
}

We first create a new instance of the RssItemViewController and set a couple of its properties. We then create a new UINavigationViewController instance, passing it the view controller we created a moment ago. Finally, we set the new navigation controller as the RootViewController of the UIWindow instance. That's it.

We now have an Android and an iOS application that displays the RSS feed of the Xamarin blog. You can run these applications one at a time and see them work individually, or you can run them both in one debugging session. To do this, you just need to go into your XamFeed solution properties or options, depending on your IDE. In here, you can choose which projects you want to start up when you run the application. In your solution, select both XamFeed.Android and XamFeed.iOS.

Save the changes and now, when you run your application, both the iOS Simulator and Android Emulator start up and run your application.

Conclusion

Congratulations. You've successfully created a cross-platform mobile application with a single code base. It's important to understand that creating these applications doesn't necessarily coincide with the Write Once Run Anywhere mindset. There will always be customization needed. Luckily, with a combination of Xamarin and some very cool code sharing strategies, you can get very close. If you simply start to think of your mobile apps in terms of platform specific code and platform agnostic code, you are well on your way.

2014-07-21T16:45:37.000Z2014-07-21T16:45:37.000ZDerek Jensen

Create a Plane Fighting Game in Corona: Finishing Gameplay

$
0
0
Final product image
What You'll Be Creating

Introduction

In the fourth and final part of this series, we continue where we left of in the previous tutorial. We'll create enemy planes the player needs to avoid or shoot, and we'll also create a game over screen.

1. generateEnemys

The generateEnemys function generates a number between three and seven, and calls the generateEnemyPlane function every two seconds for however many times numberOfEnemysToGenerate is equal to. Enter the following code snippet to gamelevel.lua.

function generateEnemys()
    numberOfEnemysToGenerate = math.random(3,7)
    timer.performWithDelay( 2000, generateEnemyPlane,numberOfEnemysToGenerate)
end

We also need to invoke this function in the enterScene method as shown below.

function scene:enterScene( event )
    --SNIP--
    Runtime:addEventListener("enterFrame", gameLoop)
    startTimers()
    generateEnemys()
end

Let's see what the implementation of generateEnemyPlane looks like.

2. generateEnemyPlane

The generateEnemyPlane function generates one enemy plane. There are three types of enemy planes in this game.

  • Regular , moves down the screen in a straight line
  • Waver, moves in a wave pattern on the x axis
  • Chaser, chases the player's plane
function generateEnemyPlane()
    if(gameOver ~= true) then
	local randomGridSpace = math.random(11)
   	local randomEnemyNumber = math.random(3)
	local tempEnemy 
	if(planeGrid[randomGridSpace]~=0) then
	    generateEnemyPlane()
	return
	else
	    if(randomEnemyNumber == 1)then
                tempEnemy =  display.newImage("enemy1.png", (randomGridSpace*65)-28,-60)
	        tempEnemy.type = "regular"
	    elseif(randomEnemyNumber == 2) then
	         tempEnemy =  display.newImage("enemy2.png", display.contentWidth/2 -playerWidth/2,-60)
		tempEnemy.type = "waver"
	    else
		tempEnemy =  display.newImage("enemy3.png", (randomGridSpace*65)-28,-60)
		tempEnemy.type = "chaser"
	   end
      planeGrid[randomGridSpace] = 1
      table.insert(enemyPlanes,tempEnemy)
      planeGroup:insert(tempEnemy)
      numberOfEnemysGenerated = numberOfEnemysGenerated+1;
	 end
     if(numberOfEnemysGenerated == numberOfEnemysToGenerate)then
         numberOfEnemysGenerated = 0;
        resetPlaneGrid()
        timer.performWithDelay(2000,generateEnemys,1)
    end
end
end

We first check to make sure the game isn't over yet. We then generate a randomGridSpace, a number between 1 and 11, and a random randomEnemyNumber, a number between 1 and 3. The randomGridSpace is used to position the plane in one of eleven slots at the top of the screen on the x axis. If you think of the game area being divided into eleven sections, then we only want to place new planes in a slot that hasn't been taken yet by another plane. The planeGrid table contains eleven 0's and when we place a new plane in one of the slots, we set the corresponding position in the table to 1 to indicate that slot has been taken by a plane.

We check if the index of the randomGridSpace in the table isn't equal to 0. If it isn't, we know that slot is currently taken and we shouldn't continue, so we call generateEnemyPlane and return from the function.

Next, we check what randomEnemyNumber is equal to and set tempEnemy to one of the three enemy images, we also give it a property of either regular, waver, or chaser. Because Lua is a dynamic language, we can add new properties to an object at runtime. We then set whatever index is equal to randomGridSpace to 1 in the planeGrid table.

We insert tempEnemy into the enemyPlanes table for later reference and increment numberOfEnemysGenerated. If numberOfEnemysGenerated is equal to  numberOfEnemysToGenerate, we reset numberOfEnemysGenerated to 0, invoke resetPlaneGrid, and set a timer that will call generateEnemys again after two seconds. This process repeats for as long as the game isn't over.

3.moveEnemyPlanes

As you may have guessed, the moveEnemyPlanes function is responsible for moving the enemy planes. Depending on the plane's type, the appropriate function is called.

function moveEnemyPlanes()
    if(#enemyPlanes > 0) then
        for i=1, #enemyPlanes do
            if(enemyPlanes[i].type ==  "regular") then
               moveRegularPlane(enemyPlanes[i])
	    elseif(enemyPlanes[i].type == "waver") then
	       moveWaverPlane(enemyPlanes[i])
            else
	       moveChaserPlane(enemyPlanes[i])
	    end
	end
    end
end

This function needs to be invoked in the gameLoop function.

function gameLoop()
    --SNIP--
    checkFreeLifesOutOfBounds()
    checkPlayerCollidesWithFreeLife()
    moveEnemyPlanes()
end

4.moveRegularPlane

The moveRegularPlane simply moves the plane down the screen across the y axis.

function moveRegularPlane(plane)
    plane.y = plane.y+4
end

5.moveWaverPlane

The moveWaverPlane function moves the plane down the screen across the y axis and, in a wave pattern, across the x axis. This is achieved by using the cos function of Lua's math library.

If this concept is foreign to you, Michael James Williams wrote a great introduction to Sinusoidal Motion. The same concepts apply, the only difference is that we are using cosine. You should think sine when dealing with the y axis and cosine when dealing with the x axis.

function moveWaverPlane(plane) 
    plane.y =plane.y+4 plane.x = (display.contentWidth/2)+ 250* math.cos(numberOfTicks * 0.5 * math.pi/30)
end

In the above snippet, we use the numberOfTicks variable. We need to increment this  each time the gameLoop function is called. Add the following as the very first line in the gameLoop function.

function gameLoop()
    numberOfTicks = numberOfTicks + 1
end

6.moveChaserPlane

The moveChaserPlane function has the plane chasing the player. It moves down the y axis at a constant speed and it moves towards the player's position on the x axis. Take a look at the implementation of moveChaserPlane for clarification.

function moveChaserPlane(plane)
    if(plane.x < player.x)then
	plane.x =plane.x +4
    end
    if(plane.x  > player.x)then
	plane.x = plane.x - 4
   end
    plane.y = plane.y + 4
end

If you test the game now, you should see the planes moving down the screen.

7.fireEnemyBullets

Every so often, we want the enemy planes to fire a bullet. We don't want all of them firing at the same time, however, so we choose only a couple of planes to fire.

function fireEnemyBullets()
    if(#enemyPlanes >= 2) then
	local numberOfEnemyPlanesToFire = math.floor(#enemyPlanes/2)
   	local tempEnemyPlanes = table.copy(enemyPlanes)
   	local function fireBullet() 
    	local randIndex = math.random(#tempEnemyPlanes)
    	local tempBullet = display.newImage("bullet.png",  (tempEnemyPlanes[randIndex].x+playerWidth/2) + bulletWidth,tempEnemyPlanes[randIndex].y+playerHeight+bulletHeight)
    	tempBullet.rotation = 180
    	planeGroup:insert(tempBullet)
	table.insert(enemyBullets,tempBullet);
	table.remove(tempEnemyPlanes,randIndex)
   	end
   	for i = 0, numberOfEnemyPlanesToFire do
            fireBullet()
	end
    end
end

We first check to make sure the enemyPlanes table has more than two planes in it. If it does, we get the numberOfEnemyPlanes to fire by taking the length of the enemyPlanes table, divide it by two, and round it down. We also make a copy of the enemyPlanes table, so we can manipulate it separately.

The fireBullet function chooses a plane from the tempEnemyPlanes table and makes the plane fire a bullet. We generate a random number based on the length of the tempEnemyPlanes table, create a bullet image, and  position it  by using whichever plane is at the randIndex in the tempEnemyPlanes table. We then remove that plane from the temporary table to ensure it won't be chosen again next time fireBullet is called.

We repeat this process however many times numerOfEnemyPlanesToFire is equal to and call the fireBullet function.

We need to start the timer that calls this function every so often. To accomplish this, add the following to the startTimers function.

function startTimers()
    firePlayerBulletTimer = timer.performWithDelay(2000, firePlayerBullet ,-1)
    generateIslandTimer = timer.performWithDelay( 5000, generateIsland ,-1)
    generateFreeLifeTimer = timer.performWithDelay(7000,generateFreeLife, - 1)
    fireEnemyBulletsTimer = timer.performWithDelay(2000,fireEnemyBullets,-1)
end

8. moveEnemyBullets

We also need to move the enemy bullets that are on-screen. This is pretty simple using the following code snippet.

function moveEnemyBullets()
    if(#enemyBullets > 0) then
	for i=1,#enemyBullets do
           enemyBullets[i]. y = enemyBullets[i].y + 7
	end
    end
end

Invoke this function in the gameLoop function.

function gameLoop()
    --SNIP--
    checkPlayerCollidesWithFreeLife()
    moveEnemyPlanes()
    moveEnemyBullets()
end

9. checkEnemyBulletsOutOfBounds

In addition to moving the enemy bullets, we need to check when the enemy bullets have gone off-screen and remove them when they do. The implementation of checkEnemyBulletsOutOfBounds should feel familiar by now.

function checkEnemyBulletsOutOfBounds() 
    if(#enemyBullets > 0) then
	for i=#enemyBullets,1,-1 do
	    if(enemyBullets[i].y > display.contentHeight) then
		enemyBullets[i]:removeSelf()
		enemyBullets[i] = nil
		table.remove(enemyBullets,i)
	    end				
	end
    end
end

Invoke this function in the gameLoop function.

function gameLoop()
    --SNIP--
    moveEnemyBullets()
    checkEnemyBulletsOutOfBounds()
end

10. checkEnemyPlanesOutOfBounds

We should also check if the enemy planes have moved off-screen.

function checkEnemyPlanesOutOfBounds()
    if(#enemyPlanes> 0) then
	for i=#enemyPlanes,1,-1 do
            if(enemyPlanes[i].y > display.contentHeight) then
	       enemyPlanes[i]:removeSelf()
	       enemyPlanes[i] = nil
	       table.remove(enemyPlanes,i) 
           end
	end
    end
end

Invoke this function in the gameLoop function

function gameLoop()
    --SNIP--
    moveEnemyBullets()
    checkEnemyBulletsOutOfBounds()
    checkEnemyPlanesOutOfBounds()
end

11. checkPlayerBulletsCollideWithEnemyPlanes

The checkPlayerBulletCollidesWithEnemyPlanes function uses the hasCollided function to check whether any of the player's bullets has collided with any of the enemy planes.

function checkPlayerBulletsCollideWithEnemyPlanes()
    if(#playerBullets > 0 and #enemyPlanes > 0) then
        for i=#playerBullets,1,-1 do
	    for j=#enemyPlanes,1,-1 do
	         if(hasCollided(playerBullets[i], enemyPlanes[j])) then
		     playerBullets[i]:removeSelf()
	             playerBullets[i] =  nil
	             table.remove(playerBullets,i)
		     generateExplosion(enemyPlanes[j].x,enemyPlanes[j].y)
                     enemyPlanes[j]:removeSelf()
	             enemyPlanes[j] = nil
	             table.remove(enemyPlanes,j)
		     local explosion = audio.loadStream("explosion.mp3")
		     local backgroundMusicChannel = audio.play( explosion, {fadein=1000 } )
	         end
	    end
        end
    end
end

This function uses two nested for loops to check whether the objects have collided. For each of the playerBullets, we run through all the planes in the enemyPlanes table and call the hasCollided function. If there's a collision, we remove the bullet and plane, call the generateExplosion function, and load and play an explosion sound.

Invoke this function in the gameLoop function.

function gameLoop()
    --SNIP--
    checkEnemyBulletsOutOfBounds()
    checkEnemyPlanesOutOfBounds()
    checkPlayerBulletsCollideWithEnemyPlanes()
end

12. generateExplosion

The generateExplosion function uses Corona's SpriteObject class. Sprites allow for animated sequences of frames that reside on Image or Sprite Sheets. By grouping images into a single image, you can pull certain frames from that image and create an animation sequence.

function generateExplosion(xPosition , yPosition)
    local options = { width = 60,height = 49,numFrames = 6}
    local explosionSheet = graphics.newImageSheet( "explosion.png", options )
    local sequenceData = {
  	 { name = "explosion", start=1, count=6, time=400,   loopCount=1 }
	}
    local explosionSprite = display.newSprite( explosionSheet, sequenceData )
    explosionSprite.x = xPosition
    explosionSprite.y = yPosition
    explosionSprite:addEventListener( "sprite", explosionListener )
    explosionSprite:play()
end

The newImageSheet method takes as parameters the path to the image and a table of options for the Sprite Sheet. The options we set are the width, the height, and the numFrames, how many individual images make up this sheet. There are six separate explosion images as shown in the image below.

Next, we set up a table, sequenceData, which is needed by the SpriteObject. We set the start property to 1, the count to 6, and time to 400.  The start property is the frame that the animation will start on, the count is how many frames the animation includes, and the time property is how long the animation takes to play through.

We then create the SpriteObject passing in the explosionSheet and sequenceData, set the x and y positions, and add a listener to the sprite. The listener will be used to remove the sprite once it has finished its animation.

13. explosionListener

The explosionListener function is used to remove the sprite. If the event's phase property is equal to ended, then we know the sprite has finished its animation and we can remove it.

function explosionListener( event )
     if ( event.phase == "ended" ) then
        local explosion = event.target 
	explosion:removeSelf()
	explosion = nil
	end
end

14. checkEnemyBulletsCollideWithPlayer

The checkEnemyBulletsCollideWithPlayer checks to see if any of the enemies' bullets have collided with the player's plane.

function checkEnemyBulletsCollideWithPlayer()
    if(#enemyBullets > 0) then
        for i=#enemyBullets,1,-1 do
	    if(hasCollided(enemyBullets[i],player)) then
	        enemyBullets[i]:removeSelf()
	         enemyBullets[i] = nil
		 table.remove(enemyBullets,i)
		 if(playerIsInvincible == false) then
			killPlayer()
		end
	   end
        end
    end
end

We loop through the enemyBullets table and check if any of them have collided with the player. If true, we remove that particular bullet, and, if playerIsInvincible is false, we invoke killPlayer.

Invoke this function in the gameLoop function.

function gameLoop()
    --SNIP--
    checkEnemyPlanesOutOfBounds()
    checkPlayerBulletsCollideWithEnemyPlanes()
    checkEnemyBulletsCollideWithPlayer()
end

15. killPlayer

The killPlayer function is responsible for checking whether the game is over and spawning a new player if it isn't.

function killPlayer()
    numberOfLives = numberOfLives- 1;
    if(numberOfLives == 0) then
        gameOver = true
        doGameOver()
   else
        spawnNewPlayer()
        hideLives()
        showLives()
        playerIsInvincible = true
   end
end

We first decrement numberOfLives by 1, and, if it's equal to 0, we call the gameOver function. It the player has lives left, we call spawnNewPlayer, followed by hideLives, showLives, and set playerIsInvincible to true.

16. doGameOver

The doGameOver function tells the storyboard to go to the gameover scene.

function  doGameOver()
    storyboard.gotoScene("gameover")
end

17.spawnNewPlayer

The spawnNewPlayer function is responsible for spawning a new player after it has died. The player's plane blinks for a few seconds to show that it's temporarily invincible.

function spawnNewPlayer()
    local numberOfTimesToFadePlayer = 5
    local numberOfTimesPlayerHasFaded = 0
    local  function fadePlayer()
        player.alpha = 0;
        transition.to( player, {time=200, alpha=1})
        numberOfTimesPlayerHasFaded = numberOfTimesPlayerHasFaded + 1
        if(numberOfTimesPlayerHasFaded == numberOfTimesToFadePlayer) then
             playerIsInvincible = false
  	end
    end
    	timer.performWithDelay(400, fadePlayer,numberOfTimesToFadePlayer)
end

To make the player's plane blink, we fade it in and out five times. In the fadePlayer function, we set the plane's alpha property to 0, which makes it transparent. We then use the transition library to fade the alpha back to 1 over a period of 200 milliseconds. The to method of the transition object takes a table of options. In our example, the options table includes a time in milliseconds and the property we'd like to animate, alpha, and the desired value, 1.

We increment numberOfTimesThePlayerHasFaded and check if it's equal to the number of times we wanted the player to fade. We then set playerIsInvincible to false. We use a timer to call the fadePlayer function however many times numberOfTimerToFadePlayer is equal to.

There is a way to do all this without using the timer and that is by using the transition's iterations property in combination with its onComplete handler. Read through the documentation to learn more about this alternative approach.

18. checkEnemyPlaneCollidesWithPlayer

There is one more collision check we should do and that is to see if an enemy plane collides with the player's plane.

function checkEnemyPlaneCollideWithPlayer() 
    if(#enemyPlanes > 0) then
        for i=#enemyPlanes,1,-1 do
	    if(hasCollided(enemyPlanes[i], player)) then
	        enemyPlanes[i]:removeSelf()
		enemyPlanes[i] = nil
		table.remove(enemyPlanes,i)
		if(playerIsInvincible == false) then
		    killPlayer()
		end
	  end
	end
    end
end

We loop through the enemy planes and see if any one of them collides with the player's plane. If true, we remove that enemy plane and call killPlayer. If you think it makes the game more interesting, you could also generate an explosion here.

19.exitScene

When the game is over, we transition to the gameover scene. Remember from earlier in the tutorial, the exitScene function is where you remove any event listeners, stop timers, and stop audio that's playing.

function scene:exitScene( event )
    local group = self.view
    rectUp:removeEventListener( "touch", movePlane)
    rectDown:removeEventListener( "touch", movePlane)
    rectLeft:removeEventListener( "touch", movePlane)
    rectRight:removeEventListener( "touch", movePlane)
    audio.stop(planeSoundChannel)
    audio.dispose(planeSoundChannel)
    Runtime:removeEventListener("enterFrame", gameLoop)
    cancelTimers()
end
scene:addEventListener( "exitScene", scene )

We are basically undoing what we did in the enterScene function. We call the dispose method on the audio object to release the memory associated with the audio channel. Calling stop alone does not release the memory.

20. cancelTimers

As its name indicates, the cancelTimers function does the opposite of  startTimers, it cancels all the timers.

function cancelTimers()
    timer.cancel( firePlayerBulletTimer )
    timer.cancel(generateIslandTimer)
    timer.cancel(fireEnemyBulletsTimer)
    timer.cancel(generateFreeLifeTimer)
end

21. Game Over Scene

It's time to create the gameover scene. Start by adding a new Lua file to your project named gameover.lua, and add  the following code to it.

local storyboard = require( "storyboard" )
local scene = storyboard.newScene()
local gameOverText
local newGameButton

return scene

22. createScene

Add the following to gameover.lua above return scene. From here on out, all code should be placed above the return scene statement.

function scene:createScene( event )
    local group = self.view
    local background = display.newRect( 0, 0, display.contentWidth, display.contentHeight)
    background:setFillColor( 0,.39,.75)
    group:insert(background)
    gameOverText = display.newText( "Game Over", display.contentWidth/2,400, native.systemFont, 16 )
    gameOverText:setFillColor( 1, 1, 0 )
    gameOverText.anchorX = .5
    gameOverText.anchorY = .5
    group:insert(gameOverText)
    newGameButton = display.newImage("newgamebutton.png",264,670)
    group:insert(newGameButton)
    newGameButton.isVisible = false
 end

As we did in the previous two scenes, we give the gameover scene a blue background. We then create a TextObject instance by calling newText on display. The newText method takes a few options, the text for the object, its position, and the font to use. We give it a yellow color by invoking setFillColor, passing in RGB values as percentages. Finally, we create a button and hide it for the time being.

23. enterScene

When the storyboard has fully transitioned to the gameover scene, the enterScene method is called.

In enterScene, we remove the previous scene from the storyboard. We use the convenience method scaleTo from the Transition Library to scale the gameOverText by a factor of 4. We add an onComplete listener to the transition that calls the showButton function once the transition has completed. Lastly, we add a tap event listener to the game button that invokes the startNewGame function.

function scene:enterScene( event )
    local group = self.view
    storyboard.removeScene("gamelevel" )
    transition.scaleTo( gameOverText, { xScale=4.0, yScale=4.0, time=2000,onComplete=showButton} )
    newGameButton:addEventListener("tap", startNewGame)
end

24. showButton

The showButton function hides the gameOverText and shows the newGameButton.

 function showButton()
    gameOverText.isVisible = false
    newGameButton.isVisible= true
 end

25. startNewGame

The startNewGame function tells the storyboard to transition to the gamelevel scene.

function startNewGame()
    storyboard.gotoScene("gamelevel")
end

26. exitScene

We need to do some cleanup when we leave the gameover scene. We remove the tap event listener we added earlier to the newGameButton.

function scene:exitScene( event )
    local group = self.view
    newGameButton:removeEventListener("tap",startNewGame)
end

27. Add Scene Listeners

The final piece of the puzzle is adding the scene event listeners we talked about earlier. To do this, addd the following code snippet to gameover.lua.

scene:addEventListener( "createScene", scene )
scene:addEventListener( "enterScene", scene )
scene:addEventListener( "exitScene", scene )

Conclusion

We have come to the end of this series and now have a fully functional plane fighting game. I hope you've found these tutorials useful and have learned something along the way. Thanks for reading.


2014-07-23T17:45:18.000Z2014-07-23T17:45:18.000ZJames Tyner

Create a Plane Fighting Game in Corona: Finishing Gameplay

$
0
0
tag:code.tutsplus.com,2005:PostPresenter/cms-21356
Final product image
What You'll Be Creating

Introduction

In the fourth and final part of this series, we continue where we left of in the previous tutorial. We'll create enemy planes the player needs to avoid or shoot, and we'll also create a game over screen.

1. generateEnemys

The generateEnemys function generates a number between three and seven, and calls the generateEnemyPlane function every two seconds for however many times numberOfEnemysToGenerate is equal to. Enter the following code snippet to gamelevel.lua.

function generateEnemys()
    numberOfEnemysToGenerate = math.random(3,7)
    timer.performWithDelay( 2000, generateEnemyPlane,numberOfEnemysToGenerate)
end

We also need to invoke this function in the enterScene method as shown below.

function scene:enterScene( event )
    --SNIP--
    Runtime:addEventListener("enterFrame", gameLoop)
    startTimers()
    generateEnemys()
end

Let's see what the implementation of generateEnemyPlane looks like.

2. generateEnemyPlane

The generateEnemyPlane function generates one enemy plane. There are three types of enemy planes in this game.

  • Regular , moves down the screen in a straight line
  • Waver, moves in a wave pattern on the x axis
  • Chaser, chases the player's plane
function generateEnemyPlane()
    if(gameOver ~= true) then
	local randomGridSpace = math.random(11)
   	local randomEnemyNumber = math.random(3)
	local tempEnemy 
	if(planeGrid[randomGridSpace]~=0) then
	    generateEnemyPlane()
	return
	else
	    if(randomEnemyNumber == 1)then
                tempEnemy =  display.newImage("enemy1.png", (randomGridSpace*65)-28,-60)
	        tempEnemy.type = "regular"
	    elseif(randomEnemyNumber == 2) then
	         tempEnemy =  display.newImage("enemy2.png", display.contentWidth/2 -playerWidth/2,-60)
		tempEnemy.type = "waver"
	    else
		tempEnemy =  display.newImage("enemy3.png", (randomGridSpace*65)-28,-60)
		tempEnemy.type = "chaser"
	   end
      planeGrid[randomGridSpace] = 1
      table.insert(enemyPlanes,tempEnemy)
      planeGroup:insert(tempEnemy)
      numberOfEnemysGenerated = numberOfEnemysGenerated+1;
	 end
     if(numberOfEnemysGenerated == numberOfEnemysToGenerate)then
         numberOfEnemysGenerated = 0;
        resetPlaneGrid()
        timer.performWithDelay(2000,generateEnemys,1)
    end
end
end

We first check to make sure the game isn't over yet. We then generate a randomGridSpace, a number between 1 and 11, and a random randomEnemyNumber, a number between 1 and 3. The randomGridSpace is used to position the plane in one of eleven slots at the top of the screen on the x axis. If you think of the game area being divided into eleven sections, then we only want to place new planes in a slot that hasn't been taken yet by another plane. The planeGrid table contains eleven 0's and when we place a new plane in one of the slots, we set the corresponding position in the table to 1 to indicate that slot has been taken by a plane.

We check if the index of the randomGridSpace in the table isn't equal to 0. If it isn't, we know that slot is currently taken and we shouldn't continue, so we call generateEnemyPlane and return from the function.

Next, we check what randomEnemyNumber is equal to and set tempEnemy to one of the three enemy images, we also give it a property of either regular, waver, or chaser. Because Lua is a dynamic language, we can add new properties to an object at runtime. We then set whatever index is equal to randomGridSpace to 1 in the planeGrid table.

We insert tempEnemy into the enemyPlanes table for later reference and increment numberOfEnemysGenerated. If numberOfEnemysGenerated is equal to  numberOfEnemysToGenerate, we reset numberOfEnemysGenerated to 0, invoke resetPlaneGrid, and set a timer that will call generateEnemys again after two seconds. This process repeats for as long as the game isn't over.

3.moveEnemyPlanes

As you may have guessed, the moveEnemyPlanes function is responsible for moving the enemy planes. Depending on the plane's type, the appropriate function is called.

function moveEnemyPlanes()
    if(#enemyPlanes > 0) then
        for i=1, #enemyPlanes do
            if(enemyPlanes[i].type ==  "regular") then
               moveRegularPlane(enemyPlanes[i])
	    elseif(enemyPlanes[i].type == "waver") then
	       moveWaverPlane(enemyPlanes[i])
            else
	       moveChaserPlane(enemyPlanes[i])
	    end
	end
    end
end

This function needs to be invoked in the gameLoop function.

function gameLoop()
    --SNIP--
    checkFreeLifesOutOfBounds()
    checkPlayerCollidesWithFreeLife()
    moveEnemyPlanes()
end

4.moveRegularPlane

The moveRegularPlane simply moves the plane down the screen across the y axis.

function moveRegularPlane(plane)
    plane.y = plane.y+4
end

5.moveWaverPlane

The moveWaverPlane function moves the plane down the screen across the y axis and, in a wave pattern, across the x axis. This is achieved by using the cos function of Lua's math library.

If this concept is foreign to you, Michael James Williams wrote a great introduction to Sinusoidal Motion. The same concepts apply, the only difference is that we are using cosine. You should think sine when dealing with the y axis and cosine when dealing with the x axis.

function moveWaverPlane(plane) 
    plane.y =plane.y+4 plane.x = (display.contentWidth/2)+ 250* math.cos(numberOfTicks * 0.5 * math.pi/30)
end

In the above snippet, we use the numberOfTicks variable. We need to increment this  each time the gameLoop function is called. Add the following as the very first line in the gameLoop function.

function gameLoop()
    numberOfTicks = numberOfTicks + 1
end

6.moveChaserPlane

The moveChaserPlane function has the plane chasing the player. It moves down the y axis at a constant speed and it moves towards the player's position on the x axis. Take a look at the implementation of moveChaserPlane for clarification.

function moveChaserPlane(plane)
    if(plane.x < player.x)then
	plane.x =plane.x +4
    end
    if(plane.x  > player.x)then
	plane.x = plane.x - 4
   end
    plane.y = plane.y + 4
end

If you test the game now, you should see the planes moving down the screen.

7.fireEnemyBullets

Every so often, we want the enemy planes to fire a bullet. We don't want all of them firing at the same time, however, so we choose only a couple of planes to fire.

function fireEnemyBullets()
    if(#enemyPlanes >= 2) then
	local numberOfEnemyPlanesToFire = math.floor(#enemyPlanes/2)
   	local tempEnemyPlanes = table.copy(enemyPlanes)
   	local function fireBullet() 
    	local randIndex = math.random(#tempEnemyPlanes)
    	local tempBullet = display.newImage("bullet.png",  (tempEnemyPlanes[randIndex].x+playerWidth/2) + bulletWidth,tempEnemyPlanes[randIndex].y+playerHeight+bulletHeight)
    	tempBullet.rotation = 180
    	planeGroup:insert(tempBullet)
	table.insert(enemyBullets,tempBullet);
	table.remove(tempEnemyPlanes,randIndex)
   	end
   	for i = 0, numberOfEnemyPlanesToFire do
            fireBullet()
	end
    end
end

We first check to make sure the enemyPlanes table has more than two planes in it. If it does, we get the numberOfEnemyPlanes to fire by taking the length of the enemyPlanes table, divide it by two, and round it down. We also make a copy of the enemyPlanes table, so we can manipulate it separately.

The fireBullet function chooses a plane from the tempEnemyPlanes table and makes the plane fire a bullet. We generate a random number based on the length of the tempEnemyPlanes table, create a bullet image, and  position it  by using whichever plane is at the randIndex in the tempEnemyPlanes table. We then remove that plane from the temporary table to ensure it won't be chosen again next time fireBullet is called.

We repeat this process however many times numerOfEnemyPlanesToFire is equal to and call the fireBullet function.

We need to start the timer that calls this function every so often. To accomplish this, add the following to the startTimers function.

function startTimers()
    firePlayerBulletTimer = timer.performWithDelay(2000, firePlayerBullet ,-1)
    generateIslandTimer = timer.performWithDelay( 5000, generateIsland ,-1)
    generateFreeLifeTimer = timer.performWithDelay(7000,generateFreeLife, - 1)
    fireEnemyBulletsTimer = timer.performWithDelay(2000,fireEnemyBullets,-1)
end

8. moveEnemyBullets

We also need to move the enemy bullets that are on-screen. This is pretty simple using the following code snippet.

function moveEnemyBullets()
    if(#enemyBullets > 0) then
	for i=1,#enemyBullets do
           enemyBullets[i]. y = enemyBullets[i].y + 7
	end
    end
end

Invoke this function in the gameLoop function.

function gameLoop()
    --SNIP--
    checkPlayerCollidesWithFreeLife()
    moveEnemyPlanes()
    moveEnemyBullets()
end

9. checkEnemyBulletsOutOfBounds

In addition to moving the enemy bullets, we need to check when the enemy bullets have gone off-screen and remove them when they do. The implementation of checkEnemyBulletsOutOfBounds should feel familiar by now.

function checkEnemyBulletsOutOfBounds() 
    if(#enemyBullets > 0) then
	for i=#enemyBullets,1,-1 do
	    if(enemyBullets[i].y > display.contentHeight) then
		enemyBullets[i]:removeSelf()
		enemyBullets[i] = nil
		table.remove(enemyBullets,i)
	    end				
	end
    end
end

Invoke this function in the gameLoop function.

function gameLoop()
    --SNIP--
    moveEnemyBullets()
    checkEnemyBulletsOutOfBounds()
end

10. checkEnemyPlanesOutOfBounds

We should also check if the enemy planes have moved off-screen.

function checkEnemyPlanesOutOfBounds()
    if(#enemyPlanes> 0) then
	for i=#enemyPlanes,1,-1 do
            if(enemyPlanes[i].y > display.contentHeight) then
	       enemyPlanes[i]:removeSelf()
	       enemyPlanes[i] = nil
	       table.remove(enemyPlanes,i) 
           end
	end
    end
end

Invoke this function in the gameLoop function

function gameLoop()
    --SNIP--
    moveEnemyBullets()
    checkEnemyBulletsOutOfBounds()
    checkEnemyPlanesOutOfBounds()
end

11. checkPlayerBulletsCollideWithEnemyPlanes

The checkPlayerBulletCollidesWithEnemyPlanes function uses the hasCollided function to check whether any of the player's bullets has collided with any of the enemy planes.

function checkPlayerBulletsCollideWithEnemyPlanes()
    if(#playerBullets > 0 and #enemyPlanes > 0) then
        for i=#playerBullets,1,-1 do
	    for j=#enemyPlanes,1,-1 do
	         if(hasCollided(playerBullets[i], enemyPlanes[j])) then
		     playerBullets[i]:removeSelf()
	             playerBullets[i] =  nil
	             table.remove(playerBullets,i)
		     generateExplosion(enemyPlanes[j].x,enemyPlanes[j].y)
                     enemyPlanes[j]:removeSelf()
	             enemyPlanes[j] = nil
	             table.remove(enemyPlanes,j)
		     local explosion = audio.loadStream("explosion.mp3")
		     local backgroundMusicChannel = audio.play( explosion, {fadein=1000 } )
	         end
	    end
        end
    end
end

This function uses two nested for loops to check whether the objects have collided. For each of the playerBullets, we run through all the planes in the enemyPlanes table and call the hasCollided function. If there's a collision, we remove the bullet and plane, call the generateExplosion function, and load and play an explosion sound.

Invoke this function in the gameLoop function.

function gameLoop()
    --SNIP--
    checkEnemyBulletsOutOfBounds()
    checkEnemyPlanesOutOfBounds()
    checkPlayerBulletsCollideWithEnemyPlanes()
end

12. generateExplosion

The generateExplosion function uses Corona's SpriteObject class. Sprites allow for animated sequences of frames that reside on Image or Sprite Sheets. By grouping images into a single image, you can pull certain frames from that image and create an animation sequence.

function generateExplosion(xPosition , yPosition)
    local options = { width = 60,height = 49,numFrames = 6}
    local explosionSheet = graphics.newImageSheet( "explosion.png", options )
    local sequenceData = {
  	 { name = "explosion", start=1, count=6, time=400,   loopCount=1 }
	}
    local explosionSprite = display.newSprite( explosionSheet, sequenceData )
    explosionSprite.x = xPosition
    explosionSprite.y = yPosition
    explosionSprite:addEventListener( "sprite", explosionListener )
    explosionSprite:play()
end

The newImageSheet method takes as parameters the path to the image and a table of options for the Sprite Sheet. The options we set are the width, the height, and the numFrames, how many individual images make up this sheet. There are six separate explosion images as shown in the image below.

Next, we set up a table, sequenceData, which is needed by the SpriteObject. We set the start property to 1, the count to 6, and time to 400.  The start property is the frame that the animation will start on, the count is how many frames the animation includes, and the time property is how long the animation takes to play through.

We then create the SpriteObject passing in the explosionSheet and sequenceData, set the x and y positions, and add a listener to the sprite. The listener will be used to remove the sprite once it has finished its animation.

13. explosionListener

The explosionListener function is used to remove the sprite. If the event's phase property is equal to ended, then we know the sprite has finished its animation and we can remove it.

function explosionListener( event )
     if ( event.phase == "ended" ) then
        local explosion = event.target 
	explosion:removeSelf()
	explosion = nil
	end
end

14. checkEnemyBulletsCollideWithPlayer

The checkEnemyBulletsCollideWithPlayer checks to see if any of the enemies' bullets have collided with the player's plane.

function checkEnemyBulletsCollideWithPlayer()
    if(#enemyBullets > 0) then
        for i=#enemyBullets,1,-1 do
	    if(hasCollided(enemyBullets[i],player)) then
	        enemyBullets[i]:removeSelf()
	         enemyBullets[i] = nil
		 table.remove(enemyBullets,i)
		 if(playerIsInvincible == false) then
			killPlayer()
		end
	   end
        end
    end
end

We loop through the enemyBullets table and check if any of them have collided with the player. If true, we remove that particular bullet, and, if playerIsInvincible is false, we invoke killPlayer.

Invoke this function in the gameLoop function.

function gameLoop()
    --SNIP--
    checkEnemyPlanesOutOfBounds()
    checkPlayerBulletsCollideWithEnemyPlanes()
    checkEnemyBulletsCollideWithPlayer()
end

15. killPlayer

The killPlayer function is responsible for checking whether the game is over and spawning a new player if it isn't.

function killPlayer()
    numberOfLives = numberOfLives- 1;
    if(numberOfLives == 0) then
        gameOver = true
        doGameOver()
   else
        spawnNewPlayer()
        hideLives()
        showLives()
        playerIsInvincible = true
   end
end

We first decrement numberOfLives by 1, and, if it's equal to 0, we call the gameOver function. It the player has lives left, we call spawnNewPlayer, followed by hideLives, showLives, and set playerIsInvincible to true.

16. doGameOver

The doGameOver function tells the storyboard to go to the gameover scene.

function  doGameOver()
    storyboard.gotoScene("gameover")
end

17.spawnNewPlayer

The spawnNewPlayer function is responsible for spawning a new player after it has died. The player's plane blinks for a few seconds to show that it's temporarily invincible.

function spawnNewPlayer()
    local numberOfTimesToFadePlayer = 5
    local numberOfTimesPlayerHasFaded = 0
    local  function fadePlayer()
        player.alpha = 0;
        transition.to( player, {time=200, alpha=1})
        numberOfTimesPlayerHasFaded = numberOfTimesPlayerHasFaded + 1
        if(numberOfTimesPlayerHasFaded == numberOfTimesToFadePlayer) then
             playerIsInvincible = false
  	end
    end
    	timer.performWithDelay(400, fadePlayer,numberOfTimesToFadePlayer)
end

To make the player's plane blink, we fade it in and out five times. In the fadePlayer function, we set the plane's alpha property to 0, which makes it transparent. We then use the transition library to fade the alpha back to 1 over a period of 200 milliseconds. The to method of the transition object takes a table of options. In our example, the options table includes a time in milliseconds and the property we'd like to animate, alpha, and the desired value, 1.

We increment numberOfTimesThePlayerHasFaded and check if it's equal to the number of times we wanted the player to fade. We then set playerIsInvincible to false. We use a timer to call the fadePlayer function however many times numberOfTimerToFadePlayer is equal to.

There is a way to do all this without using the timer and that is by using the transition's iterations property in combination with its onComplete handler. Read through the documentation to learn more about this alternative approach.

18. checkEnemyPlaneCollidesWithPlayer

There is one more collision check we should do and that is to see if an enemy plane collides with the player's plane.

function checkEnemyPlaneCollideWithPlayer() 
    if(#enemyPlanes > 0) then
        for i=#enemyPlanes,1,-1 do
	    if(hasCollided(enemyPlanes[i], player)) then
	        enemyPlanes[i]:removeSelf()
		enemyPlanes[i] = nil
		table.remove(enemyPlanes,i)
		if(playerIsInvincible == false) then
		    killPlayer()
		end
	  end
	end
    end
end

We loop through the enemy planes and see if any one of them collides with the player's plane. If true, we remove that enemy plane and call killPlayer. If you think it makes the game more interesting, you could also generate an explosion here.

19.exitScene

When the game is over, we transition to the gameover scene. Remember from earlier in the tutorial, the exitScene function is where you remove any event listeners, stop timers, and stop audio that's playing.

function scene:exitScene( event )
    local group = self.view
    rectUp:removeEventListener( "touch", movePlane)
    rectDown:removeEventListener( "touch", movePlane)
    rectLeft:removeEventListener( "touch", movePlane)
    rectRight:removeEventListener( "touch", movePlane)
    audio.stop(planeSoundChannel)
    audio.dispose(planeSoundChannel)
    Runtime:removeEventListener("enterFrame", gameLoop)
    cancelTimers()
end
scene:addEventListener( "exitScene", scene )

We are basically undoing what we did in the enterScene function. We call the dispose method on the audio object to release the memory associated with the audio channel. Calling stop alone does not release the memory.

20. cancelTimers

As its name indicates, the cancelTimers function does the opposite of  startTimers, it cancels all the timers.

function cancelTimers()
    timer.cancel( firePlayerBulletTimer )
    timer.cancel(generateIslandTimer)
    timer.cancel(fireEnemyBulletsTimer)
    timer.cancel(generateFreeLifeTimer)
end

21. Game Over Scene

It's time to create the gameover scene. Start by adding a new Lua file to your project named gameover.lua, and add  the following code to it.

local storyboard = require( "storyboard" )
local scene = storyboard.newScene()
local gameOverText
local newGameButton

return scene

22. createScene

Add the following to gameover.lua above return scene. From here on out, all code should be placed above the return scene statement.

function scene:createScene( event )
    local group = self.view
    local background = display.newRect( 0, 0, display.contentWidth, display.contentHeight)
    background:setFillColor( 0,.39,.75)
    group:insert(background)
    gameOverText = display.newText( "Game Over", display.contentWidth/2,400, native.systemFont, 16 )
    gameOverText:setFillColor( 1, 1, 0 )
    gameOverText.anchorX = .5
    gameOverText.anchorY = .5
    group:insert(gameOverText)
    newGameButton = display.newImage("newgamebutton.png",264,670)
    group:insert(newGameButton)
    newGameButton.isVisible = false
 end

As we did in the previous two scenes, we give the gameover scene a blue background. We then create a TextObject instance by calling newText on display. The newText method takes a few options, the text for the object, its position, and the font to use. We give it a yellow color by invoking setFillColor, passing in RGB values as percentages. Finally, we create a button and hide it for the time being.

23. enterScene

When the storyboard has fully transitioned to the gameover scene, the enterScene method is called.

In enterScene, we remove the previous scene from the storyboard. We use the convenience method scaleTo from the Transition Library to scale the gameOverText by a factor of 4. We add an onComplete listener to the transition that calls the showButton function once the transition has completed. Lastly, we add a tap event listener to the game button that invokes the startNewGame function.

function scene:enterScene( event )
    local group = self.view
    storyboard.removeScene("gamelevel" )
    transition.scaleTo( gameOverText, { xScale=4.0, yScale=4.0, time=2000,onComplete=showButton} )
    newGameButton:addEventListener("tap", startNewGame)
end

24. showButton

The showButton function hides the gameOverText and shows the newGameButton.

 function showButton()
    gameOverText.isVisible = false
    newGameButton.isVisible= true
 end

25. startNewGame

The startNewGame function tells the storyboard to transition to the gamelevel scene.

function startNewGame()
    storyboard.gotoScene("gamelevel")
end

26. exitScene

We need to do some cleanup when we leave the gameover scene. We remove the tap event listener we added earlier to the newGameButton.

function scene:exitScene( event )
    local group = self.view
    newGameButton:removeEventListener("tap",startNewGame)
end

27. Add Scene Listeners

The final piece of the puzzle is adding the scene event listeners we talked about earlier. To do this, addd the following code snippet to gameover.lua.

scene:addEventListener( "createScene", scene )
scene:addEventListener( "enterScene", scene )
scene:addEventListener( "exitScene", scene )

Conclusion

We have come to the end of this series and now have a fully functional plane fighting game. I hope you've found these tutorials useful and have learned something along the way. Thanks for reading.


2014-07-23T17:45:18.000Z2014-07-23T17:45:18.000ZJames Tyner

Core Data from Scratch: NSFetchedResultsController

$
0
0

In the previous installments of this series, we covered the fundamentals of the Core Data framework. It's time we put our knowledge to use by building a simple application powered by Core Data.

In this tutorial, we'll also meet another star player of the Core Data framework, the NSFetchedResultsController class. The application that we're about to create manages a list of to-do items. With the application, we can add, update, and delete to-do items. You'll quickly learn that the NSFetchedResultsController class makes this very easy to do.

1. Project Setup

Open Xcode, select New > Project... from the File menu, and choose the Single View Application template from the iOS > Application category.

Name the project Done, set Devices to iPhone, and tell Xcode where you want to save the project.

Because we chose the Single View Application template, Xcode hasn't created a Core Data setup for us. However, setting up the Core Data stack should be easy, if you've read the previous installments of this series.

2. Core Data Setup

Open the implementation file of the application delegate class, TSPAppDelegate.m, and declare three properties in a private class extension, managedObjectContextmanagedObjectModel, and persistentStoreCoordinator. If you're confused by this step, then I recommend you revisit the first article of this series, which covers the Core Data stack in detail.

Note that I've also added an import statement for the Core Data framework at the top of TSPAppDelegate.m.

#import "TSPAppDelegate.h"

#import <CoreData/CoreData.h>

@interface TSPAppDelegate ()

@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;

@end

As you may remember, the Core Data stack is set up lazily. This means that we instantiate the managed object context, the managed object model, and the persistent store coordinator the moment they are needed by the application. In other words, the aforementioned objects are instantiated in the getters of their corresponding properties. The following code snippets should look very familiar.

- (NSManagedObjectContext *)managedObjectContext {
    if (_managedObjectContext) {
        return _managedObjectContext;
    }
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator) {
        _managedObjectContext = [[NSManagedObjectContext alloc] init];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _managedObjectContext;
}
- (NSManagedObjectModel *)managedObjectModel {
    if (_managedObjectModel) {
        return _managedObjectModel;
    }
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Done" withExtension:@"momd"];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return _managedObjectModel;
}
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    if (_persistentStoreCoordinator) {
        return _persistentStoreCoordinator;
    }
    NSURL *applicationDocumentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    NSURL *storeURL = [applicationDocumentsDirectory URLByAppendingPathComponent:@"Done.sqlite"];
    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    return _persistentStoreCoordinator;
}

There are three things you should be aware of. First, the data model, which we'll create next, will be named Done.momd. Second, we'll name the backing store Done and it will be a SQLite database. Third, if the backing store is incompatible with the data model, we invoke abort, killing the application. As I mentioned earlier in this series, while this is fine during development, you should never call abort in production. We'll revisit migrations and incompatibility issues later in this series.

While our application won't crash if you try to run it, the Core Data stack won't get properly set up. The reason is simple, we haven't created a data model yet. Let's take care of that in the next step.

3. Creating the Data Model

Select New > File... from the File menu and choose Data Model from the iOS > Core Data category.

Name the file Done, double-check that it's added to the Done target, and tell Xcode where it needs to be saved.

The data model is going to be very simple. Create a new entity and name it TSPItem. Add three attributes to the entity, name of  type String, createdAt of type Date, and done of type Boolean.

Mark the attributes as required, not optional, and set the default value of the done attribute to NO.

The Core Data stack is set up and the data model is configured correctly. It's time to become familiar with a new class of the Core Data framework, the NSFetchedResultsController class.

4. Managing Data

This article isn't just about the NSFetchedResultsController class, it's about what the NSFetchedResultsController class does behind the scenes. Let me clarify what I mean by this.

If we were to build our application without the NSFetchedResultsController class, we would need to find a way to keep the application's user interface synchronized with the data managed by Core Data. Fortunately, Core Data has an elegant solution to this problem.

Whenever a record is inserted, updated, or deleted in a managed object context, the managed object context posts a notification through notification center. A managed object context can post three types of notifications:

  • NSManagedObjectContextObjectsDidChangeNotification: This notification is posted every time a record in the managed object context is inserted, updated, or deleted.
  • NSManagedObjectContextWillSaveNotification: This notification is posted by the managed object context before pending changes are committed to the backing store.
  • NSManagedObjectContextDidSaveNotification: This notification is posted by the managed object context immediately after pending changes have been committed to the backing store.

The contents of these notifications are identical, that is, the object property of the notification is the NSManagedObjectContext instance that posted the notification and the notification's userInfo dictionary contains the records that were inserted, updated, and deleted.

The gist is that it requires a fair amount of boilerplate code to keep the results of a fetch request up to date. In other words, if we were to create our application without using the NSFetchedResultsController class, we would have to implement a mechanism that monitored the managed object context for changes and update the user interface accordingly. Let's see how the NSFetchedResultsController can help us with this task.

5. Setting Up the User Interface

Working with the NSFetchedResultsController class is pretty easy. An instance of the NSFetchedResultsController class takes a fetch request and has a delegate object. The NSFetchedResultsController object makes sure that it keeps the results of the fetch request up to date by monitoring the managed object context the fetch request was executed by.

If the NSFetchedResultsController object is notified of any changes by the NSManagedObjectContext object of the fetch request, it notifies its delegate. You may be wondering how this is different from a view controller directly monitoring the NSManagedObjectContext object. The beauty of the NSFetchedResultsController class is that it processes the notifications it receives from the NSManagedObjectContext object and tells the delegate only what it needs to know to update the user interface in response to these changes. The methods of the NSFetchedResultsControllerDelegate protocol should clarify this.

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller;
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type;
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath;

The signatures of the above delegate methods reveal the true purpose of the NSFetchedResultsController class. On iOS, the NSFetchedResultsController class was designed to manage the data displayed by a UITableView or a UICollectionView. It tells its delegate exactly which records changed, how to update the user interface, and when to do this.

Don't worry if you're still unsure about the purpose or advantages of the NSFetchedResultsController class. It'll make more sense once we've implemented the NSFetchedResultsControllerDelegate protocol. Let's revisit our application and put the NSFetchedResultsController class to use.

Step 1: Populating the Storyboard

Open the project's main storyboard, Main.storyboard, select the View Controller Scene, and embed it in a navigation controller by selecting Embed In > Navigation Controller from the Editor menu.

Drag a UITableView object in the View Controller Scene, create an outlet in the TSPViewController class, and connect it in the storyboard. Don't forget to make the TSPViewController class conform to the UITableViewDataSource and UITableViewDelegate protocols.

#import <UIKit/UIKit.h>

@interface TSPViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>

@property (weak, nonatomic) IBOutlet UITableView *tableView;

@end

Select the table view, open the Connections Inspector, and connect the table view's dataSource and delegate outlets to the View Controller object. With the table view selected, open the Attributes Inspector and set the number of Prototype Cells to 1.

Before we continue, we need to create a UITableViewCell subclass for the prototype cell. Create a new Objective-C subclass, TSPToDoCell, and set its superclass to UITableViewCell. Create two outlets, nameLabel of type UILabel and doneButton of type UIButton.

Head back to the storyboard, select the prototype cell in the table view, and set the class to TSPToDoCell in the Identity Inspector. Add a UILabel and a UIButton object to the cell's content view and connect the outlets in the Connections Inspector. With the prototype cell selected, open the Attributes Inspector and set the identifier of the prototype cell to ToDoCell. This identifier will serve as the cell's reuse identifier. The prototype cell's layout should look something like the screenshot below.

Create a new UIViewController subclass and name it TSPAddToDoViewController. Declare an outlet textField of type UITextField in the view controller's header file and conform the view controller to the UITextFieldDelegate protocol.

#import <UIKit/UIKit.h>

@interface TSPAddToDoViewController : UIViewController <UITextFieldDelegate>

@property (weak, nonatomic) IBOutlet UITextField *textField;

@end

Before we add the view controller to the storyboard, add the following two actions to the view controller's implementation file.

#pragma mark -
#pragma mark Actions
- (IBAction)cancel:(id)sender {
}

- (IBAction)save:(id)sender {
    
}

Open the storyboard one more time and add a bar button item with an identifier of Add to the right of the navigation bar of the TSPViewController. Add a new view controller to the storyboard and set its class to TSPAddToDoViewController in the Identity Inspector. With the view controller selected, choose Embed In > Navigation Controller from the Editor menu.

The new view controller should now have a navigation bar. Add two bar button items to the navigation bar, one on the left with an identity of Cancel and one on the right with an identity of Save. Connect the cancel: action to the left bar button item and the save: action to the right bar button item.

Add a UITextField object to the view controller's view and position it 20 points below the navigation bar. The text field should remain at 20 points below the navigation bar. Note that the layout constraint at the top references the top layout guide, a very convenient addition that was added in iOS 7.

Connect the text field with the corresponding outlet in the view controller and set the view controller as the text field's delegate. Finally, control drag from the bar button item of the TSPViewController to the navigation controller of which the TSPAddToDoViewController is the root view controller. Set the segue type to modal and set the segue's identifier to addToDoViewController in the Attributes Inspector. That was a lot to take in. The interesting part is yet to come though.

Step 2: Implementing the Table View

Before we can take our application for a spin, we need to implement the UITableViewDataSource protocol in the TSPViewController class. However, this is where the NSFetchedResultsController class comes into play. To make sure that everything is working return 0 from the tableView:numberOfRowsInSection: method. This will result in an empty table view, but it will allow us to run the application without running into a crash.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 0;
}

Step 3: Save & Cancel

Open the TSPAddToDoViewController class and implement the cancel: and save: methods as shown below. We'll update their implementations later in this tutorial.

#pragma mark -
#pragma mark Actions
- (IBAction)cancel:(id)sender {
    // Dismiss View Controller
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (IBAction)save:(id)sender {
    // Dismiss View Controller
    [self dismissViewControllerAnimated:YES completion:nil];
}

Build and run the application in the iOS Simulator to see if everything is wired up correctly. You should be able to tap the add button in the top right to bring up the TSPAddToDoViewController and dismiss the latter by tapping the cancel or save button.

6. Implementing the NSFetchedResultsController Class

The NSFetchedResultsController class is part of the Core Data framework and it's meant to manage the results of a fetch request. The class was designed to work seamlessly with UITableView and UICollectionView on iOS and NSTableView on OS X. However, it can be used for other purposes as well.

Step 1: Laying the Groundwork

Before we can start working with the NSFetchedResultsController class, however, the TSPViewController class needs access to an NSManagedObjectContext instance, the NSManagedObjectContext instance we created earlier in the application delegate. Start by declaring a property managedObjectContext of type NSManagedObjectContext in the header file of the TSPViewController class.

#import <UIKit/UIKit.h>

@interface TSPViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>

@property (weak, nonatomic) IBOutlet UITableView *tableView;

@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;

@end

Open Main.storyboard, select the storyboard's initial view controller, an UINavigationController instance, and set its Storyboard ID to rootNavigationController in the Identity Inspector.

In the application delegate's application:didFinishLaunchingWithOptions: method, we get a reference to the TSPViewController instance, the root view controller of the navigation controller, and set its managedObjectContext property. The updated application:didFinishLaunchingWithOptions: method looks as follows:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Fetch Main Storyboard
    UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:@"Main" bundle: nil];
    // Instantiate Root Navigation Controller
    UINavigationController *rootNavigationController = (UINavigationController *)[mainStoryboard instantiateViewControllerWithIdentifier:@"rootNavigationController"];
    // Configure View Controller
    TSPViewController *viewController = (TSPViewController *)[rootNavigationController topViewController];
    if ([viewController isKindOfClass:[TSPViewController class]]) {
        [viewController setManagedObjectContext:self.managedObjectContext];
    }
    // Configure Window
    [self.window setRootViewController:rootNavigationController];
    return YES;
}

To make sure that everything is working, add the following log statement to the viewDidLoad method of the TSPViewController class.

#pragma mark -
#pragma mark View Life Cycle
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%@", self.managedObjectContext);
}

Step 2: Initializing the NSFetchedResultsController Instance

Open the implementation file of the TSPViewController class and declare a property of type NSFetchedResultsController in a private class extension. Name the property fetchedResultsController. An NSFetchedResultsController instance also has a delegate property that needs to conform to the NSFetchedResultsControllerDelegate protocol. Because the TSPViewController instance will serve as the delegate of the NSFetchedResultsController instance, we need to conform the TSPViewController class to the NSFetchedResultsControllerDelegate protocol as shown below.

#import "TSPViewController.h"

#import <CoreData/CoreData.h>

@interface TSPViewController () <NSFetchedResultsControllerDelegate>

@property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController;

@end

It's time to initialize the NSFetchedResultsController instance. The heart of a fetched results controller is the NSFetchRequest object, because it determines which records the fetched results controller will manage. In the view controller's viewDidLoad method, we initialize the fetch request by passing @"TSPItem" to the initWithEntityName: method. This should be familiar by now and so is the next line in which we add sort descriptors to the fetch request to sort the results based on the value of the createdAt attribute of each record.

#pragma mark -
#pragma mark View Life Cycle
- (void)viewDidLoad {
    [super viewDidLoad];
    // Initialize Fetch Request
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"TSPItem"];
    // Add Sort Descriptors
    [fetchRequest setSortDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"createdAt" ascending:YES]]];
    // Initialize Fetched Results Controller
    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
    // Perform Fetch
    NSError *error = nil;
    [self.fetchedResultsController performFetch:&error];
    if (error) {
        NSLog(@"Unable to perform fetch.");
        NSLog(@"%@, %@", error, error.localizedDescription);
    }
}

The initialization of the fetched results controller is pretty straightforward. The initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName: method takes four arguments:

  • the fetch request
  • the managed object context the fetched results controller will be monitoring
  • a section key path if you want the results to be split up into sections
  • a cache name if you want to enable caching

We pass nil for the last two parameters for now. The first argument is obvious, but why do we need to pass a NSManagedObjectContext object as well? Not only is the passed in managed object context used to execute the fetch request, it is also the managed object context that the fetched results controller will be monitoring for changes. This will become clearer in a few minutes when we start implementing the delegate methods of the NSFetchedResultsControllerDelegate protocol.

Finally, we need to tell the fetched results controller to execute the fetch request we passed it. We do this by invoking performFetch: and pass it a pointer to an NSError object. This is similar to the executeFetchRequest:error: method of the NSManagedObjectContext class.

Step 3: Implementing the Delegate Protocol

With the fetched results controller set up and ready to use, we need to implement the NSFetchedResultsControllerDelegate protocol. The protocol defines five methods, three of which are of interest to us in this tutorial:

  • controllerWillChangeContent:
  • controllerDidChangeContent:
  • controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:

The first and second methods tell us when the data the fetched results controller is managing will and did change. This is important to batch update the user interface. For example, it's perfectly possible that multiple changes occur at the same time. Instead of updating the user interface for every change, we batch update the user interface once every change has been made.

In our example, this boils down to the following implementations of controllerWillChangeContent: and controllerDidChangeContent:.

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}

The implementation of controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: is a bit trickier. This delegate method takes no less than five arguments:

  • the NSFetchedResultsController instance
  • the NSManagedObject instance that changed
  • the current index path of the record in the fetched results controller
  • the type of change, that is, insert, update, or delete
  • the new index path of the record in the fetched results controller, after the change

Note that the index paths have nothing to do with our table view. An NSIndexPath is nothing more than an object that contains one or more indexes to represent a path in a hierarchical structure, hence the class's name.

Internally the fetched results controller manages a hierarchical data structure and it notifies its delegate when that data structure changes. It's up to us to visualize those changes in, for example, a table view.

The implementation of controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: looks daunting, but let me walk you through it.

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
    switch (type) {
        case NSFetchedResultsChangeInsert: {
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        }
        case NSFetchedResultsChangeDelete: {
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        }
        case NSFetchedResultsChangeUpdate: {
            [self configureCell:[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;
        }
        case NSFetchedResultsChangeMove: {
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        }
    }
}

There are four possible change types:

  • NSFetchedResultsChangeInsert
  • NSFetchedResultsChangeDelete
  • NSFetchedResultsChangeUpdate
  • NSFetchedResultsChangeMove

The names are pretty self-explanatory. If the type is NSFetchedResultsChangeInsert, we tell the table view to insert a row at newIndexPath. Similarly, if the type is NSFetchedResultsChangeDelete, we remove the row at indexPath from the table view.

If a record is updated, we update the corresponding row in the table view by invoking configureCell:atIndexPath:, a helper method that accepts a UITableViewCell object and an NSIndexPath object. We'll implement this method in a moment.

If the change type is equal to NSFetchedResultsChangeMove, we remove the row at indexPath and insert a row at newIndexPath to reflect the record's updated position in the fetched results controller's internal data structure.

Step 4: Implementing the UITableViewDataSource Protocol

That wasn't too difficult. Was it? Implementing the UITableViewDataSource protocol is much easier, but there are a few things you should be aware of. Let's start with the numberOfSectionsInTableView: and tableView:numberOfRowsInSection: methods.

Even though the table view in our sample application will only have one section, let's ask the fetched results controller for the number of sections. We do this by calling sections on it, which returns an array of objects that conform to the NSFetchedResultsSectionInfo protocol.

Objects conforming to the NSFetchedResultsSectionInfo protocol need to implement a few methods, including numberOfObjects. This gives us what we need to implement the first two methods of the UITableViewDataSource protocol.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [[self.fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    NSArray *sections = [self.fetchedResultsController sections];
    id<NSFetchedResultsSectionInfo> sectionInfo = [sections objectAtIndex:section];
    return [sectionInfo numberOfObjects];
}

Next up is the tableView:cellForRowAtIndexPath: and configureCell:atIndexPath: methods. Start by adding an import statement for the header of the TSPToDoCell class.

#import "TSPToDoCell.h"

The implementation of tableView:cellForRowAtIndexPath: is short, because we move most of the cell's configuration to configureCell:atIndexPath:. We ask the table view for a reusable cell with reuse identifier @"ToDoCell" and pass the cell and the index path to configureCell:atIndexPath:.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TSPToDoCell *cell = (TSPToDoCell *)[tableView dequeueReusableCellWithIdentifier:@"ToDoCell" forIndexPath:indexPath];
    // Configure Table View Cell
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

The magic happens in configureCell:atIndexPath:. We ask the fetched results controller for the item at indexPath. The fetched results controller returns an NSManagedObject instance to us. We update the nameLabel and the state of the doneButton by asking the record for its name and done attributes.

- (void)configureCell:(TSPToDoCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    // Fetch Record
    NSManagedObject *record = [self.fetchedResultsController objectAtIndexPath:indexPath];
    // Update Cell
    [cell.nameLabel setText:[record valueForKey:@"name"]];
    [cell.doneButton setSelected:[[record valueForKey:@"done"] boolValue]];
}

We'll revisit the UITableViewDataSource protocol later in this tutorial when we delete items from the list. We first need to populate the table view with some data.

7. Adding Records

Let's finish this tutorial by adding the ability to create to-do items. Open the TSPAddToDoViewController class, add an import statement for the Core Data framework, and declare a property managedObjectContext of type NSManagedObjectContext.

#import <UIKit/UIKit.h>

#import <CoreData/CoreData.h>

@interface TSPAddToDoViewController : UIViewController <UITextFieldDelegate>

@property (weak, nonatomic) IBOutlet UITextField *textField;

@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;

@end

Head back to the TSPViewController class and implement the prepareForSegue:sender: method. In this method, we set the managedObjectContext property of the TSPAddToDoViewController instance. If you've worked with storyboards before, then the implementation of prepareForSegue:sender: should be straightforward.

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"addToDoViewController"]) {
        // Obtain Reference to View Controller
        UINavigationController *nc = (UINavigationController *)[segue destinationViewController];
        TSPAddToDoViewController *vc = (TSPAddToDoViewController *)[nc topViewController];
        // Configure View Controller
        [vc setManagedObjectContext:self.managedObjectContext];
    }
}

If the user enters text in the text field of the TSPAddToDoViewController and taps the Save button, we create a new record, populate it with data, and save it. This logic goes into the save: method we created earlier.

- (IBAction)save:(id)sender {
    // Helpers
    NSString *name = self.textField.text;
    if (name && name.length) {
        // Create Entity
        NSEntityDescription *entity = [NSEntityDescription entityForName:@"TSPItem" inManagedObjectContext:self.managedObjectContext];
        // Initialize Record
        NSManagedObject *record = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];
        // Populate Record
        [record setValue:name forKey:@"name"];
        [record setValue:[NSDate date] forKey:@"createdAt"];
        // Save Record
        NSError *error = nil;
        if ([self.managedObjectContext save:&error]) {
            // Dismiss View Controller
            [self dismissViewControllerAnimated:YES completion:nil];
        } else {
            if (error) {
                NSLog(@"Unable to save record.");
                NSLog(@"%@, %@", error, error.localizedDescription);
            }
            // Show Alert View
            [[[UIAlertView alloc] initWithTitle:@"Warning" message:@"Your to-do could not be saved." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
        }
    } else {
        // Show Alert View
        [[[UIAlertView alloc] initWithTitle:@"Warning" message:@"Your to-do needs a name." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
    }
}

The save: method looks pretty impressive, but there's nothing in there that we haven't covered yet. We create a new managed object by passing an NSEntityDescription instance and a NSManagedObjectContext instance. We then populate the managed object with a name and date. If saving the managed object context is successful, we dismiss the view controller, otherwise we show an alert view. If the user taps the save button without entering any text, we also show an alert view.

Run the application and add a few items. I'm sure you agree that the NSFetchedResultsController class makes the process of adding items incredibly easy. It takes care of monitoring the managed object context for changes and we update the user interface, the table view of the TSPViewController class, based on what the NSFetchedResultsController instance tells us through the NSFetchedResultsControllerDelegate protocol.

Conclusion

In the next article, we'll finish our application by adding the ability to delete and update to-do items. It's important that you understand the concepts we discussed in this article. The way Core Data broadcasts the changes of a managed object context's state is essential so make sure you understand this before moving on.

2014-07-26T08:37:55.056Z2014-07-26T08:37:55.056ZBart Jacobs

Core Data from Scratch: NSFetchedResultsController

$
0
0
tag:code.tutsplus.com,2005:PostPresenter/cms-21681

In the previous installments of this series, we covered the fundamentals of the Core Data framework. It's time we put our knowledge to use by building a simple application powered by Core Data.

In this tutorial, we'll also meet another star player of the Core Data framework, the NSFetchedResultsController class. The application that we're about to create manages a list of to-do items. With the application, we can add, update, and delete to-do items. You'll quickly learn that the NSFetchedResultsController class makes this very easy to do.

1. Project Setup

Open Xcode, select New > Project... from the File menu, and choose the Single View Application template from the iOS > Application category.

Name the project Done, set Devices to iPhone, and tell Xcode where you want to save the project.

Because we chose the Single View Application template, Xcode hasn't created a Core Data setup for us. However, setting up the Core Data stack should be easy, if you've read the previous installments of this series.

2. Core Data Setup

Open the implementation file of the application delegate class, TSPAppDelegate.m, and declare three properties in a private class extension, managedObjectContextmanagedObjectModel, and persistentStoreCoordinator. If you're confused by this step, then I recommend you revisit the first article of this series, which covers the Core Data stack in detail.

Note that I've also added an import statement for the Core Data framework at the top of TSPAppDelegate.m.

#import "TSPAppDelegate.h"

#import <CoreData/CoreData.h>

@interface TSPAppDelegate ()

@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;

@end

As you may remember, the Core Data stack is set up lazily. This means that we instantiate the managed object context, the managed object model, and the persistent store coordinator the moment they are needed by the application. In other words, the aforementioned objects are instantiated in the getters of their corresponding properties. The following code snippets should look very familiar.

- (NSManagedObjectContext *)managedObjectContext {
    if (_managedObjectContext) {
        return _managedObjectContext;
    }
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator) {
        _managedObjectContext = [[NSManagedObjectContext alloc] init];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _managedObjectContext;
}
- (NSManagedObjectModel *)managedObjectModel {
    if (_managedObjectModel) {
        return _managedObjectModel;
    }
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Done" withExtension:@"momd"];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return _managedObjectModel;
}
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    if (_persistentStoreCoordinator) {
        return _persistentStoreCoordinator;
    }
    NSURL *applicationDocumentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    NSURL *storeURL = [applicationDocumentsDirectory URLByAppendingPathComponent:@"Done.sqlite"];
    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    return _persistentStoreCoordinator;
}

There are three things you should be aware of. First, the data model, which we'll create next, will be named Done.momd. Second, we'll name the backing store Done and it will be a SQLite database. Third, if the backing store is incompatible with the data model, we invoke abort, killing the application. As I mentioned earlier in this series, while this is fine during development, you should never call abort in production. We'll revisit migrations and incompatibility issues later in this series.

While our application won't crash if you try to run it, the Core Data stack won't get properly set up. The reason is simple, we haven't created a data model yet. Let's take care of that in the next step.

3. Creating the Data Model

Select New > File... from the File menu and choose Data Model from the iOS > Core Data category.

Name the file Done, double-check that it's added to the Done target, and tell Xcode where it needs to be saved.

The data model is going to be very simple. Create a new entity and name it TSPItem. Add three attributes to the entity, name of  type String, createdAt of type Date, and done of type Boolean.

Mark the attributes as required, not optional, and set the default value of the done attribute to NO.

The Core Data stack is set up and the data model is configured correctly. It's time to become familiar with a new class of the Core Data framework, the NSFetchedResultsController class.

4. Managing Data

This article isn't just about the NSFetchedResultsController class, it's about what the NSFetchedResultsController class does behind the scenes. Let me clarify what I mean by this.

If we were to build our application without the NSFetchedResultsController class, we would need to find a way to keep the application's user interface synchronized with the data managed by Core Data. Fortunately, Core Data has an elegant solution to this problem.

Whenever a record is inserted, updated, or deleted in a managed object context, the managed object context posts a notification through notification center. A managed object context can post three types of notifications:

  • NSManagedObjectContextObjectsDidChangeNotification: This notification is posted every time a record in the managed object context is inserted, updated, or deleted.
  • NSManagedObjectContextWillSaveNotification: This notification is posted by the managed object context before pending changes are committed to the backing store.
  • NSManagedObjectContextDidSaveNotification: This notification is posted by the managed object context immediately after pending changes have been committed to the backing store.

The contents of these notifications are identical, that is, the object property of the notification is the NSManagedObjectContext instance that posted the notification and the notification's userInfo dictionary contains the records that were inserted, updated, and deleted.

The gist is that it requires a fair amount of boilerplate code to keep the results of a fetch request up to date. In other words, if we were to create our application without using the NSFetchedResultsController class, we would have to implement a mechanism that monitored the managed object context for changes and update the user interface accordingly. Let's see how the NSFetchedResultsController can help us with this task.

5. Setting Up the User Interface

Working with the NSFetchedResultsController class is pretty easy. An instance of the NSFetchedResultsController class takes a fetch request and has a delegate object. The NSFetchedResultsController object makes sure that it keeps the results of the fetch request up to date by monitoring the managed object context the fetch request was executed by.

If the NSFetchedResultsController object is notified of any changes by the NSManagedObjectContext object of the fetch request, it notifies its delegate. You may be wondering how this is different from a view controller directly monitoring the NSManagedObjectContext object. The beauty of the NSFetchedResultsController class is that it processes the notifications it receives from the NSManagedObjectContext object and tells the delegate only what it needs to know to update the user interface in response to these changes. The methods of the NSFetchedResultsControllerDelegate protocol should clarify this.

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller;
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type;
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath;

The signatures of the above delegate methods reveal the true purpose of the NSFetchedResultsController class. On iOS, the NSFetchedResultsController class was designed to manage the data displayed by a UITableView or a UICollectionView. It tells its delegate exactly which records changed, how to update the user interface, and when to do this.

Don't worry if you're still unsure about the purpose or advantages of the NSFetchedResultsController class. It'll make more sense once we've implemented the NSFetchedResultsControllerDelegate protocol. Let's revisit our application and put the NSFetchedResultsController class to use.

Step 1: Populating the Storyboard

Open the project's main storyboard, Main.storyboard, select the View Controller Scene, and embed it in a navigation controller by selecting Embed In > Navigation Controller from the Editor menu.

Drag a UITableView object in the View Controller Scene, create an outlet in the TSPViewController class, and connect it in the storyboard. Don't forget to make the TSPViewController class conform to the UITableViewDataSource and UITableViewDelegate protocols.

#import <UIKit/UIKit.h>

@interface TSPViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>

@property (weak, nonatomic) IBOutlet UITableView *tableView;

@end

Select the table view, open the Connections Inspector, and connect the table view's dataSource and delegate outlets to the View Controller object. With the table view selected, open the Attributes Inspector and set the number of Prototype Cells to 1.

Before we continue, we need to create a UITableViewCell subclass for the prototype cell. Create a new Objective-C subclass, TSPToDoCell, and set its superclass to UITableViewCell. Create two outlets, nameLabel of type UILabel and doneButton of type UIButton.

Head back to the storyboard, select the prototype cell in the table view, and set the class to TSPToDoCell in the Identity Inspector. Add a UILabel and a UIButton object to the cell's content view and connect the outlets in the Connections Inspector. With the prototype cell selected, open the Attributes Inspector and set the identifier of the prototype cell to ToDoCell. This identifier will serve as the cell's reuse identifier. The prototype cell's layout should look something like the screenshot below.

Create a new UIViewController subclass and name it TSPAddToDoViewController. Declare an outlet textField of type UITextField in the view controller's header file and conform the view controller to the UITextFieldDelegate protocol.

#import <UIKit/UIKit.h>

@interface TSPAddToDoViewController : UIViewController <UITextFieldDelegate>

@property (weak, nonatomic) IBOutlet UITextField *textField;

@end

Before we add the view controller to the storyboard, add the following two actions to the view controller's implementation file.

#pragma mark -
#pragma mark Actions
- (IBAction)cancel:(id)sender {
}

- (IBAction)save:(id)sender {
    
}

Open the storyboard one more time and add a bar button item with an identifier of Add to the right of the navigation bar of the TSPViewController. Add a new view controller to the storyboard and set its class to TSPAddToDoViewController in the Identity Inspector. With the view controller selected, choose Embed In > Navigation Controller from the Editor menu.

The new view controller should now have a navigation bar. Add two bar button items to the navigation bar, one on the left with an identity of Cancel and one on the right with an identity of Save. Connect the cancel: action to the left bar button item and the save: action to the right bar button item.

Add a UITextField object to the view controller's view and position it 20 points below the navigation bar. The text field should remain at 20 points below the navigation bar. Note that the layout constraint at the top references the top layout guide, a very convenient addition that was added in iOS 7.

Connect the text field with the corresponding outlet in the view controller and set the view controller as the text field's delegate. Finally, control drag from the bar button item of the TSPViewController to the navigation controller of which the TSPAddToDoViewController is the root view controller. Set the segue type to modal and set the segue's identifier to addToDoViewController in the Attributes Inspector. That was a lot to take in. The interesting part is yet to come though.

Step 2: Implementing the Table View

Before we can take our application for a spin, we need to implement the UITableViewDataSource protocol in the TSPViewController class. However, this is where the NSFetchedResultsController class comes into play. To make sure that everything is working return 0 from the tableView:numberOfRowsInSection: method. This will result in an empty table view, but it will allow us to run the application without running into a crash.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 0;
}

Step 3: Save & Cancel

Open the TSPAddToDoViewController class and implement the cancel: and save: methods as shown below. We'll update their implementations later in this tutorial.

#pragma mark -
#pragma mark Actions
- (IBAction)cancel:(id)sender {
    // Dismiss View Controller
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (IBAction)save:(id)sender {
    // Dismiss View Controller
    [self dismissViewControllerAnimated:YES completion:nil];
}

Build and run the application in the iOS Simulator to see if everything is wired up correctly. You should be able to tap the add button in the top right to bring up the TSPAddToDoViewController and dismiss the latter by tapping the cancel or save button.

6. Implementing the NSFetchedResultsController Class

The NSFetchedResultsController class is part of the Core Data framework and it's meant to manage the results of a fetch request. The class was designed to work seamlessly with UITableView and UICollectionView on iOS and NSTableView on OS X. However, it can be used for other purposes as well.

Step 1: Laying the Groundwork

Before we can start working with the NSFetchedResultsController class, however, the TSPViewController class needs access to an NSManagedObjectContext instance, the NSManagedObjectContext instance we created earlier in the application delegate. Start by declaring a property managedObjectContext of type NSManagedObjectContext in the header file of the TSPViewController class.

#import <UIKit/UIKit.h>

@interface TSPViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>

@property (weak, nonatomic) IBOutlet UITableView *tableView;

@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;

@end

Open Main.storyboard, select the storyboard's initial view controller, an UINavigationController instance, and set its Storyboard ID to rootNavigationController in the Identity Inspector.

In the application delegate's application:didFinishLaunchingWithOptions: method, we get a reference to the TSPViewController instance, the root view controller of the navigation controller, and set its managedObjectContext property. The updated application:didFinishLaunchingWithOptions: method looks as follows:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Fetch Main Storyboard
    UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:@"Main" bundle: nil];
    // Instantiate Root Navigation Controller
    UINavigationController *rootNavigationController = (UINavigationController *)[mainStoryboard instantiateViewControllerWithIdentifier:@"rootNavigationController"];
    // Configure View Controller
    TSPViewController *viewController = (TSPViewController *)[rootNavigationController topViewController];
    if ([viewController isKindOfClass:[TSPViewController class]]) {
        [viewController setManagedObjectContext:self.managedObjectContext];
    }
    // Configure Window
    [self.window setRootViewController:rootNavigationController];
    return YES;
}

To make sure that everything is working, add the following log statement to the viewDidLoad method of the TSPViewController class.

#pragma mark -
#pragma mark View Life Cycle
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%@", self.managedObjectContext);
}

Step 2: Initializing the NSFetchedResultsController Instance

Open the implementation file of the TSPViewController class and declare a property of type NSFetchedResultsController in a private class extension. Name the property fetchedResultsController. An NSFetchedResultsController instance also has a delegate property that needs to conform to the NSFetchedResultsControllerDelegate protocol. Because the TSPViewController instance will serve as the delegate of the NSFetchedResultsController instance, we need to conform the TSPViewController class to the NSFetchedResultsControllerDelegate protocol as shown below.

#import "TSPViewController.h"

#import <CoreData/CoreData.h>

@interface TSPViewController () <NSFetchedResultsControllerDelegate>

@property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController;

@end

It's time to initialize the NSFetchedResultsController instance. The heart of a fetched results controller is the NSFetchRequest object, because it determines which records the fetched results controller will manage. In the view controller's viewDidLoad method, we initialize the fetch request by passing @"TSPItem" to the initWithEntityName: method. This should be familiar by now and so is the next line in which we add sort descriptors to the fetch request to sort the results based on the value of the createdAt attribute of each record.

#pragma mark -
#pragma mark View Life Cycle
- (void)viewDidLoad {
    [super viewDidLoad];
    // Initialize Fetch Request
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"TSPItem"];
    // Add Sort Descriptors
    [fetchRequest setSortDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"createdAt" ascending:YES]]];
    // Initialize Fetched Results Controller
    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
    // Perform Fetch
    NSError *error = nil;
    [self.fetchedResultsController performFetch:&error];
    if (error) {
        NSLog(@"Unable to perform fetch.");
        NSLog(@"%@, %@", error, error.localizedDescription);
    }
}

The initialization of the fetched results controller is pretty straightforward. The initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName: method takes four arguments:

  • the fetch request
  • the managed object context the fetched results controller will be monitoring
  • a section key path if you want the results to be split up into sections
  • a cache name if you want to enable caching

We pass nil for the last two parameters for now. The first argument is obvious, but why do we need to pass a NSManagedObjectContext object as well? Not only is the passed in managed object context used to execute the fetch request, it is also the managed object context that the fetched results controller will be monitoring for changes. This will become clearer in a few minutes when we start implementing the delegate methods of the NSFetchedResultsControllerDelegate protocol.

Finally, we need to tell the fetched results controller to execute the fetch request we passed it. We do this by invoking performFetch: and pass it a pointer to an NSError object. This is similar to the executeFetchRequest:error: method of the NSManagedObjectContext class.

Step 3: Implementing the Delegate Protocol

With the fetched results controller set up and ready to use, we need to implement the NSFetchedResultsControllerDelegate protocol. The protocol defines five methods, three of which are of interest to us in this tutorial:

  • controllerWillChangeContent:
  • controllerDidChangeContent:
  • controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:

The first and second methods tell us when the data the fetched results controller is managing will and did change. This is important to batch update the user interface. For example, it's perfectly possible that multiple changes occur at the same time. Instead of updating the user interface for every change, we batch update the user interface once every change has been made.

In our example, this boils down to the following implementations of controllerWillChangeContent: and controllerDidChangeContent:.

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}

The implementation of controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: is a bit trickier. This delegate method takes no less than five arguments:

  • the NSFetchedResultsController instance
  • the NSManagedObject instance that changed
  • the current index path of the record in the fetched results controller
  • the type of change, that is, insert, update, or delete
  • the new index path of the record in the fetched results controller, after the change

Note that the index paths have nothing to do with our table view. An NSIndexPath is nothing more than an object that contains one or more indexes to represent a path in a hierarchical structure, hence the class's name.

Internally the fetched results controller manages a hierarchical data structure and it notifies its delegate when that data structure changes. It's up to us to visualize those changes in, for example, a table view.

The implementation of controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: looks daunting, but let me walk you through it.

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
    switch (type) {
        case NSFetchedResultsChangeInsert: {
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        }
        case NSFetchedResultsChangeDelete: {
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        }
        case NSFetchedResultsChangeUpdate: {
            [self configureCell:(TSPToDoCell *)[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;
        }
        case NSFetchedResultsChangeMove: {
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        }
    }
}

There are four possible change types:

  • NSFetchedResultsChangeInsert
  • NSFetchedResultsChangeDelete
  • NSFetchedResultsChangeUpdate
  • NSFetchedResultsChangeMove

The names are pretty self-explanatory. If the type is NSFetchedResultsChangeInsert, we tell the table view to insert a row at newIndexPath. Similarly, if the type is NSFetchedResultsChangeDelete, we remove the row at indexPath from the table view.

If a record is updated, we update the corresponding row in the table view by invoking configureCell:atIndexPath:, a helper method that accepts a UITableViewCell object and an NSIndexPath object. We'll implement this method in a moment.

If the change type is equal to NSFetchedResultsChangeMove, we remove the row at indexPath and insert a row at newIndexPath to reflect the record's updated position in the fetched results controller's internal data structure.

Step 4: Implementing the UITableViewDataSource Protocol

That wasn't too difficult. Was it? Implementing the UITableViewDataSource protocol is much easier, but there are a few things you should be aware of. Let's start with the numberOfSectionsInTableView: and tableView:numberOfRowsInSection: methods.

Even though the table view in our sample application will only have one section, let's ask the fetched results controller for the number of sections. We do this by calling sections on it, which returns an array of objects that conform to the NSFetchedResultsSectionInfo protocol.

Objects conforming to the NSFetchedResultsSectionInfo protocol need to implement a few methods, including numberOfObjects. This gives us what we need to implement the first two methods of the UITableViewDataSource protocol.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [[self.fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    NSArray *sections = [self.fetchedResultsController sections];
    id<NSFetchedResultsSectionInfo> sectionInfo = [sections objectAtIndex:section];
    return [sectionInfo numberOfObjects];
}

Next up is the tableView:cellForRowAtIndexPath: and configureCell:atIndexPath: methods. Start by adding an import statement for the header of the TSPToDoCell class.

#import "TSPToDoCell.h"

The implementation of tableView:cellForRowAtIndexPath: is short, because we move most of the cell's configuration to configureCell:atIndexPath:. We ask the table view for a reusable cell with reuse identifier @"ToDoCell" and pass the cell and the index path to configureCell:atIndexPath:.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TSPToDoCell *cell = (TSPToDoCell *)[tableView dequeueReusableCellWithIdentifier:@"ToDoCell" forIndexPath:indexPath];
    // Configure Table View Cell
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

The magic happens in configureCell:atIndexPath:. We ask the fetched results controller for the item at indexPath. The fetched results controller returns an NSManagedObject instance to us. We update the nameLabel and the state of the doneButton by asking the record for its name and done attributes.

- (void)configureCell:(TSPToDoCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    // Fetch Record
    NSManagedObject *record = [self.fetchedResultsController objectAtIndexPath:indexPath];
    // Update Cell
    [cell.nameLabel setText:[record valueForKey:@"name"]];
    [cell.doneButton setSelected:[[record valueForKey:@"done"] boolValue]];
}

We'll revisit the UITableViewDataSource protocol later in this tutorial when we delete items from the list. We first need to populate the table view with some data.

7. Adding Records

Let's finish this tutorial by adding the ability to create to-do items. Open the TSPAddToDoViewController class, add an import statement for the Core Data framework, and declare a property managedObjectContext of type NSManagedObjectContext.

#import <UIKit/UIKit.h>

#import <CoreData/CoreData.h>

@interface TSPAddToDoViewController : UIViewController <UITextFieldDelegate>

@property (weak, nonatomic) IBOutlet UITextField *textField;

@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;

@end

Head back to the TSPViewController class and implement the prepareForSegue:sender: method. In this method, we set the managedObjectContext property of the TSPAddToDoViewController instance. If you've worked with storyboards before, then the implementation of prepareForSegue:sender: should be straightforward.

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"addToDoViewController"]) {
        // Obtain Reference to View Controller
        UINavigationController *nc = (UINavigationController *)[segue destinationViewController];
        TSPAddToDoViewController *vc = (TSPAddToDoViewController *)[nc topViewController];
        // Configure View Controller
        [vc setManagedObjectContext:self.managedObjectContext];
    }
}

If the user enters text in the text field of the TSPAddToDoViewController and taps the Save button, we create a new record, populate it with data, and save it. This logic goes into the save: method we created earlier.

- (IBAction)save:(id)sender {
    // Helpers
    NSString *name = self.textField.text;
    if (name && name.length) {
        // Create Entity
        NSEntityDescription *entity = [NSEntityDescription entityForName:@"TSPItem" inManagedObjectContext:self.managedObjectContext];
        // Initialize Record
        NSManagedObject *record = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];
        // Populate Record
        [record setValue:name forKey:@"name"];
        [record setValue:[NSDate date] forKey:@"createdAt"];
        // Save Record
        NSError *error = nil;
        if ([self.managedObjectContext save:&error]) {
            // Dismiss View Controller
            [self dismissViewControllerAnimated:YES completion:nil];
        } else {
            if (error) {
                NSLog(@"Unable to save record.");
                NSLog(@"%@, %@", error, error.localizedDescription);
            }
            // Show Alert View
            [[[UIAlertView alloc] initWithTitle:@"Warning" message:@"Your to-do could not be saved." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
        }
    } else {
        // Show Alert View
        [[[UIAlertView alloc] initWithTitle:@"Warning" message:@"Your to-do needs a name." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
    }
}

The save: method looks pretty impressive, but there's nothing in there that we haven't covered yet. We create a new managed object by passing an NSEntityDescription instance and a NSManagedObjectContext instance. We then populate the managed object with a name and date. If saving the managed object context is successful, we dismiss the view controller, otherwise we show an alert view. If the user taps the save button without entering any text, we also show an alert view.

Run the application and add a few items. I'm sure you agree that the NSFetchedResultsController class makes the process of adding items incredibly easy. It takes care of monitoring the managed object context for changes and we update the user interface, the table view of the TSPViewController class, based on what the NSFetchedResultsController instance tells us through the NSFetchedResultsControllerDelegate protocol.

Conclusion

In the next article, we'll finish our application by adding the ability to delete and update to-do items. It's important that you understand the concepts we discussed in this article. The way Core Data broadcasts the changes of a managed object context's state is essential so make sure you understand this before moving on.

2014-07-26T08:37:55.056Z2014-07-26T08:37:55.056ZBart Jacobs

An Introduction to Swift: Part 2

$
0
0

In the first article of this introductory series on Swift, we talked about Swift's philosophy, took a first look at its syntax, and highlighted a few key differences with Objective-C. In this article, we continue our exploration of Swift's syntax. You'll also learn about optionals and see how memory management works in Swift.

1. Conditionals and Loops

If

If statements are identical in Swift and Objective-C with the exception of two subtle differences:

  • parentheses around the condition variable are optional
  • curly braces are required

These are about the only differences with if statements in Objective-C.

Ranges

As we saw in the first article, Swift includes two range operators ..< and ... to specify a range of values. These two operators are the half-closed range operator and the closed range operator.

A half-closed range, such as 1..<5, represents the values 1, 2, 3, and 4, excluding 5. A closed range, such as 1...5, represents the values 1, 2, 3, 4, and includes 5.

Ranges can be used in for loops, array subscript, and even in switch statements. Take a look at the following examples.

// for loop example
for i in 1..<10 {
}
// iterates from 1 to 9
// array subscript example
let someArray = ["apple","pair","peach","watermelon","strawberry"]
for fruit in someArray[2..<4] {
    println(fruit)
}
// outputs: peach and watermelon
// switch example
switch someInt {
    case 0:
    // do something with 0
    case 1..<5:
    // do something with 1,2,3,4
    case 5...10:
    // do something with 5,6,7,8,9,10
    default:
    // everything else
}

Switch

Switch statements are more powerful in Swift than they are in Objective-C. In Objective-C,  the result of the expression of a switch statement needs to be of type integer and the values of each case statement should be a constant or a constant expression. This isn't true in Swift. In Swift, the case statements can be of any type, including ranges.

In Swift, switch has no break statements and it doesn't automatically fall through from one case to another. When writing a switch statement, care must be taken that all conditions are being handled by its case statements, failing to do so will result in a compiler error. A sure way to cover all conditions is to include a default case statement.

Here's an example of a switch statement with String cases:

let vegetable = "red pepper"
switch vegetable {
case "celery":
    let vegetableComment = "Add some raisins and make ants on a log."
case "cucumber", "watercress":
    let vegetableComment = "That would make a good tea sandwich."
default:
    let vegetableComment = "Everything tastes good in soup."
}

In Swift, case statements don't fall through by default. This was a deliberate design decision to avoid common errors. If a specific case needs to fall through, you can use the fallthrough keyword to indicate this to the compiler.

switch someInt {
    case 0:
    // do something with 0
    case 1:
    // do something with 1
    case 2:
    // do something with 2
        fallthrough
    default:
    //do something for everything else
}

// case 2 will fall through to default case

It doesn't stop here. Swift adds two other features to switch, value bindings and the whereclause. Value binding is used with the case let keywords to bind a constant with the matching case. The where clause adds an extra condition to the case statement using the where keyword.

These two concepts are better explained with examples. The following code block shows how value binding works.

let somePoint = (xaxis:2, yaxis:0)
switch somePoint {
case (let x, 0):
    println("on the x-axis with an x value of \(x)")
case (0, let y):
    println("on the y-axis with a y value of \(y)")
case let (x, y):
    println("somewhere else at (\(x), \(y))")
}

The first case statement, case (let x, 0), will match the values where yaxis is equal to 0 and any value  for xaxis, and we bind xaxis to the constant x to be used inside the case statement.

Here's an example of the where clause in action.

let vegetable = "red pepper"
switch vegetable {
case "celery":
    println("Add some raisins and make ants on a log.")
case let x where x.hasSuffix("pepper"):
    println("I'm allergic to \(x)")
default:
    println( "Everything tastes good in soup.")
}

// outputs: I'm allergic to red pepper

2. Functions and Closures

Functions

Defining functions and closures is easy in Swift. Objective-C developers know them better as functions and blocks.

In Swift, function parameters can have default values, which is reminiscent of scripting languages, such as PHP and Ruby.

The syntax for functions is as follows:

func functionName(parameterName: Type = DefaultValue) -> returnType {
    [...]
    return returnType;
}

To write a sayHello function that takes a parameter name of type String and returns a Bool when successful, we write the following:

func sayHello(name: String) -> Bool {
    println("hello \(name)");
    return true;
}

sayHello("World")
// output
// hello World

To pass a default value for the name parameter, the function's implementation would look as follows:

func sayHello(name: String = "World") -> Bool {
    println("hello \(name)");
    return true;
}

sayHello()
// output
// hello World

sayHello("mike")
// output
// hello mike

A feature that is completely absent in Objective-C are tuples. In Swift, functions can return multiple values in the form of a tuple. Tuples are treated as a single variable, which means that you can pass it around just like a variable.

Tuples are very easy to use. In fact, we've already worked with tuples in the previous article when we enumerated a dictionary. In the next code snippet, the key/value pair is a tuple.

for (key,value) in someDictionary {
    println("Key \(key) has value \(value)"
}

How are tuples used and how do you benefit from them? Let's take a look at another example. Let's modify the above sayHello function to return a boolean when successful as well as the resulting message. We do this by returning a tuple, (Bool, String). The updated sayHello function looks like this:

func sayHello(name: String = "World") -> (Bool, String) {
    let greeting = "hello \(name)"
    return (true, greeting);
}

let (success, greeting) = sayHello()
println("sayHello returned success:\(success) with greeting: \(greeting)");
// output
// sayHello returned success:1 with greeting: hello World

Tuples have been on the wish list of many Objective-C programmers for a long time.

Another cool feature of tuples is that we can name the returned variables. If we revisit the previous example and give names to the variables of the tuple, we get the following:

func sayHello(name: String = "World") -> (success: Bool, greeting: String) {
    let greeting = "hello \(name)"
    return (true, greeting);
}

let status = sayHello()
println("sayHello returned success:\(status.success) with greeting: \(status.greeting)");
// output
// sayHello returned success:1 with greeting: hello World

This means that instead of defining a separate constant for each return element of a tuple, we can access the returned tuple elements using dot notation as shown in the above example, status.success and status.greeting.

Closures

Closures in Swift are the same as blocks in Objective-C. They can be defined inline, passed as a parameter, or returned by functions. We use them exactly as we would use blocks in Objective-C.

Defining closures is also easy. Actually, a function is a special case of closures. So it's no wonder that defining a closure looks a lot like defining a function.

Closures are a first-class type, which means that they can be passed and returned by functions just like any other type, such as IntStringBool, etc. Closures are essentially code blocks that can be called later and have access to the scope in which they were defined.

Creating an nameless closure is as simple as wrapping a block of code in curly braces. The parameters and return type of the closure are separated from the closure's body with the in keyword.

Let's say we want to define a closure that returns true if a number is even, then that closure could look something like:

let isEven = {
(number: Int) -> Bool in
let mod = number % 2
return (mod==0)
}

The isEven closure takes an Int as its single parameter and returns a Bool. The type of this closure is (number: Int) -> Bool, or (Int -> Bool) for short. We can call isEven anywhere in our code just like we would invoke a code block in Objective-C.

To pass a closure of this type as a parameter of a function, we use the closure's type in the function's definition:

let isEven = {
    (number: Int) -> Bool in
    let mod = number % 2;
    return (mod==0);
}

func verifyIfEven(number: Int, verifier:(Int->Bool)) ->Bool {
    return verifier(number);
}

verifyIfEven(12, isEven);
// returns true

verifyIfEven(19, isEven);
// returns false

In the above example, the verifier parameter of the verifyIfEven function is a closure that we pass to the function.

3. Classes & Structures

Classes

It's time to talk about the cornerstone of object-oriented programming, classes. Classes, as mentioned before, are defined in a single implementation file with a .swift extension. Property declarations and methods are all defined in that file.

We create a class with the class keyword followed by the name of the class. The class's implementation is wrapped in a pair of curly braces. As in Objective-C, the naming convention for classes is to use upper camel case for class names.

class Hotel {
    //properties
    //functions
}

To create an instance of the Hotel class we write:

let h = Hotel()

In Swift, there's no need to call init on objects as init is called automatically for us.

Class inheritance follows the same pattern as in Objective-C, a colon separates the class name and that of its superclass. In the following example, Hotel inherits from the BigHotel class.

class BigHotel: Hotel {

}

As in Objective-C, we use dot notation to access an object's properties. However, Swift also uses the dot notation to invoke class and instance methods as you can see below.

// Objective-C
UIView* view = [[UIView alloc] init];
[self.view addSubview:view];

// Swift
let view = UIView()
self.view.addSubview(view)

Properties

Another difference with Objective-C is that Swift doesn't distinguish between instance variables (ivars) and properties. An instance variable is a property.

Declaring a property is just like defining a variable or a constant, using the var and let keywords. The only difference is the context in which they are defined, that is, the context of a class.

class Hotel {
    let rooms = 10
    var fullRooms = 0
}

In the above example, rooms is an immutable value, a constant, set to 10 and fullRooms is a variable with an initial value of 0, which we can change later. The rule is that properties need to be initialized when they're declared. The only exception to this rule are optionals, which we'll discuss in a moment.

Computed Properties

The Swift language also defines computed properties. Computed properties are nothing more than fancy getters and setters that don't store a value. As their name indicates, they are computed or evaluated on the fly.

Below is an example of a computed property. I have changed the rooms property to a var for the rest of these examples. You'll find out why later.

class Hotel {
    var rooms = 10
    var fullRooms = 0
    var description: String {
        get {
            return "Size of Hotel: \(rooms) rooms capacity:\(fullRooms)/\(rooms)"
        }
    }
}

Because the description property is read-only and only has a return statement, we can omit the get keyword and curly braces, and only keep the return statement. This is shorthand and that's what I'll be using in the rest of this tutorial.

class Hotel {
    var rooms = 10
    var fullRooms = 0
    var description: String {
        return "Size of Hotel: \(rooms) rooms capacity:\(fullRooms)/\(rooms)"
    }    
}

We can also define read-write computed properties. In our class Hotel, we want an emptyRooms property that gets the number of empty rooms in the hotel, but we also want to update fullRooms when we set the emptyRooms computed property. We can do this by using the set keyword as shown below.

class Hotel {
    var rooms = 10
    var fullRooms = 0
    var description: String {
        return "Size of Hotel: \(rooms) rooms capacity:\(fullRooms)/\(rooms)"
    }    
    var emptyRooms :Int {
        get {
            return rooms - fullRooms
        }
        set {
            // newValue constant is available here 
            // containing the passed value
            if(newValue < rooms) {
                fullRooms = rooms - newValue
            } else {
                fullRooms = rooms
            }
        }
    }
}

let h = Hotel()
h.emptyRooms = 3
h.description
// Size of Hotel: 10 rooms capacity:7/10

In the emptyRooms setter, the newValue constant is handed to us and represents the value passed to the setter. It's also important to note that computed properties are always declared as variables, using the var keyword, because their computed value can change.

Methods

We've already covered functions earlier in this article. Methods are nothing more than functions that are tied to a type, such as a class. In the following example we implement an instance method, bookNumberOfRooms, in the Hotel class we created earlier.

class Hotel {
    var rooms = 10
    var fullRooms = 0
    var description: String {
        return "Size of Hotel: \(rooms) rooms capacity:\(fullRooms)/\(rooms)"
    }
    var emptyRooms :Int {
        get {
            return rooms - fullRooms
        }
        set {
            // newValue constant is available here
            // containing the passed value
            if(newValue < rooms) {
                fullRooms = rooms - newValue
            } else {
                fullRooms = rooms
            }
        }

    }
    func bookNumberOfRooms(room:Int = 1) -> Bool
    {
        if(self.emptyRooms>room) {
            self.fullRooms++;
            return true
        } else {
            return false
        }
    }
}

let h = Hotel()
h.emptyRooms = 7
h.description
//Size of Hotel: 10 rooms capacity:3/10
h.bookNumberOfRooms(room: 2)
// returns true
h.description
//Size of Hotel: 10 rooms capacity:5/10
h.bookNumberOfRoom()
// returns true
h.description
//Size of Hotel: 10 rooms capacity:6/10


Initializers

The default initializer for classes is init. In the init function, we set the initial values of the instance that is created.

For example, if we would need a Hotel subclass with 100 rooms, then we would need an initializer to set the rooms property to 100. Remember that I previously changed rooms from being a constant to being a variable in the Hotel class. The reason is that we cannot change inherited constants in a subclass, only inherited variables can be changed.

class BigHotel: Hotel {
    init() {
        super.init()
        rooms = 100
    }
}

let bh = BigHotel()
println(bh.description);
//Size of Hotel: 100 rooms capacity:0/100

Initializers can also take parameters. The following example shows you how this works.

class CustomHotel: Hotel {
    init(size:Int) {
        super.init()
        rooms = size
    }
}

let c = CustomHotel(size:20)
c.description
//Size of Hotel: 20 rooms capacity:0/20

Overriding Methods and Computed Properties

This is one of the coolest things in Swift. In Swift, a subclass can override both methods and computed properties. To do so, we use the override keyword. Let's override the description computed property in the CustomHotel class:

class CustomHotel: Hotel {
    init(size:Int) {
        super.init()
        rooms = size
    }
    override var description:String {
        return super.description + " Howdy!"
    }
}

let c = CustomHotel(size:20)
c.description
// Size of Hotel: 20 rooms capacity:0/20 Howdy!

The result is that description returns the result of the superclass's description method with the string "Howdy!" appended to it.

What's cool about overriding methods and computed properties is the override keyword. When the compiler sees the override keyword, it checks wether the class's superclass implements the method that's being overridden. The compiler also checks if the properties and methods of a class are in conflict with properties or methods higher up the inheritance tree.

I don't know how many times a typo in an overridden method in Objective-C made me curse, because the code wasn't working. In Swift, the compiler will tell you exactly what's wrong in these situations.

Structures

Structures, defined with the struct keyword, are more powerful in Swift than they are in C and Objective-C. In C, structs define only values and pointers. Swift structs are just like C structs, but they also support computed properties and methods.

Anything you can do with a class, you can do with a structure, with two important differences:

  • structures don't support inheritance like classes do
  • structures are passed around by value while classes are passed by reference

Here are a few examples of structures in Swift:

struct Rect {
    var origin: Point
    var size: Size
    var area: Double {
        return size.width * size.height
    }
    func isBiggerThanRect(r:Rect) -> Bool {
        return (self.area > r.area)
    }
}

struct Point {
    var x = 0
    var y = 0
}
    
struct Size {
    var width = 0
    var height = 0
}

4. Optionals

Solution To a Problem

Optionals are a new concept if you're coming from Objective-C. They solve a problem we all face as programmers. When we access a variable whose value we're not sure about, we usually return an indicator, known as a sentinel, to indicate that the returned value is a no-value. Let me illustrate this with an example from Objective-C:

NSString* someString = @"ABCDEF";
NSInteger pos = [someString rangeOfString:@"B"].location;

// pos = 1

In the above example, we are trying to find the position of @"B" in someString. If @"B" is found, its location or position is stored in pos. But what happens if @"B" isn't found in someString?

The documentation states that rangeOfString: returns an NSRange with location set to the NSNotFound constant. In the case of rangeOfString:, the sentinel is NSNotFound. Sentinels are used to indicate that the returned value is not valid.

In Cocoa, there are many uses of this concept, but the sentinel value differs from context to context, 0, -1, NULL, NSIntegerMax, INT_MAX, Nil, etc. The problem for the programmer is that she must remember which sentinel is used in which context. If the programmer isn't careful, she can mistake a valid value for a sentinel and vice versa. Swift solves this problem with optionals. To quote Brian Lanier "Optionals are the one sentinel to rule them all."

Optionals have two states, a nil state, which means the optional contains no value, and a second state, which means it holds a valid value. Think of optionals as a package with an indicator to tell you if the package's contents is valid or not.

Usage

All types in Swift can become an optional. We define an optional by adding a ? after the type declaration like so:

let someInt: Int?

// someInt == nil

We assign a value to an optional's package just like we do with constants and variables.

someInt = 10

// someInt! == 10

Remember that optionals are like packages. When we declared let someInt: Int?, we defined an empty box with a value of nil. By assigning the value 10 to the optional, the box contains an integer that is equal to 10 and its indicator or state becomes not nil.

To get to the contents of an optional we use the ! operator. We must be sure that the optional has a valid value before unwrapping it. Failing to do so will cause a runtime error. This is how we access the value stored in an optional:

if ( someInt != nil) {
    println("someInt: \(someInt!)")
} else {
    println("someInt has no value")
}

// someInt: 10

The above pattern is so common in Swift that we can simplify the above code block by using optional binding with the if let keywords. Take a look at the updated code block below.

if let value = someInt {
    println("someInt: \(value)")
} else {
    println("someInt has no value")
}

Optionals are the only type that can take a nil value. Constants and variables cannot be initialized or set to nil. This is part of Swift's safety policy, all non-optional variables and constants must have a value.

5. Memory Management

If you remember, back when ARC was introduced we were using the strong and weak keywords to define object ownership. Swift also has a strong and weak ownership model, but it also introduces a new one, unowned. Let's take a look at each object ownership model in Swift.

strong

Strong references are the default in Swift. Most of the time, we own the object that we're referencing and we are the ones responsible for keeping the referenced object alive.

Since strong references are the default, there is no need to explicitly keep a strong reference to an object, any reference is a strong reference.

weak

A weak reference in Swift indicates that the reference points to an object that we're not responsible for keeping alive. It's mainly used between two objects that don't need the other to be around in order for the object to continue its life cycle.

There is one but, however. In Swift, weak references must always be variables with an optional type, because they are set to nil when the referenced object is deallocated. The weak keyword is used to declare a variable as weak:

weak var view: UIView?

unowned

Unowned references are new for Objective-C programmers. An unowned reference means that we're not responsible for keeping the referenced object alive, just like weak references.

The difference with a weak reference is that an unowned reference is not set to nil when  the object it references is deallocated. Another important difference with weak references is that unowned references are defined as a non-optional type.

Unowned references can be constants. An unowned object doesn't exist without its owner  and therefore the unowned reference is never nil. Unowned references need the unowned keyword before the definition of the variable or constant.

unowned var view: UIView

Conclusion

Swift is an amazing language that has a lot of depth and potential. It's fun to write programs with and it removes a lot of the boilerplate code we write in Objective-C to make sure our code is safe.

I highly recommend The Swift Programming Language, which is available for free in Apple's iBooks Store.

2014-07-28T17:20:10.794Z2014-07-28T17:20:10.794ZMichael Musallam
Viewing all 1836 articles
Browse latest View live