The Motion Sickness Conundrum

When iOS 7 was released, Apple moved away from Skeuomorphism and towards minimalism. This was a welcome change at the time, which brought solutions to certain design crimes that prevailed and one very peculiar problem — Motion Sickness.
There were a lot of reports and criticism because of a feature called Parallax, which among other things was a cause of concern for Apple at the time. Of course, this was fixed in iOS 7.1 by a simple accessibility setting which could be turned off.
Parallax, when used right, could be key to offering a sense of depth while making sure the experience is unperturbed. Let’s check out how to craft delightful experiences using motion instead of headaches. The key is to keep is subtle.

Also, incase you just want to see code, there's an Xcode project at the end of the post, feel free to download and check it out.

The Basics

The Boss Class behind the parallax sensation is UIMotionEffect. This is an abstract class, which means it cannot be instantiated directly, which means you have to subclass it.

There’s a particular method keyPathsAndRelativeValues(forViewerOffset:) which needs to be implemented by all subclasses of UIMotionEffect in order for the system to be able to do what you want it to.

This method basically returns a dictionary of view properties/keypaths which are animatable and values by which the property will be modified based on a parameter called viewerOffset.

The parameter viewerOffset is of type UIOffset which is passed by the system to the method we have been talking about when there’s a subclass of UIMotionEffect present.

The viewerOffset property has two offsets based on the direction that the device is facing with respect to its viewer:

  • Horizontal Offset (which ranges from (-1, 0) to (1, 0))
  • Vertical Offset (which ranges from (0, -1) to (0, 1))

When the screen is directly pointed at the user, the viewerOffset value is at (0, 0). This property changes over the range based on how the user holds the device and it's tilt.

Here’s a scruffy looking illustration showing how the offset varies along the axes:

Once you comprehend this bit, it’s just a matter of applying this concept to your views using the API, which in my opinion, is quite straightforward.

Dealing with key paths and values for simple cases can be cumbersome. Looks like someone else also thought so at Apple because of which UIKit comes with a subclass of UIMotionEffect which already has this method implemented, which is UIInterpolatingMotionEffect.

The Recipe

Thanks to UIKit, we have access to UIInterpolatingMotionEffect which lets us create motion effects very easily. For instance, it’s quite easy to create parallax effects like this:

The process for implementing something like this would be:

  1. Create an instance of UIInterpolatingMotionEffect by passing a keypath you want to animate and the axis along which the motion needs to be tracked
  2. Set minimumRelativeValue and maximumRelativeValue properties on the instance
  3. Add it to your view using addMotionEffect(effect:) method

Let’s get into implementation details now.

  1. You would create an instance of UIInterpolatingMotionEffect this way:
let verticalInterpolatingEffect = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis)

The parameters that we pass to the class are a keyPath of the view property that will be modified based on the motion along the axis that we specify in the type property.

The type property takes one of two values:

  • tiltAlongVerticalAxis
  • tiltAlongHorizontalAxis
  1. Once we have instantiated an object, we need to set the following properties on it:
  • minimumRelativeValue
  • maximumRelativeValue

These properties are of type Any? if you have noticed. For our instance to be able to alter our view’s center, which we need to be able to induce parallax effect, we pass CGFloat values which denote the minimum/maximum values returned when the viewerOffset are at their extremes say (-1, 0), (0, 1) and so forth something like this:

verticalInterpolatingEffect.minimumRelativeValue = CGFloat(-100)
verticalInterpolatingEffect.maximumRelativeValue = CGFloat(100)

This denotes the extent to which modifications to a property can take place. The reason these properties are of type Any? because they can take values of CGFloat, or even a CATransform (by wrapping it in an NSValue object) which could give you more control over the kind of animation you could do based on the device's tilt.

  1. Finally, we add the object to our view using the addMotionEffect(effect:) method.

To do this, we could simply do:

view.addMotionEffect(verticalInterpolatingEffect)

and get done with it. But you would notice that we modify the center only based on the movement along the vertical axis. To make sure we do this along both axes, we would need to

  • Create a horizontalInterpolatingEffect in a similar fashion by passing tiltAlongHorizontalAxis to the class
  • Create a Motion Effect Group
  • Add both effects to the group
  • Add the group to the view using the same method

Creating a motion effect group and adding effects is as simple as:

let motionEffectsGroup = UIMotionEffectGroup()
motionEffectsGroup.motionEffects = [horizontalInterpolatingEffect, verticalInterpolatingEffect]

All that’s left to do to is adding the effects group to the view:

view.addMotionEffect(motionEffectsGroup)

BYOC: Bring Your Own Class

Now that we know how to create simple parallax effects, we’ll see how to subclass UIInterpolatingMotionEffect by implementing a detail which tries to avoid giving people motion sickness, which kind of was the point of the article.
The answer is subtlety.

  1. Subclass UIIntepolatingMotionEffect, say something like SubtleMotionEffect
  2. Override keyPathsAndRelativeValues(forViewerOffset:) method to divide the offset values by 5 points to make the movement more subtle
  3. Use objects of SubtleMotionEffect while creating motion effects and add it to the view using addMotionEffect(effect:)

Overridden Method:

override func keyPathsAndRelativeValues(forViewerOffset viewerOffset: UIOffset) -> [String : Any]? {

    // 1
    var motionAmplitude: CGFloat = 0.0
    
    // 2
    switch self.type {
    case .tiltAlongHorizontalAxis:
      motionAmplitude = viewerOffset.horizontal
    case .tiltAlongVerticalAxis:
      motionAmplitude = viewerOffset.vertical
    }
    
    //3
    if (motionAmplitude > 0) {
      guard let max = (maximumRelativeValue as? CGFloat) else { return nil }
      return [self.keyPath: max * motionAmplitude / 5.0]
    } else {
      guard let min = (minimumRelativeValue as? CGFloat) else { return nil }
      return [self.keyPath: -(min) * motionAmplitude / 5.0]
    }
  }

In this method, the following happens:

  1. We define an variable to hold our offset value
  2. Assign the offset to our variable based on the tilt-axis
  3. Return the keypath dividing the offset value by 5

This will make the effect more subtle:


This is a guide to help someone get started with UIMotionEffect covering just the basic cases of parallax motion, there’s a lot of potential to use this class in various ways to create interesting experiences and I encourage you to do it.

Feel free to leave comments, also if you want other cases of the class to be covered do let me know, if there’s enough interest I’ll consider writing another post going through this in detail.

Thanks to corsarus' article on the same subject which helped me understand how to override the dictionary returning method.

Resources: