Figure 1.
22.1 Going Universal in Xcode
A universal application is defined as one targeted at both the iPhone/iPod touch and iPad. If you want to build a universal iOS application then you have to use the runtime and Xcode. There is currently no way to target iPhone development from within Codea.
This is handy, since we plan to use our old staple Mine Sweeper to demonstrate how to turn your Codea App into one which can run on an iPhone (or iPod Touch for that matter). As we have added Game Center code to MineSweeper, all future development of this game will have to occur in Xcode.
In principle this is a simple three step process:
- Target the application to iPhone/iPad in the Xcode deployment build settings;
- Add the relevant iPhone graphical assets (icons, default images etc.);
- Use a bunch of if (device is an iPhone) then do one thing else do another thing. Step 2 is simplified by Apple providing a new macro called UI_USER_INTERFACE_IDIOM() that will tell you what device your code is currently running on. There are two values defined, UIUserInterfaceIdiomPhone and UIUserInterfaceIdiomPad, and this macro will return the appropriate value based on the current device.
It would be a very short tutorial if that was all there was to it. If your App has any sort of complexity then there are a few things that you need to watch out for.
22.2 Step 1: Targeting iPhone and iPad in Xcode
As with the previous few tutorials, we are assuming that you have already got your Codea App up and humming in the Xcode iPad simulator using the runtime. If you haven't got to this state then go back and have a look at Tutorial 12.
Targeting your App for iPhone and iPad is easy one you have found the setting. Open up Xcode with your App. You need to have the Project Navigator open (top left hand tool bar item - it looks like a file folder). Click on the item at the very top of the Navigator called "Codea Template."
When you create a new project, it includes one or more targets, where each target specifies one build product and the instructions for how that product is to be built. You can use the project editor to specify every aspect of the build, from the version of the SDK to specific compiler options.
In the next column over you will see two items, Project (CodeaTemplate) and Targets (MineSweeper - in our case, this will be the name of your App).
Selecting the project file opens the project settings editor. The project settings editor is Xcode 4′s replacement for Xcode 3′s project and target inspectors. Select the target from the project settings editor.
Figure 2. Setting your App to Universal.
To specify the device families on which you want your app to be able to run:
- In the project navigator, select the project.
- From the target list in the project editor, select the target that builds your app, and click Summary.
- From the Devices pop-up menu, choose Universal (to target both families - iPhone and iPad are the other two options - see Figure 2.).
22.3 Step 2: Adding iPhone Graphical Assets
Once you have made your app universal, Xcode will allow you to set up the icon and default images for your iPhone. Before we do this, here is a quick refresher on the screen resolutions that we need to consider.
iPad:
- 1024 x 768 px; and
- 2048 x 1536 px for Retina displays (@2x).
iPhone:
- 320 x 480 px;
- 640 x 960 px for Retina displays (@2x); and
- 640 x 1136 px for the iPhone 5 and iPod touch (5th Generation).
You will also need an icon in the 57 x 57 pixel size and 114 x 114 pixels (@2x for the retina version). These are fairly easy to scale from your iPad icons.
You can also use your default iPad images for your iPhone but they won't scale exactly (you will need to crop either the width or height) if you want to maintain the aspect ratio.
The easiest way to add the iPhone versions of your icon and default load images is to drag them from Finder onto the deployment information page in Xcode. To bring up this page, select the CodeaTemplate at the top of the left hand Project Navigator pane. Click on Targets (Minesweeper in our case) in the middle pane and Summary in the far right pane (see Figure 3.)
Figure 3. Adding the iPhone Graphical Assets
Once you have the screen shown in Figure 3, you can drag across the appropriate image files from Finder. If necessary, Xcode will rename the files and copy them into your project. You should now be able to run your application in the iPhone simulator and see the default image load and when you exit the app using the home button you will be able to see the appropriate icon. That may be all you need to do, but probably not.
22.4 Step 3: Code Modifications
To get our App running on the iPhone we need to make some changes. What you have to watch out for is any hard coded view sizes. Some of our graphical assets will have to be redone to suit the dimensions of the iPhone screen. Let's start by getting the splash screen to work.
In the CodifyAppDelegate of the runtime the base app window is created when the application finishes launching. The good news is that the following line of code will create a window which will fit either an iPhone or iPad.
self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
So there is nothing we need to do from a runtime perspective (unless you are using Game Center - see below). If we did need to modify anything in the Objective C portion of our code we could use the following pattern:
// Check if device is an iPhone or iPad
// We now also need to check whether we have a iPhone 5
// or 5th Generation iPod touch.
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
NSLog(@"App Delegate - iPad device detected.");
else if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
CGSize result = [[UIScreen mainScreen] bounds].size;
if (result.height == 480)
{
NSLog(@"App Delegate - iPhone/iPod Touch detected (< v5).");
}
if (result.height == 568)
{
NSLog(@"App Delegate - iPhone/iPod Touch detected (> v5).");
}
}
// Check if device is an iPhone or iPad
// We now also need to check whether we have a iPhone 5
// or 5th Generation iPod touch.
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
NSLog(@"App Delegate - iPad device detected.");
else if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
CGSize result = [[UIScreen mainScreen] bounds].size;
if (result.height == 480)
{
NSLog(@"App Delegate - iPhone/iPod Touch detected (< v5).");
}
if (result.height == 568)
{
NSLog(@"App Delegate - iPhone/iPod Touch detected (> v5).");
}
}
Paste a copy of this in the - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions method of the CodifyAppDelegate.m file if you want to try it out.
Now we do have to modify our Lua splash screen class as it loads background images which are too large for the iPhone screen. To assist with the conditional coding we need to add, we wrote a new helper function for the Lua portion of the code to determine what device we are running on. This was added to the Main tab, so that we know it will be available throughout the rest of the code. Depending on what resolution the forthcoming iPad mini ends up with, we might need to update this in the next couple of weeks.
-- This function will detect the Device which is in use,
-- required as our app is now universal
function deviceIsIpad()
if CurrentOrientation == LANDSCAPE_LEFT
or CurrentOrientation == LANDSCAPE_RIGHT then
if WIDTH == 1024 then
return true
else
return false
end
else
if WIDTH == 768 then
return true
else
return false
end
end
end
As the device isn't going to change mid program, you can call this function once and assign it to a global boolean if you are worried about performance. Performance isn't an issue for MineSweeper so we wont bother.
Using this function we can now conditionally load the background image depending on the device. In the function SplashScreen:init(splashText, fadeSpeed) we have modified the sprite loading as follows:
if deviceIsIpad() then
landscapeImage = readImage("Dropbox:MS_TitleScreen_1024x768")
portraitImage = readImage("Dropbox:MS_TitleScreen_768x1024")
else
landscapeImage = readImage("Dropbox:Default-Landscape~iphone")
portraitImage = readImage("Dropbox:Default-Portrait~iphone")
end
-- This function will detect the Device which is in use,
-- required as our app is now universal
function deviceIsIpad()
if CurrentOrientation == LANDSCAPE_LEFT
or CurrentOrientation == LANDSCAPE_RIGHT then
if WIDTH == 1024 then
return true
else
return false
end
else
if WIDTH == 768 then
return true
else
return false
end
end
end
As the device isn't going to change mid program, you can call this function once and assign it to a global boolean if you are worried about performance. Performance isn't an issue for MineSweeper so we wont bother.
Using this function we can now conditionally load the background image depending on the device. In the function SplashScreen:init(splashText, fadeSpeed) we have modified the sprite loading as follows:
if deviceIsIpad() then
landscapeImage = readImage("Dropbox:MS_TitleScreen_1024x768")
portraitImage = readImage("Dropbox:MS_TitleScreen_768x1024")
else
landscapeImage = readImage("Dropbox:Default-Landscape~iphone")
portraitImage = readImage("Dropbox:Default-Portrait~iphone")
end
Remember that if you need to add new images to your Dropbox folder on the Mac then you will have to do a clean build for Xcode to recognise them. If you don't do this you will get a sprite loading error. To find the folder on your Mac to add the images, right click on the Drop Box sprite pack folder in the Xcode Project Navigator (Frameworks -> Codea -> SpritePacks) and select "Show in Finder". You can then drag across the new images that you want to use.
The other change that we need to make is in the function SplashScreen:draw().
if deviceIsIpad() then
sprite(logoImage, WIDTH - logoImage.width - 20, 20)
text(self.splashText, WIDTH/2, HEIGHT/2 + 250 + portraitTextOffset)
fill(whiteColor)
fontSize(20)
text("Version: "..version, WIDTH/2, HEIGHT/2 + 200 + portraitTextOffset)
else
fontSize(42)
text(self.splashText, WIDTH/2, HEIGHT - 80)
fill(whiteColor)
fontSize(14)
text("Version: "..version, WIDTH/2, 50)
end
You will probably need to make similar adjustments in font size and composition to suit the smaller iPhone screen We also don't load the Reefwing Software logo if the device is an iPhone due to the space constraints. Make sure that the resulting view looks okay in both landscape and portrait orientations (command right arrow to change the orientation of the simulator in Xcode). As an aside you should find that the Fader class works as expected regardless of the device.
Now that you have the splash screen sorted we can turn our attention to the Menu screen. For us the changes here are minimal for the portrait orientation. We load up a smaller MineSweeper logo to fit on the iPhone screen and once again there is no room for the Reefwing and Codea logos so we dispense with them. The RoundBorder class works on all devices without any changes.
In the Main setup function where we load images we added:
-- Load the MineSweeper Text Logo
if deviceIsIpad() then
msTextLogo = readImage("Dropbox:minesweeper_logo 400x76px")
else
msTextLogo = readImage("Dropbox:minesweeper_logo 200x38px")
end
And then in drawMenu() we shuffle the button positions and eliminate the "Medium" and "Hard" game options since those grid sizes wont fit on an iPhone screen (see section 22.5 Lessons Learned). The upside of removing the "Medium" and "Hard" buttons is that the remaining three buttons fit easily on the iPhone screen in both portrait and landscape orientations. Since there is only one game difficulty available on the iPhone we also changed the button text from "Easy" to "Start".
The achievement and leader board Game Center functionality works fine on an iPad but due to the way these modal views are displayed, on an iPhone the Lua draw() loop gets stopped if you tap the "Achievements" or "High Score" buttons. This results in the app still running but the screen never gets refreshed. To prevent this from occurring we need to modify the aGameCenter_Codea.m file that we introduced in the previous Game Center tutorial. The fix is to call [[SharedRenderer renderer] startAnimation] when the Game Center view controller is dismissed. This method is in the BasicRendererViewController of the runtime. An example for dismissing achievements is shown below. The same is required for dismissing leader boards.
- (void)achievementViewControllerDidFinish:(GKAchievementViewController *)viewController
{
[[SharedRenderer renderer] dismissViewControllerAnimated:YES completion:nil];
[[SharedRenderer renderer] startAnimation];
}
This isn't a problem on the iPad because the Game Center view doesn't cover the whole screen (which causes the viewWillDisappear method to be called in the rendering code and stops animating the draw loop).
We have two more screens that we need to tweak and then we are done. Firstly the High Score screen which shows the scores stored locally is not particularly useful on the iPhone version (and is somewhat redundant as we are now using Game Center). So in the function scoreButtonPressed() we wont show this screen if we are an iPhone using the following code.
if deviceIsIpad() then
gameState = stateScore
end
Remember that the gameState determines what the draw() loop displays. By not setting this to stateScore if the device is an iPhone, this screen wont be displayed.
Our last job is to sort out the game playing screen. The grid display is platform agnostic so it is only the button and text placement that we need to worry about. In the function drawCellsLeftDisplay() we do some more conditional placement.
local iPhoneHeightAdjustment = 0
if CurrentOrientation == LANDSCAPE_LEFT
or CurrentOrientation == LANDSCAPE_RIGHT then
iPhoneHeightAdjustment = 50
end
if deviceIsIpad() then
text(cellsLeft, w + 50, HEIGHT - 100)
text(math.round(gameTime), WIDTH - 150, HEIGHT - 100)
else
text(cellsLeft, w - 30, HEIGHT - 100 + iPhoneHeightAdjustment)
text(math.round(gameTime), WIDTH - 70, HEIGHT - 100 + iPhoneHeightAdjustment)
end
The buttons on the iPhone screen will need to be placed based on the current orientation due to the space constraints. We do this in the drawGameButtons() function.
function drawGameButtons()
-- These are the buttons visible on the game run screen
-- Adjust the button position based on the device and orientation.
-- In particular, we need to move the button location
-- if the current device is an iPhone/iPod touch.
local mButtonSize = vec2(100, 50)
local mLocX, mLocY = 0, 0
if CurrentOrientation == LANDSCAPE_LEFT
or CurrentOrientation == LANDSCAPE_RIGHT then
if deviceIsIpad() then
mLocX = WIDTH - 200
mLocY = HEIGHT - 195
else
mLocX = WIDTH - 200 + 90
mLocY = HEIGHT - 195 - 100
end
else
if deviceIsIpad() then
mLocX = WIDTH - 125
mLocY = HEIGHT - 320
else
mLocX = WIDTH - 120
mLocY = HEIGHT - 320 - 140
end
end
flagButton.x, flagButton.y = mLocX, mLocY
if CurrentOrientation == LANDSCAPE_LEFT
or CurrentOrientation == LANDSCAPE_RIGHT then
if deviceIsIpad() then
mLocY = 110 + mButtonSize.y*2
else
mLocY = 110 + mButtonSize.y
end
else
if deviceIsIpad() then
mLocY = 240 + mButtonSize.y*2
else
mLocX = 20
end
end
newGameButton.x, newGameButton.y = mLocX, mLocY
if CurrentOrientation == LANDSCAPE_LEFT
or CurrentOrientation == LANDSCAPE_RIGHT then
if deviceIsIpad() then
mLocY = 110
else
mLocY = 90
end
else
if deviceIsIpad() then
mLocY = 240
else
mLocX = WIDTH / 2 - mButtonSize.x/2
mLocY = HEIGHT - mButtonSize.y - 10
end
end
menuButton.x, menuButton.y = mLocX, mLocY
flagButton:draw()
newGameButton:draw()
menuButton:draw()
end
22.5 Lessons Learned
Converting to a universal App was easier than we were expecting. The Codea runtime works out of the box without modification so it is only your Lua code and graphical assets which need to be modified.
For us the most important lesson is that if you want you App to eventually be universal then design it to work on an iPhone/iPod sized screen (320 x 480 pixels) first. It is much easier to convert this to work on a larger screen than vice versa.
The other lesson is to avoid using hard coded co-ordinates if possible and use positions relative to the WIDTH and HEIGHT constants. This will make conversion between different screen sizes much easier and in many cases wont require any changes to your code at all.
Finally if your app includes Game Center functionality then you need to modify the methods which dismiss the Game Center view controllers to restart the Lua draw() loop.
22.6 Download the Code
You can download the updated code below. For simplicity we have provided all the classes even if they haven't changed from a previous version. You should be able to tell from the version number whether you need to download the code or can use a previous version (v1.6 indicates code that was modified for this tutorial).
Lua Code:
- Main v1.6
- Cell v1.5
- Colors v1.1
- Physics v1.0
- PhysicsDebugDraw v1.0
- RoundBorder v1.2
- ScoreScreen v1.0
- SplashScreen v1.6
- Button v1.3
- Fader v1.1
- IconImages v1.1
- TextBox v1.0
- Twinkle v1.1
Runtime (Objective C) Code: