6
\$\begingroup\$

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 <Foundation/Foundation.h>
#import <CoreMotion/CoreMotion.h>

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 <Foundation/Foundation.h>
#import <CoreMotion/CoreMotion.h>

@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 iOS_Concrete_Pedometer.m file for the implementation:

#import "iOS_Concrete_Pedometer.h"
#import "UnityBridge.h"

@implementation iOS_Concrete_Pedometer

+ (iOS_Concrete_Pedometer*)staticInstance
{
    iOS_Concrete_Pedometer *staticInstance = [iOS_Concrete_Pedometer sharedInstance];
    if (!staticInstance)
        staticInstance = [[iOS_Concrete_Pedometer alloc] init];
    return staticInstance;

}

+ (id)sharedInstance
{
    static dispatch_once_t p = 0;
    __strong static id _sharedObject = nil;

    dispatch_once(&p, ^{
        _sharedObject = [[self alloc] init];
    });

    return _sharedObject;
}

// MAIN METHODS

- (void)__initialize: (char *) callingInstance
{
    NSString * name = [NSString stringWithCString:callingInstance encoding:NSUTF8StringEncoding];

    _caller = name;
}

- (void)__requestPedometerData:(NSDate *)startDate :(NSDate *)endDate
{
    if(!_pedometerInstance)
        _pedometerInstance = [[CMPedometer alloc] init];

    [_pedometerInstance queryPedometerDataFromDate:startDate toDate:endDate withHandler:^(CMPedometerData * _Nullable pedometerData, NSError * _Nullable error)
    {
        [[iOS_Concrete_Pedometer staticInstance] __onPedometerDataUpdate:pedometerData];
    }];
}

+ (void) __sendMessage:(NSString*) messageName
            toObject:(NSString*) objectName
       withParameter:(NSString*) parameterString
{
    UnitySendMessage(
                     [messageName cStringUsingEncoding:NSUTF8StringEncoding],
                     [objectName cStringUsingEncoding:NSUTF8StringEncoding],
                     [parameterString cStringUsingEncoding:NSUTF8StringEncoding]
                     );
}

// EVENT HANDLERS

- (void)__onPedometerDataUpdate: (CMPedometerData *) pData;
{
    int steps = pData.numberOfSteps.integerValue;
    double distance = pData.distance.doubleValue;

    NSString * stepsString = [NSString stringWithFormat:@"%d", steps];
    NSString * distanceString = [NSString stringWithFormat:@"%f", distance];

    NSString * messageForUnity = [ stepsString stringByAppendingString:@";"];
    messageForUnity = [messageForUnity stringByAppendingString:distanceString];

    [iOS_Concrete_Pedometer __sendMessage:self.caller toObject:@"ReceivePluginMessage" withParameter:messageForUnity];
}


@end

Finally, if it's of any interest I'm adding the C interface that would be the glue to communicate between Unity and the plug-in - C_Interface.m:

#import <Foundation/Foundation.h>
#import "iOS_Concrete_Pedometer.h"


void requestPedometerData(char * fromDate, char * toDate)
{
    NSDateFormatter * dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"MM/dd/yyyy HH:mm:ss"];

    NSString * fromDateString = [NSString stringWithCString:fromDate encoding:NSUTF8StringEncoding];
    NSString * toDateString = [NSString stringWithCString:toDate encoding:NSUTF8StringEncoding];

    NSDate * startDate = [dateFormatter dateFromString: fromDateString];
    NSDate * endDate = [dateFormatter dateFromString: toDateString];

    [[iOS_Concrete_Pedometer staticInstance] __requestPedometerData:startDate :endDate];
}

void init(char * callingInstance)
{
    [[iOS_Concrete_Pedometer staticInstance] __initialize :callingInstance];
}

The requestPedometerData method from the C_Interface.m is to be called multiple times during the live of the application, the availability checks are, probably, going to be checked only once.

NOTE: The error handling of trying to use the plug-in before initialising it first is done on the C# layer. I wanted to note this so that people wouldn't put too much attention to that part.

But feel free to point out any other flaws in the code.

\$\endgroup\$
7
\$\begingroup\$

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:

  1. fromDate, a char * which we convert to a date from which the data begins.
  2. toDate, a char * which we convert to a date from which the data ends.
  3. 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) {
            completionHandler(data.stepsData.integerValue, data.distanceData.doubleValue);
        }
        else {
            // there was an error described by the error object
        }
    }];
}

You may want your completion handler to take a third argument--a boolean to represent whether or not the data was successfully retrieved (in which case, a fourth argument--a string to describe the error in case of one). Alternatively, instead of just a completion handler, we also pass an error handler which is called in place of the completion handler in the case that there was an error with the query (or maybe there was an error converting the string to a date, or whatever).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.