- Accessing Basic Device Information
- Adding Device Capability Restrictions
- Recipe: Checking Device Proximity and Battery States
- Recipe: Recovering Additional Device Information
- Recipe: Using Acceleration to Locate "Up"
- Working with Basic Orientation
- Retrieving the Current Accelerometer Angle Synchronously
- Recipe: Using Acceleration to Move Onscreen Objects
- Recipe: Accelerometer-Based Scroll View
- Recipe: Core Motion Basics
- Recipe: Retrieving and Using Device Attitude
- Detecting Shakes Using Motion Events
- Recipe: Using External Screens
- Tracking Users
- One More Thing: Checking for Available Disk Space
- Summary
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]; [queue addOperationWithBlock: ^{ // Load the weather data NSURL *weatherURL = [NSURL URLWithString:map]; NSData *imageData = [NSData dataWithContentsOfURL:weatherURL]; // Update the image on the main thread using the main queue [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 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; [sv addSubview:imageView]; // Activate the accelerometer [[UIAccelerometer sharedAccelerometer] setDelegate:self]; // Start the physics timer [NSTimer scheduledTimerWithTimeInterval: 0.03f target: self selector: @selector(tick) userInfo: nil repeats: YES]; }]; }]; }