Simulating CoreMotion iOS Simulator

4.4k views Asked by At

I am trying my hand at developing an application which makes use of the CoreMotion framework. I do not want to keep having to run out to test out my application, so instead I would like to find a technique which allows me to send fake it so I can try out lots of different scenarios. I wish to test scenarios such as user walking, driving, stopping etc. I do not want to have to go out walk, drive, stop etc everytime I want to test the application.

Edit: I know CoreMotion is not yet supported in the simulator. But was hoping someone could give me some ideas on how to fake this

UPDATE: What I have achieved so far using method swizzling. I have swapped back to objective-c for this, as I figure best try and figure out in this language first, then try and translate it into Swift

I have set up a method swizzle class like so

+ (void)swizzleClass:(Class)class method:(NSString*)methodName {

  SEL originalSelector = NSSelectorFromString(methodName);
  SEL newSelector = NSSelectorFromString([NSString stringWithFormat:@"%@%@", @"override_", methodName]);

  Method originalMethod = class_getInstanceMethod(class, originalSelector);
  Method newMethod = class_getInstanceMethod(class, newSelector);

  method_exchangeImplementations(originalMethod, newMethod);
}

I have created a category for CMMotion and CMAccelerometerData

- (void) simx_setAcceleration:(CMAcceleration)acceleration {
  NSValue *value = [NSValue value:&acceleration withObjCType:@encode(CMAcceleration)];
  objc_setAssociatedObject(self, ACCELERATION_IDENTIFIER, value, OBJC_ASSOCIATION_RETAIN);
}

- (CMAcceleration)override_acceleration {
  NSValue *value = objc_getAssociatedObject(self, ACCELERATION_IDENTIFIER);
  CMAcceleration acc;
  [value getValue:&acc];
  return acc;
}

Then the category for CMMotionManager class.

- (void) simx_setAccelerationData:(CMAcceleration )accelerationData
{
    NSValue *value = [NSValue value:&accelerationData withObjCType:@encode(CMAcceleration)];
    objc_setAssociatedObject(self, HANDLER_IDENTIFIER, value, OBJC_ASSOCIATION_RETAIN);
}

- (CMAccelerometerData *)override_accelerometerData
{
    NSValue *value = objc_getAssociatedObject(self, HANDLER_IDENTIFIER);
    CMAcceleration acc;
    [value getValue:&acc];

    CMAccelerometerData *data = [[CMAccelerometerData alloc] init];

    //Add
    [data simx_setAcceleration:acc];

    return data;
}

Swizzling the methods is done like this

 [CESwizzleUtils swizzleClass:[CMMotionManager class]
                              method:@"INSERT METHOD NAME TO BE SWIZZLED HERE"];

This allows me to pass in my own data

 //Add
    CMAccelerometerData *data = [[CMAccelerometerData alloc] init];
    [data simx_setAcceleration:acc];
    [motionManager simx_setAccelerationData:acc];

So I can retrieve data like this

motionManager.accelerometerData.acceleration.x;

I have also method swizzled the DeviceMotion class as well. Here is quick example app I have which pulls data from the accelerometer and gyroscope using the method swizzle techniques

enter image description here

When the test button is clicked, it randomly generates accelerometer and gyroscope data and updates the labels.

Code looks like this

-(IBAction)testing:(id)sender
{
  //random double just generates a random double between 0 and 1
    CMAcceleration acc;
    acc.x = [self randomDouble:0 :1];
    acc.y = [self randomDouble:0 :1];
    acc.z = [self randomDouble:0 :1];

    //Add
    CMAccelerometerData *data = [[CMAccelerometerData alloc] init];
    [data simx_setAcceleration:acc];
    [motionManager simx_setAccelerationData:acc];

    //Sim gravity and userAcel
    CMAcceleration grav;
    grav.x = [self randomDouble:0 :1];
    grav.y = [self randomDouble:0 :1];
    grav.z = [self randomDouble:0 :1];

    CMAcceleration userAcel;
    userAcel.x = [self randomDouble:0 :1];
    userAcel.y = [self randomDouble:0 :1];
    userAcel.z = [self randomDouble:0 :1];

    CMDeviceMotion *deviceMotion = [[CMDeviceMotion alloc] init];
    [deviceMotion simx_setUserAcceleration:userAcel];
    [deviceMotion simx_setGravity:grav];
    [motionManager simx_setDeviceMotion:deviceMotion];

    accelLabael.text = [NSString stringWithFormat:@"Accelerometer: %.02f %.02f %.02f", motionManager.accelerometerData.acceleration.x,motionManager.accelerometerData.acceleration.y,motionManager.accelerometerData.acceleration.z];

    gravityLabel.text = [NSString stringWithFormat:@"Gravity: %.02f %.02f %.02f", motionManager.deviceMotion.gravity.x,motionManager.deviceMotion.gravity.y,motionManager.deviceMotion.gravity.z];


    accelSpeedLabel.text = [NSString stringWithFormat:@"Accel: %.02f %.02f %.02f", motionManager.deviceMotion.userAcceleration.x,motionManager.deviceMotion.userAcceleration.y,motionManager.deviceMotion.userAcceleration.z];
}

What I am struggling to figure out is how to get this methods. I have swizzled many of the CoreMotion methods, but need a little help with this one. At present on the simulator, despite the fact that MotionManager now stores data for the gyroscope, accelerometer etc, these block methods do not fire.

 [activityManager startActivityUpdatesToQueue:[[NSOperationQueue alloc] init] withHandler:   ^(CMMotionActivity *activity){

}];


 [motionManager startDeviceMotionUpdatesToQueue:[[NSOperationQueue alloc] init]
                                           withHandler:^ (CMDeviceMotion *motionData, NSError *error) {

}];

I have also swizzled these methods

- (BOOL)override_isAccelerometerActive;
- (BOOL)override_isDeviceMotionActive;
-(BOOL)override_isGyroAvailable;

So they always return true, but still can't get these blocks firing. I would like some help trying to figure out how to correctly swizzle these methods so I can begin sending and recieving mock data to the simulator

Edit: I did swizzle the accelerometer update method by adding the following category. to the CMMotionManager class

-(void)override_startAccelerometerUpdatesToQueue:(NSOperationQueue *)queue
                                     withHandler:(CMAccelerometerHandler)handler{

    dispatch_async(dispatch_get_main_queue(), ^{

        NSLog(@"Test %.02f %.02f %.02f", self.accelerometerData.acceleration.x,self.accelerometerData.acceleration.y,self.accelerometerData.acceleration.z);


    });
}

This works well enough, as I have swizzled the accel data.

I then tried this with the CMMotionActivityManager class by adding this category

-(void)override_startActivityUpdatesToQueue:(NSOperationQueue *)queue
                                withHandler:(CMMotionActivity *)activity
{
    dispatch_async(dispatch_get_main_queue(), ^
                   {
                      BOOL walking = [activity walking];
                       NSLog(@"%i", walking);
                   });
}

However I am getting this error here

[NSGlobalBlock walking]: unrecognized selector sent to instance 0x1086cf330

Any suggestions would be appreciated

UPDATE 2:

Sorry late response, had to wait till today to try this. I updated the method per your suggestion so it now works

-(void)override_startActivityUpdatesToQueue:(NSOperationQueue *)queue
                                withHandler:(CMMotionActivityHandler )activity
{

}

Thing is, I need access to CMMotionActivity in order to figure out if they are walking running etc. The original code

 [activityManager startActivityUpdatesToQueue:[[NSOperationQueue alloc] init] withHandler:   ^(CMMotionActivity *activity){
        dispatch_async(dispatch_get_main_queue(), ^
        {
            [activity walking];
        });
    }];

allows you to access this variable. However now that I have swizzled this, it now calls my new declaration inside my category file which contains no CMMotionActivity variable. Any ideas on how I can access this. This is getting a bit complicated, but this is the last roadblock for me before I can finally start mocking CoreMotion data. I have already simulated the gyroscope and compass data, and I have got data from real journeys, so I can feed that into the simulator. But I need to then have it tell me if the user is walking, running, driving etc.

Any suggestions?

2

There are 2 answers

9
mattt On BEST ANSWER

Your method signature for override_startActivityUpdatesToQueue:withHandler: is incorrect. The handler parameter should be a CMMotionActivityHandler block that provides a single CMMotionActivity argument:

typedef void (^CMMotionActivityHandler)(CMMotionActivity *activity)
0
firetrap On

Maybe i'm late for the show but here is my 2 cents.

I tried another approach. Instead of accessing to the sensors like @AdamM i override programmatically the CMMotionActivity when receiving from the CMMotionActivityHandler.

The problem: CMMotionActivity properties are get only and the init doesn't take any argument. Apple docs:

You don’t create instances of this class yourself. The CMMotionActivityManager object creates them and sends them to the handler block you registered

Solution: Subclass CMMotionActivity and override the get only properties.

class SimulatedCMMotionActivity: CMMotionActivity {

    private var _startDate: Date = Date()
    private var _timestamp: TimeInterval = 0.0
    private var _unknown: Bool = true
    private var _stationary: Bool = false
    private var _walking: Bool = false
    private var _running: Bool = false
    private var _automotive: Bool = false
    private var _cycling: Bool = false
    private var _confidence: CMMotionActivityConfidence = .high

    init(startDate: Date, timestamp: TimeInterval, unknown: Bool, stationary: Bool, walking: Bool, running: Bool, automotive: Bool, cycling: Bool, confidence: CMMotionActivityConfidence) {

        super.init()
        self._startDate = startDate
        self._timestamp = timestamp
        self._unknown = unknown
        self._stationary = stationary
        self._walking = walking
        self._running = running
        self._automotive = automotive
        self._cycling = cycling
        self._confidence = confidence
    }

    convenience init(motionActivity: CMMotionActivity, unknown: Bool = true, stationary: Bool = false, walking: Bool = false, running: Bool = false, automotive: Bool = false, cycling: Bool = false) {

        self.init(startDate: motionActivity.startDate, timestamp: motionActivity.timestamp, unknown: unknown, stationary: stationary, walking: walking, running: running, automotive: automotive, cycling: cycling, confidence: motionActivity.confidence)
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    override open var startDate: Date {
        _startDate
    }

    override open var timestamp: TimeInterval {
        _timestamp
    }

    override open var unknown: Bool {
        _unknown
    }

    override open var stationary: Bool {
        _stationary
    }

    override open var walking: Bool {
        _walking
    }

    override open var running: Bool {
        _running
    }

    override open var automotive: Bool {
        _automotive
    }

    override open var cycling: Bool {
        _cycling
    }

    override open var confidence: CMMotionActivityConfidence {

        _confidence
    }
}

Usage example:

 private var motionActivityHandler: CoreMotion.CMMotionActivityHandler {

        { cmMotionActivity in

            guard let motionActivity = cmMotionActivity else { return }

            let simulatedCMMotionActivity = SimulatedCMMotionActivity(motionActivity: motionActivity, unknown: false, running: true)
            self.motionActivitySample.append(simulatedCMMotionActivity)
        }
    }

Note: The SimulatedCMMotionActivity still remains a CMMotionActivity object so no code changes