Doug Hyde Doug Hyde - 6 months ago 79
iOS Question

Swift 3 device motion gravity

I have an iPhone application with a level in it that is based on the gravityY parameter of a device motion call to motionmanager. I have fixed the level to the pitch of the phone, as I wish to show the user whether the phone is elevated or declined relative to a flat plane (flat to the ground) through its x-axis...side to side or rotated is not relevant. To do that, I have programmed the app to slide an indicator (red when out of level) along the level (a bar)....its maximum travel is each end of the level.

The level works great...and a correct value is displayed, until the user locks the phone and puts it in his or her back pocket. While in this stage, the level indicator shifts to one end of the level (the end of the phone that is elevated in the pocket), and when the phone is pulled out and unlocked, the app does not immediately restore the level - it remains out of level, even if I do a manual function call to restore the level. After about 5 minutes, the level seems to restore itself.

Here is the code:

func getElevation () {

//now get the device orientation - want the gravity value
if self.motionManager.isDeviceMotionAvailable {

self.motionManager.deviceMotionUpdateInterval = 0.05

self.motionManager.startDeviceMotionUpdates(
to: OperationQueue.current!, withHandler: {
deviceMotion, error -> Void in

var gravityValueY:Double = 0

if(error == nil) {
let gravityData = self.motionManager.deviceMotion
let gravityValueYRad = (gravityData!.gravity.y)
gravityValueY = round(180/(.pi) * (gravityValueYRad))

self.Angle.text = "\(String(round(gravityValueY)))"
}
else {
//handle the error
self.Angle.text = "0"
gravityValueY = 0
}
var elevationY = gravityValueY
//limit movement of bubble
if elevationY > 45 {
elevationY = 45
}
else if elevationY < -45 {
elevationY = -45
}

let outofLevel: UIImage? = #imageLiteral(resourceName: "levelBubble-1")
let alignLevel: UIImage? = #imageLiteral(resourceName: "levelBubbleGR-1")

let highElevation:Double = 1.75
let lowElevation:Double = -1.75

if highElevation < elevationY {
self.bubble.image = outofLevel
}
else if elevationY < lowElevation {
self.bubble.image = outofLevel
}
else {
self.bubble.image = alignLevel
}
// Move the bubble on the level
if let bubble = self.bubble {
UIView.animate(withDuration: 1.5, animations: { () -> Void in
bubble.transform = CGAffineTransform(translationX: 0, y: CGFloat(elevationY))
})
}
})
}
}


I would like the level to restore almost immediately (within 2-3 seconds). I have no way to force calibration or an update. This is my first post....help appreciated.

Edit - I have tried setting up a separate application without any animation with the code that follows:

//

import UIKit
import CoreMotion

class ViewController: UIViewController {

let motionManager = CMMotionManager()

@IBOutlet weak var angle: UITextField!

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

@IBAction func startLevel(_ sender: Any) {
startLevel()
}

func startLevel() {
//now get the device orientation - want the gravity value
if self.motionManager.isDeviceMotionAvailable {

self.motionManager.deviceMotionUpdateInterval = 0.1
self.motionManager.startDeviceMotionUpdates(
to: OperationQueue.current!, withHandler: {
deviceMotion, error -> Void in

var gravityValueY:Double = 0

if(error == nil) {
let gravityData = self.motionManager.deviceMotion
let gravityValueYRad = (gravityData!.gravity.y)
gravityValueY = round(180/(.pi) * (gravityValueYRad))
}
else {
//handle the error
gravityValueY = 0
}
self.angle.text = "(\(gravityValueY))"
})}
}
}


Still behaves exactly the same way....

Answer Source

The problem is that your timing makes no sense. On the one hand you have told the motion manager to call you back every 0.05 seconds. On the other hand you are telling the bubble to animate its transform every time with an animation duration of 1.5! So your code is just tripping over its own feet. Every animation is canceling the previous one, and nothing happens.

Use pull, not push. Set a repeated timer going, and each time it fires, just ask the motion manager for its data. And don't animate the bubble while it's in the middle of an animation! Keep your animation duration shorter than the timer interface, or even better, don't animate.

As a test, I omitted your bubble and your angle calculation and just logged the y component of gravity, and I can't seem to reproduce the issue you're describing; as soon as we unlock the screen, the numbers seem correct to me:

import UIKit

import CoreMotion

class ViewController: UIViewController {

    let motionManager = CMMotionManager()
    var timer : Timer? = nil

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        if self.motionManager.isDeviceMotionAvailable {
            self.motionManager.deviceMotionUpdateInterval = 0.1
            self.motionManager.startDeviceMotionUpdates()
            let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) {
                [unowned self] _ in self.checkLevel()
            }
            self.timer = timer
        }
    }

    func checkLevel() {
        if let y = self.motionManager.deviceMotion?.gravity.y {
            print(y)
        }
    }
}
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download