Gamut: Explore Colors in iOS Simulator
Posted by Jeffrey
Updated: June 13, 2011
I’ve put together a simple OCaml app that you can run on the iOS Simulator. It displays an animation that changes colors as you touch different locations on the screen. Each screen location is associated with a certain color, and (if you’re patient) you can touch every spot on the screen to try out all the possible colors of the display. Hence the app is called Gamut.
If you don’t want to build it yourself, a prebuilt binary for Gamut is available from Psellos. See below for how to extract and try it out in the iOS Simulator (it’s extremely easy). Otherwise you can get the sources from Psellos, compile using Xcode tools, and run the app in the iOS Simulator. This is also pretty easy. All you need for either approach is an installation of Xcode with the iOS SDK; you don’t need an iOS device or have to be a registered Apple developer.
Here are some hints for trying out the app:
Touching outside the circle changes the display to a different color. Touches near the top give red, then successively lower locations give orange, yellow, green, blue, and purple. At the bottom it wraps back around to red. This is the hue component of the color.
Touches toward the left give less saturated colors (more grayish and world-weary looking), touches toward the right give more saturated ones (bright and cartoonish). This is the saturation component of the color.
The circle in the middle animates the different possible light and dark variants of the basic color. Touching inside the circle sets the overall color to the current level of lightness. This is the lightness component of the color.
You can touch outside the circle and then move your finger around to go through a series of colors.
You can touch and hold inside the circle to go through a series of lightnesses with time.
The app has been built and tested with both Xcode 3.2.5 and Xcode 4.0.2 (the latest as of this writing).
Please note that a few of the command lines of the following discussion are too long to fit on a single line of a typical browser window. In a lot of cases there is no good place to split them into smaller lines, usually because of a long filename or URL. Take care that you enter them as a single line if you’re typing them in by hand.
Overview
The main problems of running OCaml in the iOS Simulator are:
Compiling OCaml with iOS Simulator ABI
Linking to iOS Simulator libraries
Using Interface Builder
For the first problem, you can use the OCamlXSim compiler that I put together, described here. The Gamut example shows how to solve the other two problems.
One problem that you don’t have with the iOS Simulator is packaging and code signing, as the simulator doesn’t require apps to be signed. Because of this, it’s not necessary to run the Xcode app itself (the IDE) at all to build and run Gamut. All of the needed work can be done by the separate tools that come with Xcode. This includes starting the app in the Simulator, which can be done directly from the command line (through an unsupported interface).
The other two problems are solved exactly as for code that runs on an iOS device. This is what you should expect, otherwise it wouldn’t be much of an iOS Simulator. Linking to ObjC functions and classes is handled by having wrappers for them in OCaml. That is, you have small OCaml classes whose only purpose is to wrap up ObjC classes for use from OCaml. Similarly, you have inverse wrappers written in ObjC that allow access to OCaml classes. These inverse wrappers also provide Interface Builder with the information it needs to lay out the GUI and create an initial application state.
Keep in mind that the Gamut app doesn’t really do all that much, so the amount of OCaml code is quite a bit less than the amount of Objective C wrapper code. In a more realistic application (like our Cassino app), almost all the code is in OCaml. Our library of Objective C wrapper code is relatively small and fixed. (In fact, we use the dynamic method invocation facility of ObjC to allow us to write almost all our wrappers in OCaml.)
Preliminaries
Before starting, make sure you have installed Apple’s Xcode and iOS SDK. I still use Xcode 3.2.5, due to small bugs in Xcode 4.0.2 that prevent it from building the 32-bit OCaml compiler itself. However, I have also built and tested Gamut with Xcode 4.0.2 and it works. If you’re going to build Gamut from sources, you also need an OCaml compiler that conforms to the iOS Simulator ABI. You can use the one I put together—binaries and instructions for building from source are given here.
Running Prebuilt Gamut Binary
If you want to run the prebuilt Gamut binary (to prove to yourself that it works), just download the binary for Gamut 1.0.9 from Psellos:
$ curl -O -s http://psellos.com/pub/gamut/gamut-simapp-1.0.9.tgz
$ ls -l gamut-simapp-1.0.9.tgz
-rw-r--r-- 1 psellos staff 116111 Jun 11 13:45 gamut-simapp-1.0.9.tgz
To save typing, you can download it directly from this link:
Now untar.
$ tar -xzf gamut-simapp-1.0.9.tgz
This creates a directory named gamut-simapp-1.0.9
in which you should
see the binary (Gamut
) and a script named runsim
that runs the app
in the iOS Simulator. To run Gamut, just cd
and run the script:
$ cd gamut-simapp-1.0.9
$ runsim
If you don’t have Xcode installed in the usual place, you should edit the script appropriately before running it.
Note: for simplicity, this script uses an unsupported interface of the iOS Simulator. Since it’s unsupported, the interface could disappear in a future release of Xcode. There will always be a way to run the app through the Xcode IDE, of course.
Building Gamut from Source
Download the sources for Gamut 1.0.9 from Psellos:
$ curl -O -s http://psellos.com/pub/gamut/gamut-1.0.9.tgz
$ ls -l gamut-1.0.9.tgz
-rw-r--r-- 1 psellos staff 12944 Jun 11 13:45 gamut-1.0.9.tgz
To save typing, you can download it directly from this link:
Now, untar and cd
into the Gamut directory.
$ tar -xzf gamut-1.0.9.tgz
$ cd gamut-1.0.9
Building and running Gamut is done by a Makefile
. You may need to
change some of the specific settings in this file:
PLAT = /Developer/Platforms/iPhoneSimulator.platform
SDK = /Developer/SDKs/iPhoneSimulator4.2.sdk
OCAMLDIR = /usr/local/ocamlxsim
Change PLAT
to the location of your iPhoneSimulator platform
directory, part of the Xcode iOS SDK. Change SDK
to the iOS SDK
you wish to use. Probably you want to set it to the most recent SDK
that you have installed. Change OCAMLDIR
to the location of your
OCaml compiler for the iOS Simulator.
Now you can build and run the app:
$ make
...
ibtool --compile Gamut.nib Gamut.xib
echo -n 'APPL????' > PkgInfo
$ make execute
You should see the Gamut app running in the iOS Simulator.
Note: for simplicity,
make execute
uses an unsupported interface of the iOS Simulator.
Since it’s unsupported, the interface could disappear in a future
release of Xcode. There will always be a way to run the app through the
Xcode IDE, of course.
Theory of Operation
Gamut is a very simple iOS app that has just one class that really
does anything, named Gamutctlr
. There is a singleton instance of this
class that does all the work. Let’s call this instance
Gcon for short.
The Gamutctlr
class participates in the UIApplicationDelegate
protocol, and thus Gcon
receives notifications when the app starts up and shuts down.
Gamut also defines an Objective C class named ViewDelegator
. It’s a
subclass of the Cocoa Touch UIView
class, and its instances delegate
their GUI methods to a specified delegate object. In particular,
instances delegate touch events and the drawing method drawRect:
. The
Gamutctlr
class also participates in the ViewDelegate
protocol, and
thus Gcon also receives
notifications of touch and drawing events.
The startup of an iOS app is controlled by a nib file, generated by
Interface Builder. For Gamut, the file is Gamut.nib
. This file says
to create Gcon, and to
make it the delegate for the containing Gamut app and the single,
screen-sized ViewDelegator
view.
So then Gcon works by responding to outside events of four kinds:
Application status At startup, Gcon establishes a timer that fires periodically and calls its own
timerTick'
method.Touches As Gcon receives notifications of user touches, it tracks whether the touch began inside or outside the circle. If outside the circle, it tracks the most recently touched point; this is used to determine the hue and saturation values when drawing content. If inside the circle, it tracks the finishing time of the touch. This is used to determine the lightness values.
Timer ticks When Gcon receives a timer tick, it updates its record of the current time, and tells the containing view that it’s time to redraw the content.
Redraw When graphics contents need to be redrawn, Gcon uses Cocoa Touch methods to draw the content. The hue, saturation, and lightness values are determined based on the current time and on recent touches, as described above.
Interface Builder
If you open Gamut.xib
with Interface Builder, you’ll see that it’s
pretty simple. There are only three interesting objects: the main
window, a ViewDelegator
instance that occupies the whole window, and
Gcon.
There is a connection to
Gcon from the delegate
outlet of the application (represented by File’s Owner). This supports
the application startup notification.
There is a connection from the delegate
outlet of the ViewDelegator
to Gcon, and a connection
from the delegator
outlet of
Gcon back to the
ViewDelegator
. These support the notifications for touches and
drawing.
Since Interface Builder (IB) doesn’t know anything about OCaml (at least not yet), it gets its information from the header files for your wrapper classes. In Xcode 3.2.5 you can run Interface Builder on its own, but you need to manually tell IB to load the wrapper header files. In the downloaded nib files, this has already been done for you. If you want to create your own project from scratch, you may need to do this yourself.
To load header files into IB, click on the File -> Read Class Files menu item, and navigate to your ObjC header files, i.e., to the wrapper files for your OCaml classes. Note that you need wrappers only for OCaml classes that are accessed from ObjC, which should be just a few of them.
Discussion
Although I’ve packaged up Gamut to run in the iOS Simulator, it will naturally also run directly on an iOS device (iPhone, iPod Touch, iPad) with no changes to the code. The only changes required for iPhone and iPod Touch are to the building environment. For iPad, the nib file will also need to change. The code is written to be independent of the screen resolution, so it ought to run nicely as a native iPad app, in fact.
Compiling OCaml to run directly on iOS devices is described in an accompanying note here. I’ve also put together two apps that run on iOS devices. There is a simple app named Portland that tracks the devices’s orientation (portrait or landscape), and a slightly more complicated app named Slide24 that plays (and solves) the classic 5x5 sliding tile puzzle.
I’d be very happy to explain any part of Gamut in more detail. The only thing stopping me is my natural reticence. If you have questions, comments, or corrections please email me at jeffsco@psellos.com.