xyz
xyz

Reputation: 125

SwiftUI Parallax Motion Effect causes Lag when switching views or opening sheets

I found this code, and adapted it to my own app. It seems to work fine for my use. The issue I am experiencing and not sure how to resolve is that when I switch views or open a sheets over the PostView. It seems like the motion updates continue even when they're not needed, causing the app to lag and become very sluggish and choppy as it seems to render the effect nonstop.

Code for motion effect:

import SwiftUI
import CoreMotion

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/90
        self.manager.startDeviceMotionUpdates(to: .main) { [weak self] (motionData, error) in
            guard error == nil else {
                print(error!)
                return
            }
            if let motionData = motionData {
                DispatchQueue.main.async {
                    let newPitch = motionData.attitude.pitch
                    let newRoll = motionData.attitude.roll
                    // Use nil-coalescing operator to provide a default value (e.g., 0.0) if nil
                    if abs(newPitch - (self?.pitch ?? 0.0)) > 0.01 || abs(newRoll - (self?.roll ?? 0.0)) > 0.01 {
                        self?.pitch = newPitch
                        self?.roll = newRoll
                    }
                }
            }
        }
    }
}

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))
    }
}

Here is my PostView code snippet that renders random image dynamically inside a frame:

struct PostView: View {
    enum LayoutType {
        case singleImage, listView
    }

    var post: SomeTest
    var layoutType: LayoutType

    @StateObject private var motionManager = MotionManager()
    
    var body: some View {
        switch layoutType {
        case .singleImage:
            VStack {
                ZStack(alignment: .topLeading) {
                    // Apply ParallaxMotionModifier to displayImageView
                    displayImageView(for: post)
                        .modifier(ParallaxMotionModifier(manager: motionManager, magnitude: 15))
                        .drawingGroup()
                        .frame(width: 370, height: 570)
                        .scaleEffect(1.1)
                        .cornerRadius(20)
                        .clipped() 
                        .frame(width: 350, height: 550)
                        .cornerRadius(20)
                        .shadow(color: Color.black, radius: 6, x: 0, y: 0)
                        }
                    }
                }
            }

The issue happens when I switch from singleImage (where the parallax effect is active) to listView, or when I open a sheet for example over the singleImage view. The scrolling in the list view becomes extremely laggy, and I suspect the motion effect from the single image view is still active and updating in the background.

Upvotes: 0

Views: 85

Answers (1)

ITGuy
ITGuy

Reputation: 715

There are several issues with your implementation:

  1. You should make sure you're only using one CMMotionManager instance for your entire app as described in Apple's documentation:

Create only one CMMotionManager object for your app. Multiple instances of this class can affect the rate at which the system receives data from the accelerometer and gyroscope.

I.e. you should avoid creating a CMMotionManager instance per view, especially if you're doing it in a list of views.

  1. Using the main operation queue to process the motion updates is not recommended by Apple:

An operation queue provided by the caller. Because the processed events might arrive at a high rate, using the main operation queue is not recommended.

  1. You must call stopDeviceMotionUpdates when your views no longer need updates. This is also described in the official documentation.

  2. The update rate of 1/90 is too high, may cost too many CPU resources and may negatively impact your UI since you're putting unnecessary workload on the main thread. You should consider reducing it to 1/30.

For the moment, I recommend a solution based on UIInterpolatingMotionEffect until Apple provides a SwiftUI native solution. This avoids many of the problems described. You can find an implementation in the Swift package I have just published.

Upvotes: 0

Related Questions