SwiftUI Parallax Motion Effect
UPDATE (05 JULY, 2020): Thank you to one of our readers for pointing out that we were originally instantiating a
CMMotionManager
instance inside each instantiated modifier. This goes against Apple's documentation on when to createCMMotionManager
instance. Multiple instances of this class can affect the rate at which data is received from the accelerometer and gyroscope. The tutorial below has been modified to correct this issue. Thank you Dennis!
In this tutorial we're going to take a look at an interesting SwiftUI Motion effect, and discuss how we can easily apply it to our views by creating our own custom ViewModifier
struct.
This effect responds to the device attitude (roll, effect, and yaw) by adjusting the offset of the view it's applied to. It creates this crazy parallax effect when the user moves their device. Checkout the video below for an example.
Before getting started, please consider subscribing using this link, and if you aren't reading this on TrailingClosure.com, please come check us out sometime!
Building The ParallaxMotionModifier
Struct
The ParallaxMotionModifier
struct is going to hold our MotionManager
class. The MotionManager
class is going to be an ObservableObject
which publishes updates on the device attitude (pitch, roll, yaw).
Here is the boilerplate code for the ParallaxMotionModifier
struct and MotionManager
ObservableObject class.
import SwiftUI
import CoreMotion
struct ParallaxMotionModifier: ViewModifier {
func body(content: Content) -> some View {
content
}
}
class MotionManager: ObservableObject {
init() {
}
}
Setup and Receive Motion Updates
1. First we need to add @Published
variables to the MotionManager
class for the pitch and roll. We won't be needing the yaw of the device for this effect.
class MotionManager: ObservableObject {
@Published var pitch: Double = 0.0
@Published var roll: Double = 0.0
init() {
}
}
2. Next we create an instance of the CMMotionManager
class to receive motion updates from the device within our class. Additionally we set the update interval to 60
times per second.
class MotionManager: ObservableObject {
@Published var pitch: Double = 0.0
@Published var roll: Double = 0.0
private var manager: CMMotionManager
init() {
self.manager = CMMotionManager()
self.manager.deviceMotionUpdateInterval = 1/60
}
}
3. Finally we need to subscribe to motion updates and then set the new motion values to our pitch
and roll
variables. That's it for the MotionManager
class.
class MotionManager: ObservableObject {
@Published var pitch: Double = 0.0
@Published var roll: Double = 0.0
private var motionManager: CMMotionManager
init() {
self.manager = CMMotionManager()
self.manager.deviceMotionUpdateInterval = 1/60
self.manager.startDeviceMotionUpdates(to: .main) { (motionData, error) in
guard error == nil else {
print(error!)
return
}
if let motionData = motionData {
self.pitch = motionData.attitude.pitch
self.roll = motionData.attitude.roll
}
}
}
}
Apply Motion Updates to View Content
Let's move back to the ParallaxMotionModifier
struct.
1. First at the top of ParallaxMotionModifier
create an instance of the MotionManager
class we just finished building. This will be an @ObservedObject
so that the ParallaxMotionModifier
struct automatically applies the device's motion updates to our content.
struct ParallaxMotionModifier: ViewModifier {
@ObservedObject var manager: MotionManager = MotionManager()
func body(content: Content) -> some View {
content
}
}
2. Inside the body(content:)
function apply an offset change to the content using the data from the MotionManager
instance.
struct ParallaxMotionModifier: ViewModifier {
@ObservedObject var manager: MotionManager = MotionManager()
func body(content: Content) -> some View {
content
.offset(x: CGFloat(manager.roll), y: CGFloat(manager.pitch))
}
}
3. If you tried using this modifier now, you wouldn't see much. The view you apply it to would only move a minuscule amount. This is why we need to create a new variable called magnitude
. We're going to use this when applying our offset to change how much the view moves relative to device motion.
var magnitude: Double
Then apply it inside our offset calculations:
func body(content: Content) -> some View {
content
.offset(x: CGFloat(manager.roll * magnitude), y: CGFloat(manager.pitch * magnitude))
}
Full Code for ParallaxMotionModifier
and MotionManager
import SwiftUI
import CoreMotion
struct ParallaxMotionModifier: ViewModifier {
@ObservedObject var manager: MotionManager
var magnitude: Double
func body(content: Content) -> some View {
content
.offset(x: CGFloat(manager.roll * magnitude), y: CGFloat(manager.pitch * magnitude))
}
}
class MotionManager: ObservableObject {
@Published var pitch: Double = 0.0
@Published var roll: Double = 0.0
private var manager: CMMotionManager
init() {
self.manager = CMMotionManager()
self.manager.deviceMotionUpdateInterval = 1/60
self.manager.startDeviceMotionUpdates(to: .main) { (motionData, error) in
guard error == nil else {
print(error!)
return
}
if let motionData = motionData {
self.pitch = motionData.attitude.pitch
self.roll = motionData.attitude.roll
}
}
}
}
Show us what you made!
We want to see what you've made using this tutorial! Send us pics! Find us on Twitter @TrailingClosure, on Instagram, or email us at howdy@TrailingClosure.com
Examples
import SwiftUI
struct ParallaxMotionTestView: View {
@ObservedObject var manager = MotionManager()
var body: some View {
Color.red
.frame(width: 100, height: 100, alignment: .center)
.modifier(ParallaxMotionModifier(manager: manager, magnitude: 10))
}
}