Optimizing Device-Specific iOS Development

• Print
This chapter is from the book

Recipe: Accelerometer-Based Scroll View

Several readers asked me to include a tilt scroller recipe in this edition. A tilt scroller uses the device’s built-in accelerometer to control movement around a UIScrollView’s content. As the user adjusts the device, the material “falls down” accordingly. Instead of a view being positioned onscreen, the content view scrolls to a new offset.

The challenge in creating this interface lies in determining where the device should have its resting axis. Most people would initially suggest that the display should stabilize when lying on its back, with the Z-direction pointed straight up in the air. It turns out that’s actually a fairly bad design choice. To use that axis means the screen must actually tilt away from the viewer during navigation. With the device rotated away from view, the user cannot fully see what is happening onscreen, especially when using the device in a seated position and somewhat when looking at the device while standing overhead.

Instead, Recipe 1-5 assumes that the stable position is created by the Z-axis pointing at approximately 45 degrees, the natural position users holding an iPhone or iPad in their hands. This is halfway between a face-up and a face-forward position. The math in Recipe 1-5 is adjusted accordingly. Tilting back and forward from this slanting position leaves the screen with maximal visibility during adjustments.

The other change in this recipe, compared to Recipe 1-4, is the much lower acceleration constant. This enables onscreen movement to happen more slowly, letting users more easily slow down and resume navigation.

Recipe 1-5. Tilt Scroller

```- (void)accelerometer:(UIAccelerometer *)accelerometer
didAccelerate:(UIAcceleration *)acceleration
{
// extract the acceleration components
float xx = -acceleration.x;
float yy = (acceleration.z + 0.5f) * 2.0f; // between face-up and face-forward

// Has the direction changed?
float accelDirX = SIGN(xvelocity) * -1.0f;
float newDirX = SIGN(xx);
float accelDirY = SIGN(yvelocity) * -1.0f;
float newDirY = SIGN(yy);

// Accelerate. To increase viscosity lower the additive value
if (accelDirX == newDirX) xaccel = (abs(xaccel) + 0.005f) * SIGN(xaccel);
if (accelDirY == newDirY) yaccel = (abs(yaccel) + 0.005f) * SIGN(yaccel);

// Apply acceleration changes to the current velocity
xvelocity = -xaccel * xx;
yvelocity = -yaccel * yy;
}

- (void) tick
{
xoff += xvelocity;
xoff = MIN(xoff, 1.0f);
xoff = MAX(xoff, 0.0f);

yoff += yvelocity;
yoff = MIN(yoff, 1.0f);
yoff = MAX(yoff, 0.0f);

// update the content offset based on the current velocities
CGFloat xsize = sv.contentSize.width - sv.frame.size.width;
CGFloat ysize = sv.contentSize.height - sv.frame.size.height;
sv.contentOffset = CGPointMake(xoff * xsize, yoff * ysize);
}

- (void) viewDidAppear:(BOOL)animated
{
NSString *map = @"http://maps.weather.com/images/        maps/current/curwx_720x486.jpg";
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
^{
NSURL *weatherURL = [NSURL URLWithString:map];
NSData *imageData = [NSData dataWithContentsOfURL:weatherURL];

// Update the image on the main thread using the main queue
UIImage *weatherImage = [UIImage imageWithData:imageData];
UIImageView *imageView =
[[UIImageView alloc] initWithImage:weatherImage];
CGSize initSize = weatherImage.size;
CGSize destSize = weatherImage.size;

// Ensure that the content size is significantly bigger
// than the screen can show at once
while ((destSize.width < (self.view.frame.size.width * 4)) ||
(destSize.height < (self.view.frame.size.height * 4)))
{
destSize.width += initSize.width;
destSize.height += initSize.height;
}

imageView.userInteractionEnabled = NO;
imageView.frame = (CGRect){.size = destSize};
sv.contentSize = destSize;