Last week, it was all about Android. This week, I’ve started taking the plunge into the world of iOS. I’ve been using a Mac for some time – mainly to wean myself away from being so Windows-centric, but also with a view to working more with AutoCAD for Mac from a development perspective – but this was the first time I’d actually forced myself to write anything for either OS X or iOS.
It all came as a bit of a shock, initially, even though I was generally aware of the strangeness of Objective-C with respect to its message-passing syntax. So while I enjoy learning different programming languages, I found I really struggled with Objective-C. But anyway – obviously lots of people have managed to get their heads around it (and many of this blog’s readers will have done so, I’m sure), so at least there is a fair amount of help available out there on the web.
Someone has already commented on the fact that you can use C# to build apps for iOS and Android directly – such as with a toolkit like Xamarin or an engine like Unity3D – but the point of this series of posts is as much about driving my own learning as it is about presenting my readers with easy options (sorry for being selfish, but that’s just how it is). And I think there’s value in seeing the “native” approach across a variety of platforms – while knowing that options exist allowing you to maintain a largely platform-independent codebase to target them.
And so on to my deep-ish dive into iOS…
My first challenge was identifying a decent (and free) 3D engine for our viewer – all of which seem to be based on OpenGL ES, much the same as for Android. I started by looking at Cocos3D (which is based on the apparently very popular Cocos2D), but ended up discarding it as it didn’t appear to have a sphere primitive available (which was a bit of a deal-breaker for me, given the problem space ;-).
I moved on to look at iSGL3D, which certainly appeared to provide what I was looking for from a 3D engine. I spent some time looking at its online tutorials, which were reasonably comprehensive, before trying the “tests” provided with the framework and building my first basic app with the Xcode 4 template. I went through a little unnecessary thrashing, as I pulled down the latest file versions directly from GitHub before realising I really needed to install the latest stable build (version 1.2.3 at the time of writing).
But, that aside, the process was reasonably straightforward. I modified the contents of the “Hello World” files created by the Xcode template (renaming them, too, of course), to be as follows…
It’s worth noting that – in an effort to make the code a more familiar and consistent with the other code I post here – I’ve thrown away the book on Objective-C coding conventions (much as I did for Java, last week). That’s partly for the benefit of this blog’s readers, but also for my own sanity. ;-)
Firstly, the ApollonianViewer.h header file:
#import "isgl3d.h"
@interface ApollonianViewer : Isgl3dBasic3DView
{
@private
NSMutableArray * _materials;
Isgl3dNode * _container;
Isgl3dSphere * _sphereMesh;
}
-(void)createSphere
:(double)radius
x:(double)x y:(double)y z:(double)z
level:(int)level;
@end
And now the main ApollonianViewer.m implementation file:
#import "ApollonianViewer.h"
@implementation ApollonianViewer
// Our data member for the received data
NSMutableData * _receivedData = NULL;
// A response has been received from our web-service call
- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response
{
// Initialise our member variable receiving data
if (_receivedData == NULL)
_receivedData = [[NSMutableData alloc] init];
else
[_receivedData setLength:0];
}
// Data has been received from our web-service call
- (void)connection:(NSURLConnection *)connection
didReceiveData:(NSData *)data
{
// Append the received data to our member
[_receivedData appendData:data];
}
// The web-service connection failed
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
// Report an error in the log
NSLog(@"Connection failed: %@", [error description]);
}
// The call to our web-service has completed
- (void)connectionDidFinishLoading
:(NSURLConnection *)connection
{
// Release the connection
[connection release];
// Get the response string from our data member then
// release it
NSString *responseString =
[[NSString alloc]
initWithData:_receivedData
encoding:NSUTF8StringEncoding
];
[_receivedData release];
// Extract JSON data from our response string
NSData *jsonData =
[responseString
dataUsingEncoding:NSUTF8StringEncoding];
// Extract an array from our JSON data
NSError *e = nil;
NSArray *jsonArray =
[NSJSONSerialization
JSONObjectWithData: jsonData
options: NSJSONReadingMutableContainers
error: &e
];
if (!jsonArray)
{
NSLog(@"Error parsing JSON: %@", e);
}
else
{
// Loop through our JSON array, extracting spheres
for (NSDictionary *item in jsonArray)
{
// We'll need this data for each sphere
double x, y, z, radius;
int level;
// We use a single NSNumber to extract the data
NSNumber *num;
num = [item objectForKey:@"X"];
x = [num doubleValue];
num = [item objectForKey:@"Y"];
y = [num doubleValue];
num = [item objectForKey:@"Z"];
z = [num doubleValue];
num = [item objectForKey:@"R"];
radius = [num doubleValue];
num = [item objectForKey:@"L"];
level = [num intValue];
// Only create spheres for those at the edge of the
// outer sphere
double length = sqrt(x*x + y*y + z*z);
if (length + radius > 0.99f)
[self createSphere:radius x:x y:y z:z level:level];
}
// Trigger the rotation updates
[self schedule:@selector(tick:)];
}
}
- (id) init
{
if ((self = [super init]))
{
// Set up our web-service call
NSURL *url =
[NSURL
URLWithString:
@"http://apollonian.cloudapp.net/api/spheres/1/7"
];
NSMutableURLRequest *request =
[NSMutableURLRequest
requestWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0
];
[request setHTTPMethod:@"GET"];
NSURLConnection *connection =
[[NSURLConnection alloc]initWithRequest:request delegate:self];
if (connection)
{
_receivedData = [[NSMutableData data] retain];
}
// Move the default camera to the desired position
[self.camera setPosition:iv3(0, 0, -5)];
// Create a container for our spheres
_container = [self.scene createNode];
// We'll maintain an array of materials for our
// levels. Define the colors for those levels
NSArray * colors =
[NSArray arrayWithObjects:
/* white */ @"FFFFFF",
/* red */ @"FF0000",
/* yellow */ @"FFFF00",
/* green */ @"00FF00",
/* cyan */ @"00FFFF",
/* blue */ @"0000FF",
/* magenta */ @"FF00FF",
/* dark gray */ @"A9A9A9",
/* gray */ @"808080",
/* light gray */ @"D3D3D3",
/* white */ @"FFFFFF",
nil
];
// Create and populate the array of materials
_materials = [[NSMutableArray alloc] init];
for (int i=0; i < 12; i++)
{
// Anything we don't have a color for will be white
NSString *col =
(i <= 10) ? [colors objectAtIndex:i] : @"FFFFFF";
// For simplicity, make the colors the same for
// ambient, diffuse and specular lighting
Isgl3dColorMaterial * mat =
[[Isgl3dColorMaterial alloc]
initWithHexColors:col
diffuse:col
specular:col
shininess:0.7
];
[_materials addObject:mat];
}
// Create a single sphere mesh
_sphereMesh =
[[Isgl3dSphere alloc] initWithGeometry:1 longs:9 lats:9];
// Create a directional white light and add it to the scene
Isgl3dLight * light =
[Isgl3dLight
lightWithHexColor:@"A0A0A0"
diffuseColor:@"E9E9E9"
specularColor:@"C0C0C0"
attenuation:0
];
light.lightType = DirectionalLight;
light.position = iv3(4, 0, 8);
[light setDirection:1 y:2 z:-5];
[self.scene addChild:light];
// Set the scene ambient color
[self setSceneAmbient:@"000000"];
}
return self;
}
// Create a single sphere at the desired position with
// the desired radius and level
- (void)createSphere
:(double)radius
x:(double)x y:(double)y z:(double)z
level:(int)level
{
// Create the sphere based on our single mesh
Isgl3dMeshNode * sphere =
[_container
createNodeWithMesh:_sphereMesh
andMaterial:[_materials objectAtIndex:level]
];
// Position and scale it
sphere.position = iv3(x, y, z);
[sphere setScale:radius];
}
- (void) dealloc
{
// Make sure we release our materials and sphere mesh
[_materials release];
[_sphereMesh release];
[super dealloc];
}
- (void) tick:(float)dt
{
// Rotate around the y axis
_container.rotationY += 2;
}
@end
The app currently does a fair amount less that its Android counterpart – I haven’t implemented any kind of UI, including progress bars, touch gestures, etc. – but there were actually some things that just worked more smoothly: rather than worrying about threading issues, the call to the web-service seemed to execute asynchronously by default, and the code adding spheres worked well, once I’d determined I needed to control the lifetime of my supporting objects rather than allowing them to be garbage-collected at the whim of the iOS runtime. It’s not clear to me how much of this is down to the iSGL3D runtime vs. Objective-C/iOS, but I was pleasantly surprised, either way.
I expect I’ll hit more significant challenges, further down the line, but my initial impression is that the above code is actually impressively functional for the amount there is: it was pretty simple to access a REST web-service and decode the JSON results, for instance, and the iSGL3D coding was also relatively straightforward.
And while looking at the syntax still gives me a headache, at least my nose has stopped bleeding. ;-)
Here’s a screenshot of the app working on the iPad 5.1 Simulator:
I’d really like to see this working on the iPad itself, as before I start tweaking the lighting, etc. to get better results, I’d like to see what, if anything, is due to the lack of GPU-accelerated graphics in the simulator (assuming that’s the case). It seems that to do so I’ll need to sign up for the iOS Developer Progrram at $99 per year, which I find a little annoying but to some degree understandable.
In fairness, the Android simulator can’t even run OpenGL ES 2.0 code, at the time of writing, so being forced to pay to test on a physical device would probably have raised the barrier of entry high enough to put me off working with Android completely. At least there is some option for getting started for free on iOS.
Aside from testing on a physical device, I also want to implement some kind of rudimentary UI – much as I did for Android – so I’ll be working on that before I end up posting the full project.