In the last post, we saw some code to implement a simple 3D viewer of data coming from our Apollonian web-service on iOS. In this post, we’ll add support for touch gestures, as well as a simple message box announcing when the web-service is unavailable.
When compared with Android, iOS provides much higher-level gesture information via its UIKit framework: you basically get callbacks to indicate when the screen has been tapped or swiped, or when pinch or rotate gestures have been performed. This is all very helpful, in the sense that you don’t have to do so much low-level running around in your code – and risk that different apps respond to touch in a different way – but it does come at the expense of flexibility. For instance, if you want to support swipe gestures, you can really only do so in the four main directions (up, down, left, right) and you need a separate recognizer object for at least vertical and horizontal directions, and quite possibly all four.
In the Android version of this app, we had to decide how swipe gestures would be implemented at a fairly low level, but the upside was that we could calculate a more precise direction and use that to adjust the spin of our model. Now I’m sure that it’s possible with iOS to hook into events at a lower level, to essentially do the same as with Android, but there are a couple of reasons I’ve decided not to. The first is that I don’t really need feature parity between the two viewers – as this is mostly research-related – but more importantly I have doubts that I’ll have the same ability to rotate our model in an arbitrary direction, as Dennis Ippel kindly enabled for us in his Rajawali framework (I haven’t seen the same capabilities, necessarily, in iSGL3D, and have less hope of getting them implemented). I’d ideally like to specify a rotation around an axis that’s perpendicular to the swipe direction – perhaps using a quaternion – and then apply that additively to the model.
Again, it’s probably something that I could implement myself, given enough time and effort, but I’ve decided to keep things simple. The addition of the rotate gesture – even if it gets reset when the model starts to spin – seems a reasonable complement to the existing behaviour of spinning in one of four directions, at least.
Before we look at the updated code, here’s a quick video of the application in action on the iOS Simulator:
And with that, on to the code. Here’s the updated Objective-C implementation of our Apollonian Viewer. As you’ll have seen in the video, I’ve gone ahead and added a splash-screen and icons to the project, even if we don’t have much else by way of a UI, still.
Here’s the updated ApollonianViewer.h file:
#import "isgl3d.h"
@interface ApollonianViewer : Isgl3dBasic3DView
{
@private
NSMutableArray * _materials;
Isgl3dNode * _container;
Isgl3dSphere * _sphereMesh;
bool _preSpin;
bool _paused;
float _rotation;
bool _spinAroundY;
int _spinIncrement;
int _initialSpinAmount;
UITapGestureRecognizer * _tapGestureRecognizer;
UISwipeGestureRecognizer * _swipeLeftGestureRecognizer;
UISwipeGestureRecognizer * _swipeRightGestureRecognizer;
UISwipeGestureRecognizer * _swipeUpGestureRecognizer;
UISwipeGestureRecognizer * _swipeDownGestureRecognizer;
UIPinchGestureRecognizer *_pinchGestureRecognizer;
UIRotationGestureRecognizer *_rotationGestureRecognizer;
}
-(void)createSphere
:(double)radius
x:(double)x y:(double)y z:(double)z
level:(int)level;
@end
@interface ApollonianViewer () <UIGestureRecognizerDelegate>
- (void)tapGesture:(UITapGestureRecognizer *)gestureRecognizer;
- (void)swipeGesture:(UISwipeGestureRecognizer *)gestureRecognizer;
- (void)pinchGesture:(UIPinchGestureRecognizer *)gestureRecognizer;
- (void)rotationGesture:(UIRotationGestureRecognizer *)gestureRecognizer;
@end
And here’s the updated ApollonianViewer.m 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]);
UIAlertView* alert =
[[[UIAlertView alloc]
initWithTitle:@"Apollonian Viewer"
message:
@"Unable to access the web-service. "
"Please check you have internet connectivity."
delegate:self
cancelButtonTitle:@"Close"
otherButtonTitles:nil
] autorelease];
[alert show];
}
- (void)alertView
:(UIAlertView *)alertView
clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 0) exit (0);
}
// 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];
}
// Remove our progress meter
UIActivityIndicatorView * vw =
(UIActivityIndicatorView *)
[[Isgl3dDirector sharedInstance].openGLView viewWithTag:1];
[vw removeFromSuperview];
// Trigger the rotation updates
[self schedule:@selector(tick:)];
}
}
// Our main scene initialization method
- (id) init
{
if ((self = [super init]))
{
_preSpin = true;
_paused = false;
_rotation = 0.0;
_initialSpinAmount = 2;
// Create recognizers handling scene-level gestures
// Tap
_tapGestureRecognizer =
[[UITapGestureRecognizer alloc]
initWithTarget:self
action:@selector(tapGesture:)
];
_tapGestureRecognizer.delegate = self;
[[Isgl3dDirector sharedInstance]
addGestureRecognizer:_tapGestureRecognizer
forNode:nil
];
// Swipe
// (Add a recognizer for each of 4 directions)
_swipeLeftGestureRecognizer =
[[UISwipeGestureRecognizer alloc]
initWithTarget:self
action:@selector(swipeGesture:)
];
_swipeLeftGestureRecognizer.delegate = self;
[_swipeLeftGestureRecognizer
setDirection:UISwipeGestureRecognizerDirectionLeft
];
[[Isgl3dDirector sharedInstance]
addGestureRecognizer:_swipeLeftGestureRecognizer
forNode:nil
];
_swipeRightGestureRecognizer =
[[UISwipeGestureRecognizer alloc]
initWithTarget:self
action:@selector(swipeGesture:)
];
_swipeRightGestureRecognizer.delegate = self;
[_swipeRightGestureRecognizer
setDirection:UISwipeGestureRecognizerDirectionRight
];
[[Isgl3dDirector sharedInstance]
addGestureRecognizer:_swipeRightGestureRecognizer
forNode:nil
];
_swipeUpGestureRecognizer =
[[UISwipeGestureRecognizer alloc]
initWithTarget:self
action:@selector(swipeGesture:)
];
_swipeUpGestureRecognizer.delegate = self;
[_swipeUpGestureRecognizer
setDirection:UISwipeGestureRecognizerDirectionUp
];
[[Isgl3dDirector sharedInstance]
addGestureRecognizer:_swipeUpGestureRecognizer
forNode:nil
];
_swipeDownGestureRecognizer =
[[UISwipeGestureRecognizer alloc]
initWithTarget:self
action:@selector(swipeGesture:)
];
_swipeDownGestureRecognizer.delegate = self;
[_swipeDownGestureRecognizer
setDirection:UISwipeGestureRecognizerDirectionDown
];
[[Isgl3dDirector sharedInstance]
addGestureRecognizer:_swipeDownGestureRecognizer
forNode:nil
];
// Pinch
_pinchGestureRecognizer =
[[UIPinchGestureRecognizer alloc]
initWithTarget:self
action:@selector(pinchGesture:)
];
_pinchGestureRecognizer.delegate = self;
[[Isgl3dDirector sharedInstance]
addGestureRecognizer:_pinchGestureRecognizer
forNode:nil
];
// Rotate
_rotationGestureRecognizer =
[[UIRotationGestureRecognizer alloc]
initWithTarget:self
action:@selector(rotationGesture:)
];
_rotationGestureRecognizer.delegate = self;
[[Isgl3dDirector sharedInstance]
addGestureRecognizer:_rotationGestureRecognizer
forNode:nil
];
// 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 initial 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];
}
// Respond to our timer tick by rotating the model
- (void) tick:(float)dt
{
// Rotate around the appropriate axis
if (!_paused && !_preSpin)
{
// Reset any rotation around Z, first
if (abs(_container.rotationZ) > 0)
{
_container.rotationZ = 0;
_rotation = 0;
}
if (_spinAroundY)
{
// If spinning around Y, reset any X rotation
if (abs(_container.rotationX) > 0)
_container.rotationX = 0;
_container.rotationY += _spinIncrement;
}
else
{
// If spinning around X, reset any Y rotation
if (abs(_container.rotationY) > 0)
_container.rotationY = 0;
_container.rotationX += _spinIncrement;
}
}
}
// Method to specify combination of gesture recognizers
- (BOOL)gestureRecognizer:
(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer *)otherGestureRecognizer
{
// If the gesture recognizers are on different views,
// don't allow simultaneous recognition
if (gestureRecognizer.view != otherGestureRecognizer.view)
return NO;
// Also stop combination of rotation with other gestures
if ((gestureRecognizer == _rotationGestureRecognizer) ||
(otherGestureRecognizer == _rotationGestureRecognizer))
return NO;
return YES;
}
// Action methods for our gestures
// Tap-pause/play
- (void)tapGesture
:(UITapGestureRecognizer *)gestureRecognizer
{
// Toggle pause (if we have already had at least one spin)
if (!_preSpin)
_paused = !_paused;
}
// Swipe-spin
- (void)swipeGesture
:(UISwipeGestureRecognizer *)gestureRecognizer
{
switch(gestureRecognizer.direction)
{
case UISwipeGestureRecognizerDirectionDown:
if (
_preSpin || _spinAroundY ||
(!_spinAroundY && _spinIncrement > 0)
)
{
// Reset the axis and spin amount
_spinAroundY = false;
_spinIncrement = -_initialSpinAmount;
}
else
{
// Speed up the rate of spin
if (abs(_spinIncrement * 2) < 10)
_spinIncrement *= 2;
}
_preSpin = false;
_paused = false;
break;
case UISwipeGestureRecognizerDirectionUp:
if (
_preSpin || _spinAroundY ||
(!_spinAroundY && _spinIncrement < 0)
)
{
// Reset the axis and spin amount
_spinAroundY = false;
_spinIncrement = _initialSpinAmount;
}
else
{
// Speed up the rate of spin
if (abs(_spinIncrement * 2) < 10)
_spinIncrement *= 2;
}
_preSpin = false;
_paused = false;
break;
case UISwipeGestureRecognizerDirectionLeft:
if (
_preSpin || !_spinAroundY ||
(_spinAroundY && _spinIncrement > 0)
)
{
// Reset the axis and spin amount
_spinAroundY = true;
_spinIncrement = -_initialSpinAmount;
}
else
{
// Speed up the rate of spin
if (abs(_spinIncrement * 2) < 10)
_spinIncrement *= 2;
}
_preSpin = false;
_paused = false;
break;
case UISwipeGestureRecognizerDirectionRight:
if (
_preSpin || !_spinAroundY ||
(_spinAroundY && _spinIncrement < 0)
)
{
// Reset the axis and spin amount
_spinAroundY = true;
_spinIncrement = _initialSpinAmount;
}
else
{
// Speed up the rate of spin
if (abs(_spinIncrement * 2) < 10)
_spinIncrement *= 2;
}
_preSpin = false;
_paused = false;
break;
default:
break;
}
}
// Pinch-zoom
- (void)pinchGesture
:(UIPinchGestureRecognizer *)gestureRecognizer
{
if (
[gestureRecognizer state] == UIGestureRecognizerStateBegan ||
[gestureRecognizer state] == UIGestureRecognizerStateChanged
)
{
// Adjust the camera position based on the zoom scale
[self.camera setZ:self.camera.z * (1/gestureRecognizer.scale)];
[gestureRecognizer setScale:1];
}
}
// Rotate-rotate :-)
- (void)rotationGesture
:(UIRotationGestureRecognizer *)gestureRecognizer
{
if (
[gestureRecognizer state] == UIGestureRecognizerStateBegan ||
[gestureRecognizer state] == UIGestureRecognizerStateChanged
)
{
// Adjust the rotation around Z based on the rotation amount
if (_paused || _preSpin)
{
_rotation += (gestureRecognizer.rotation * 180.0 / M_PI);
[_container setRotationZ:_rotation];
[gestureRecognizer setRotation:0];
}
}
}
@end
I’ve managed to get into the Apple Developer Program via an Autodesk subscription, but there are some delays with me being able to test the app directly on a physical device. I’m still waiting to do so before I determine what additional UI needs to be implemented (in terms of progress bars, particularly), although I could probably go ahead and add some level-selection capability, at least. I may just move on to other things and revisit this at some point in the future – we’ll see.