Search code examples
objective-cswiftios-simulatorcore-motionxcode6.1

Simulating CoreMotion iOS Simulator


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?


Solution

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