HiveBrain v1.2.0
Get Started
← Back to all entries
patterncMinor

Unity3D native iOS plug-in to read pedometer data

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
nativepedometerunity3dreadiosplugdata

Problem

I've made a plug-in for Unity3d for iOS. The plug-in's job is to get the Pedometer data from history within a specified period of time and return that to Unity for later use. I am confident in my C# code, but Obj C part is what troubles me.

I'm afraid I have some potential memory-leaks should the requests come more often due to not deallocating some objects. Also it seems that iOS SDK will do some deallocations by itself and I don't need do to anything.

Please tell me if my code is acceptable quality and how it can be made better. Although this is my first time writing pure Obj C, please don't hold back on the criticism.

The code:

PedometerStatus.m - C code to determine availability of the CMPedometer.

#import 
#import 

BOOL C_isStepCountAvailable()
{
    return [CMPedometer isStepCountingAvailable];
}

BOOL C_isDistanceAvailable()
{
    return [CMPedometer isDistanceAvailable];
}


UnityBridge.h - this header file does not have a *.m file, it seem to get the method's implementation when added into Unity's Xcode project.

#ifndef UnityBridge_h
#define UnityBridge_h

#ifdef __cplusplus
extern "C"
{
#endif
    void UnitySendMessage(const char* obj, const char* method, const char* msg);
#ifdef __cplusplus
}
#endif

#endif /* UnityBridge_h */


iOS_Concrete_Pedometer.h - this is the header for my main activity. I'm not sure if I used the @property options correctly (e.g. use of nonatomic, strong etc.)

#import 
#import 

@interface iOS_Concrete_Pedometer : NSObject

+(iOS_Concrete_Pedometer *) staticInstance;

@property (nonatomic, strong) CMPedometer * pedometerInstance;
@property (readwrite) NSString * caller;

-(void) __requestPedometerData: (NSDate *) startDate : (NSDate *) endDate;
-(void) __onPedometerDataUpdate: (CMPedometerData *) pData;

+ (void) __sendMessage:(NSString*) messageName toObject:(NSString*) objectName withParameter:(NSString*) parameterString;

-(void) __initialize: (char *) callingInstance;

@end


and the

Solution

Well, there is a lot to talk about here...

I can't speak specifically about memory leaks here. I'm not sure how you're writing or compiling this code. But it's important to note that the iOS SDK absolutely does not automatically deallocate objects for you. Objective-C (in contrast to C#) is definitely not garbage collected.

In most use-cases for Objective-C (or Swift), you're writing an iOS (or OS X) application in Xcode with that language and with "ARC Enabled". ARC (Automatic Reference Counting) is memory management handled by the preprocessor. It's manual memory management like what you'd have in C or C++, but it is automatically written for you if you're compiling using ARC.

If you're not using ARC, none of your objects are ever deallocated unless you're writing the memory management code yourself. Again, I don't know enough about what you're doing and how you're getting this C# interoperability to know whether or not ARC applies here--this is something you'll have to investigate yourself.

With ARC, however, you pretty much only going to end up with memory leaks if you're creating retain cycles (to objects that strongly reference each other).

But memory issues aside, there's still a whole, whole lot going on here. Where to start...

Let's simplify this whole thing though... what you're actually doing is writing an app in another language, right? And what you actually need to create in Objective-C is a little box that takes two dates (start date, end date) and returns two values (distance, steps). Correct?

So why are we doing anything but the simplest possible implementation of this?

First, we probably don't need to bother creating an Objective-C class here. We just need to make an Objective-C .h/.m so we can use some Objective-C code, but we're going to stick with C-style functions for simplicity and language interoperability.

Now, it's important to note, instantiating NSDateFormatter objects is expensive. We only need one date formatter (ever), especially here where we're always using the same formatter.

NSDateFormatter * dateFormatter() {
    static dispatch_once_t onceToken = 0;
    static NSDateFormatter *_dateFormatter = nil;

    dispatch_once(& onceToken, ^{
        _dateFormatter = [[NSDateFormatter alloc] init];
        dateFormatter.dateFormat = @"MM/dd/yyyy HH:mm:ss";
    });

    return _dateFormatter;
}


And so we're not repeating ourselves, let's make a function to return an Objective-C NSDate object from a char *.

NSDate * dateFromCString(char *dateString) {
    NSString *dateString = [NSString stringWithCString:dateString
                                              encoding:NSUTF8StringEncoding];

    return [dateFormatter() dateFromString:dateString];
}


Both of these date-related functions are going to only be in our .m file. We have no need to expose these in the .h because remember, we're creating a box that has C-string dates as inputs and steps/distance as outputs. These are just steps in the middle.

So far, this part is pretty straight forward.

Now, again, I'm not C# expert, but given that you're creating some C-style functions, I'm going to assume C# cooperates decently enough with C. If that's a correct assumption, then we're going to talk about how C handles "callbacks", so to speak. Ultimately, the box we want to put all of this code into is a C function which has this signature:

void fetchPedometerData(
    char * fromDate,
    char * toDate,
    void (*completionHandler)(int, double)
);


And this is the only function we expose in the .h file.

So here, we have a function which takes three arguments:

  • fromDate, a char * which we convert to a date from which the data begins.



  • toDate, a char * which we convert to a date from which the data ends.



  • completionHandler, a pointer to a function which takes an integer (representing steps), a double (representing distance), and returns void.



So now, either in a C or C# layer, you implement this function (you might be able to use a C# lambda here, I don't know) and pass a pointer to that function in when you call this method.

Our implementation is going to look something like this:

```
void fetchPedometerData(char fromDate, char toDate, void (*completionHandler)(int, double)) {
_fetchPedometerData(dateFromCString(fromDate), dateFromCString(toDate), completionHandler);
}

CMPedometer *pedometer() {
static dispatch_once_t onceToken = 0;
static CMPedometer *_pedometer = nil;

dispatch_once(& onceToken, ^{
_pedometer = [[CMPedometer alloc] init];
});

return _pedometer;
}

void _fetchPedometerData(NSDate fromDate, NSDate toDate, void (*completionHandler)(int, double)) {
[pedometer() queryPedometerDataFromDate:fromDate
toDate:toDate
withHandler:^(CMPedometerData _Nullable data, NSError _Nullable error) {
if (data) {

Code Snippets

NSDateFormatter * dateFormatter() {
    static dispatch_once_t onceToken = 0;
    static NSDateFormatter *_dateFormatter = nil;

    dispatch_once(& onceToken, ^{
        _dateFormatter = [[NSDateFormatter alloc] init];
        dateFormatter.dateFormat = @"MM/dd/yyyy HH:mm:ss";
    });

    return _dateFormatter;
}
NSDate * dateFromCString(char *dateString) {
    NSString *dateString = [NSString stringWithCString:dateString
                                              encoding:NSUTF8StringEncoding];

    return [dateFormatter() dateFromString:dateString];
}
void fetchPedometerData(
    char * fromDate,
    char * toDate,
    void (*completionHandler)(int, double)
);
void fetchPedometerData(char * fromDate, char * toDate, void (*completionHandler)(int, double)) {
    _fetchPedometerData(dateFromCString(fromDate), dateFromCString(toDate), completionHandler);
}

CMPedometer *pedometer() {
    static dispatch_once_t onceToken = 0;
    static CMPedometer *_pedometer = nil;

    dispatch_once(& onceToken, ^{
        _pedometer = [[CMPedometer alloc] init];
    });

    return _pedometer;
}

void _fetchPedometerData(NSDate *fromDate, NSDate* toDate, void (*completionHandler)(int, double)) {
    [pedometer() queryPedometerDataFromDate:fromDate 
                                     toDate:toDate 
                                withHandler:^(CMPedometerData * _Nullable data, NSError * _Nullable error) {
        if (data) {
            completionHandler(data.stepsData.integerValue, data.distanceData.doubleValue);
        }
        else {
            // there was an error described by the error object
        }
    }];
}

Context

StackExchange Code Review Q#110317, answer score: 7

Revisions (0)

No revisions yet.