“Distribute” an under development app without enrolling iOS Developer Program

You are developing an iOS App.

Prior to Xcode 7, you have to enroll into Apple’s iOS Developer Program (IDP) to install an App to a device, either yours or your friend’s.

Now in Xcode 7, you can compile and deploy an under development App to your device.
The problem is: How do you “distribute” your App to your friend?

I quoted the “distribute” here, because I am talking about a casual situation that it is not a serious distribution but only within a small scope.

If your friend lives nearby, this is easy. You do the something you did for yourself. You connect his/her device to your Mac/Macbook and use Xcode to install the App.

If you need to make this happen remotely, we need some work. This solution only works if your friend has a Mac with Xcode 7 installed.

There are two options.

1. You send the whole project to your friend and ask him/her to compile it and deploy it to the device. This requires advanced knowledge if your project is somehow complex.

2. After you compiled your App, you can get the actually “AppName.app” file after compiling it for device from Xcode. You send this “AppName.app” to your friend. He/she can open Xcode and follow “Window->Device”, then drag-n-drop the “AppName.app” to his/her device.

However, it is very likely that the second option will show an error “a valid provisioning profile for this executable was not found” on your friend’ screen.

This is because without IDP, the provisioning file we get from Xcode contains the exact UDID (unique device ID) of the device connected to Xcode during compilation. Any other device would not accept the App unless its UDID is listed in the provisioning file. But without IDP, we can not explicitly add UDID to an provisioning file.

Here is the workaround.

1. Register a new Apple ID if you don’t want to share your Apple ID with your friend (for good reason).
2. Ask your friend to add this new Apple ID to your Xcode Account (Preference -> Account -> + button).
3. Your friend will need to create an iOS project (simply choose the single-view Application template), compile it and install it to the device. During this process, Xcode will prompt to ask to “fix issues”. Confirm that and make sure the right “Apple ID” is selected in the “Team” field.
4. Now the UDID of the remote device is associated with the Apple ID.
5. On your local machine, use the same Apple ID to compile the App for device and send out the “AppName.app” file. Now because the UDID of both your device and the remote device are all listed in the provisioning file, the “AppName.app” can be installed into the remote device by either iTunes or Xcode.

This solution is not perfect but it works well without paying $99 each year. Good luck!

iOS上mxnet的一个演示App

mxnet是最近火的不行的一个深度学习的框架,支持好多好多语言,有好多好多大牛在写。

之前也有想过把同样很牛很牛的caffe跑到iOS上看看速度怎么样,但是caffe有一大堆文件,感觉做起来很麻烦。

最近看到mxnet居然有一个单文件的版本
就做了一个简单的图像识别的演示App。跑在6上速度还可以,大概4秒一张图。

代码在这里:

WhatsThis-iOS on Github

利用摄像头判断手机移动方向[MPMotionPattern]

这部分代码是做原型用的,现在已经不在项目里。
也没有什么特别的算法,所以就放在这里吧。
https://github.com/pppoe/MPMotionPattern

需要用到之前的“iOS逐帧处理录像-MPVideoProcessor”
主要的算法是从TinyMotion这个项目来的,用到的也是他们的算法。
功能很简单,就是通过iPhone摄像头拍摄的图像,判断手机当前的移动方向。

Screenshot

现在也只是支持上下左右。因为iPhone本身就有传感器可以取到这种动作,所以用图像似乎也没有什么特别的优势…
只是感觉挺好玩的。XD

iOS逐帧处理录像-MPVideoProcessor

一般在iOS上做录像都可以直接使用UIImagePickerController。
但有时候难免需要做逐帧的处理,比如实时的滤镜之类的。

参照这个帖子: A (quasi-) real-time video processing on iOS 把使用AVFoundation做录像的代码,做了一个简单的封装。

Delegate可以得到逐帧的彩色或者灰度图,然后就可以加上自己需要的处理了。

代码放在Gihub上:MPVideoProcessor
具体使用请参见Github上的Readme.

[iOS]How to save and load a custom object?

Here we go.

How to write a custom object which can be archived to a text file?

Usually, we use NSKeyedArchiver to serialize an object and write it to a file.
Correspondingly, NSKeyedUnarchiver is used to get the object from the file.

The NSKeyedArchiver’s interface is like this:

+ (BOOL)archiveRootObject:(id)rootObject toFile:(NSString *)path;

NSKeyedArchiver is a NSCoder. The rootObject should conform to the protocol NSCoding.

@protocol NSCoding

- (void)encodeWithCoder:(NSCoder *)aCoder;
- (id)initWithCoder:(NSCoder *)aDecoder;

@end

For example, now we have a customObject:

@interface CustomObject : NSObject  {
    NSString*   mStringValue;
    int         mIntValue;
    BOOL        mBOOLValue;
}

@property (non-atomic, retain) NSString *stringValue;
@property (nonatomic, assign) int       intValue;
@property (nonatomic, assign) BOOL      boolValue;

@end

It conforms to NSCoding, so we need to implement those two methods in the .m file

#define kEncodeKeyStringValue   @"kEncodeKeyStringValue" 
#define kEncodeKeyIntValue      @"kEncodeKeyIntValue" 
#define kEncodeKeyBOOLValue     @"kEncodeKeyBOOLValue" 

#pragma mark - NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:self.stringValue   forKey:kEncodeKeyStringValue];
    [aCoder encodeInt:self.intValue         forKey:kEncodeKeyIntValue];
    [aCoder encodeBool:self.boolValue       forKey:kEncodeKeyBOOLValue];
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    if ((self = [super init]))
    {
        self.stringValue = [aDecoder decodeObjectForKey:kEncodeKeyStringValue];
        self.intValue    = [aDecoder decodeIntForKey:kEncodeKeyIntValue];
        self.boolValue   = [aDecoder decodeBoolForKey:kEncodeKeyBOOLValue];
    }
    return self;
}

Now, we can save and load a customObject like:

- (IBAction)saveObjectPlain:(id)sender {

    printf("==========================================\n");
    printf("saveObjectPlain===========================\n");
    printf("==========================================\n");

    //< Create and Save the Object
    {
        CustomObject *obj = [[[CustomObject alloc] init] autorelease];
        obj.stringValue = @"The String Value";
        obj.intValue    = 12345;
        obj.boolValue   = YES;
        [NSKeyedArchiver archiveRootObject:obj toFile:[self tempFilePath]];
        printf("Save: \n %s \n", [[obj description] cStringUsingEncoding:NSUTF8StringEncoding]);
    }
    
    //< Load and Print the Object
    {
        CustomObject *obj = [NSKeyedUnarchiver unarchiveObjectWithFile:[self tempFilePath]];
        printf("Load: \n %s \n", [[obj description] cStringUsingEncoding:NSUTF8StringEncoding]);
    }
    
    printf("==========================================\n");
}

Quite easy, right?

How about to save/load an array of our CustomObjects?
Since NSArray conforms to protocol NSCoding, it is quite straightforward.

- (IBAction)saveObjectsInArray:(id)sender {

    printf("==========================================\n");
    printf("saveObjectsInArray========================\n");
    printf("==========================================\n");
    
    //< Create Two Keys and Save the Object
    {
        CustomObject *obj1 = [[[CustomObject alloc] init] autorelease];
        obj1.stringValue = @"The String Value 1";
        obj1.intValue    = 12345;
        obj1.boolValue   = YES;
        
        CustomObject *obj2 = [[[CustomObject alloc] init] autorelease];
        obj2.stringValue = @"The String Value 2";
        obj2.intValue    = 54321;
        obj2.boolValue   = NO;
        
        NSArray *array = [NSArray arrayWithObjects:obj1, obj2, nil];
        [NSKeyedArchiver archiveRootObject:array toFile:[self tempFilePath]];

        printf("Save: \n %s \n ", [[array description] cStringUsingEncoding:NSUTF8StringEncoding]);
    }
    
    //< Load and Print the Object
    {
        NSArray *array = [NSKeyedUnarchiver unarchiveObjectWithFile:[self tempFilePath]];
        printf("Load: \n %s \n ", [[array description] cStringUsingEncoding:NSUTF8StringEncoding]);
    }
    
    printf("==========================================\n");
}

So, if there is a member variable of type NSArray in our CustomObject,
we need to make sure the array contains only objects conform to NSCoding.
Otherwise, an exception of unrecognized selector [.. encodeWithCoder:] will be raised.

One step further, how to save/load a NSDictionary has a CustomObject as the key and another CustomObject as the Object.

This is actually another problem. We need to make sure CustomObject can be used as a key in the NSDictionary.
According to the documentation, NSDictionary requires its key to conform to the protocol NSCopying.

@protocol NSCopying

- (id)copyWithZone:(NSZone *)zone;

@end

The key object needs to be copied to a specified piece of memory described by NSZone.
The implementation is like:

#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {
    CustomObject *copy = [[CustomObject allocWithZone:zone] init];
    copy.stringValue = [[self.stringValue copy] autorelease];
    copy.intValue = self.intValue;
    copy.boolValue = self.boolValue;
    return copy;
}

We use allocWithZone to allocate a CustomObject on the indicated piece of memory.
Then copy member variables to the new instance.

The save/load procedure is like:

- (IBAction)saveObjectAsKey:(id)sender {

    printf("==========================================\n");
    printf("saveObjectAsKey===========================\n");
    printf("==========================================\n");
    
    //< Create Two Keys and Save the Object
    {
        CustomObject *keyObj = [[[CustomObject alloc] init] autorelease];
        keyObj.stringValue = @"The String Value Key";
        keyObj.intValue    = 12345;
        keyObj.boolValue   = YES;

        CustomObject *valObj = [[[CustomObject alloc] init] autorelease];
        valObj.stringValue = @"The String Value Value";
        valObj.intValue    = 54321;
        valObj.boolValue   = NO;
        
        NSDictionary *dict = [NSDictionary dictionaryWithObject:valObj forKey:keyObj];
        [NSKeyedArchiver archiveRootObject:dict toFile:[self tempFilePath]];

        printf("Save: \n %s \n", [[dict description] cStringUsingEncoding:NSUTF8StringEncoding]);
    }
    
    //< Load and Print the Object
    {
        NSDictionary *dict = [NSKeyedUnarchiver unarchiveObjectWithFile:[self tempFilePath]];
        printf("Load: \n %s \n", [[dict description] cStringUsingEncoding:NSUTF8StringEncoding]);
    }
    
    printf("==========================================\n");
}

The whole demo can be found here:
Demo CustomObject NSCoding

Hope it helps! :]

----------------------------------------------------
When we were learning new things, we searched for answers in Google, from Stackoverflow and followed well-written tutorials. When other one faces problems we met and solved, we are glad to answer their questions, post tutorial for the related topics. This is called the spirit of Community. I love it.

[iOS]delegate和protocol

今天上班和同事讨论工程怎么组织的时候涉及到这个话题。
iOS开发上对delegate使用广泛。
记在这里,如果有新人Google到了,希望能有点帮助。

protocol和delegate完全不是一回事,放在一起说,只是因为我们经常在同一个头文件里看到这两个word。

protocol和java里interface的概念类似,是Objective-C语法的一部分。
定义protocol如下

@protocol ClassADelegate

- (void)methodA;
- (void)methodB;

@end

那么就是定义了一组函数,这组函数放在一起叫作一个protocol,也就是协议。

函数是需要被实现的,所以如果对于class如下

@interface ClassB  {
}
@end

就叫作ClassB conform to protocol ClassADelegate,也就是说ClassB实现了这个协议,
也就是实现了这一组函数。

有了上面这个头文件,我们就可以放心作调用

ClassB *b = [[ClassB alloc] init];
[b methodA];
[b methodB];

而不用担心出现unrecognized selector sent to instance这种错误了。

所以protocol就是一组函数定义,是从类声明中剥离出来的一组定义。

id b = ...;
[b methodA];

这种用法也常见,b是一个id类型,它知道ClassADelegate这组函数的实现。

那么delegate是什么?其实和protocol没有关系。Delegate本身应该称为一种设计模式。
是把一个类自己需要做的一部分事情,让另一个类(也可以就是自己本身)来完成。
比如ClassC

@interface ClassC {
    id delegate;
}
@end

那么ClassC的实现(.m文件)里就可以用delegate这个变量了。
当然这里完全可以用其它名字而不是delegate。

我们也可以这样写

@interface ClassC {
    ClassB *delegate;
}
@end

这样我们知道了delegate是一个ClassB,它就可以提供ClassB里的方法。
可以把一部分ClassC里的工作放在ClassB里去实现。
这样的写法看起来是不是有点奇怪?或者应该写成这样?

@interface ClassC {
    ClassB *classB;
}
@end

…..

delegate没有了…
所以说其实delegate只是一种模式,大家约定俗成,当把自己内部一部分实现暴露给另外一个类去做的时候,就叫实际做事的类为delegate。

为什么会需要把内部实现提出来给另一个类做呢?
最常见的目的就是为了在隐藏实现的前提下,提供一个自定义的机会。
比如Apple提供的iOS SDK里就有众多的delegate,比如最常用的UITableView,
我们没法知道Apple怎么重用UITableViewCell,怎么处理UITableView里Cell的增加、删减,因为我们没有源码。
但是我们可以通过实现Delegate的方法来控制一个UITableView的一些行为。
UITableViewDataSource其实和delegate是一样一样的,只是由于意义不同换了个名字罢了。

protocol在此扮演了什么角色呢?
protocol是一种语法,它提供了一个很方便的、实现delegate模式的机会。
比如写UITableView的时候,Apple这么干
UITableView.m

- (void)doSomething {
    [self blahblah];
    
    [self.delegate guruguru];
   
    [self blahblah];
 }

delegate是我们写的类,这个类如果可以被传给UITableView做为其delegate,那唯一要求,就是它实现了

- (void)guruguru;

这个方法。

如果我们把这个方法定义在一个protocol里

@protocol XXXProtocol

- (void)guruguru;

@end

就说明了,UITableView需要的delegate是一个conform to XXXProtocol的类。
这就正好是

id

表达的意思。
无论具体的类是什么,它还有其它什么方法,只要它conform to这个protocol,
就说明它可以被传给UITableView,作为它的delegate。
那么Apple为了让我们知道这个protocol是delegate需要conform的protocol,
它就把XXXProtocol改成了UITableViewDelegate

这样我们看到protocol的名字里有Delegate,就知道这个protocol里的函数是用来做自定义(Customization)的了。

代码最终还是给人看的,公司里尤其如此。
大家都希望对方把事情讲得清晰易懂,如果在再有两句俗语或者行话那大家就很开心了 :]

贝塞尔曲线拟合

接上一篇Blog,这里用贝塞尔曲线来平滑多个点。

和样条插值不同,在多个点上作贝塞尔曲线的时候,曲线只穿过首尾两个点,中间的点都是作为控制点。

移动控制点,曲线也随之形变,可以造成一种拉扯的效果。在各种作图工具中,经常使用贝塞尔曲线来画曲线。一般的操作都是先画一条线段,然后可以通过拖动一个控制点来调整线段的弯曲程度。

作多点贝塞尔曲线只需要一个公式。所有的点的X值,被归一化到[0,1]区间内。
具体理论,可以参考这个页面Bézier curves。89年创建的,可有年头了。

这里还是贴代码吧。

首先需要得到X区间的总长度。

CGPoint startPt = [[_points objectAtIndex:0] CGPointValue];
CGPoint endPt = [[_points objectAtIndex:(self.pointCount - 1)] CGPointValue];
float amount = endPt.x - startPt.x;

然后就是曲线方程了,这个比样条插值要简单不少。
rank是指总的阶数,也就是实际的点数。这个函数表示n个点的贝塞尔曲线在x处的值。
这里的ux属于区间[0,1]

float (^bezierSpline)(int rank, float ux) = ^(int rank, float ux) {
        
    float p = 0.0f;
        
    for (int i = 0; i < rank; i++)
    {
        CGPoint pt = [[_points objectAtIndex:i] CGPointValue];
            
        p += pt.y * powf((1 - ux), (rank - i - 1)) * powf(ux, i) * (factorial(rank - 1) 
                  / (factorial(i) * factorial(rank - i - 1)));
    }
    
    return p;
};

下面很容易了,画图的时候步长为1,求得ux之后,带入方程得到点的y值,画曲线。

    [path moveToPoint:startPt];
    
    for (float curX = startPt.x; (curX - endPt.x) < 1e-5; curX += 1.0f)
    {
        float u = (curX - startPt.x) / amount;
        [path addLineToPoint:
            CGPointMake(curX, bezierSpline(self.pointCount, u))];
    }
    
    CGContextSetLineWidth(context, 1.0f);
    CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
    CGContextAddPath(context, path.CGPath);
    CGContextStrokePath(context);
}

和三次样条相比,由于不经过中间点,丢失细节比较多,平滑度更好。

平滑前
no bezier

三次样条
no bezier

贝塞尔曲线
no bezier

示例代码: Sample-CurveFit
和上个例子放在一个repo里。

参考: Wiki

三次样条插值和曲线拟合

很多东西不在手上用着就容易忘,尤其是书本知识。就弄这么个类别,叫作“书到用时方恨少”,来记录这些知识。

曲线拟合是一个“数值计算“中的一个基本内容。在实际的项目中,使用拟合的目的就是从有限个点得到一条平滑曲线。曲线本身也是由点构成的,所以如何从有限个点得到曲线上的其它点,就是插值所关注的内容。

插值的方法有很多,把这些个点逐个用直线段连起来也是一种插值。

样条插值是一种工业设计中常用的、得到平滑曲线的一种插值方法,三次样条又是其中用的较为广泛的一种。

Google 三次样条插值可以看得到不少材料,这里就不罗列公式了,直接看看在代码里,我们怎么做。

首先我们需要各个点的坐标,以x,y表示。

        const int len = [_points count];
        float x[len];
        float y[len];
        for (int i = 0; i < len; i++)
        {
            CGPoint p = [[_points objectAtIndex:i] CGPointValue];
            x[i] = p.x;
            y[i] = p.y;
        }

取变量x,y

从算法中可以得知,我们的目标是样条插值函数,这是一个分段函数,x最高次数为三次,在各个点二次连续可导以保证最终函数曲线的光滑性。
我们每两个点求一个三次函数,我们有n个点,那么这里就需要4(n-1)个方程。
目前我们有n个点的坐标,有n-2个连接点,有n个函数两次连续可导,这里有n+n-2+2*(n-2)共4n-6个方程,还差两个条件。
这里一般有三种处理方法,最方便的,也是我们这里使用的是自然三次样条,也就是在首尾两个点上二次导为0。

具体计算不在此列举了,根据算法构建一个方程组求一组中间值sx,左边是一个三对角矩阵。

        float h[len];
        float u[len];
        float lam[len];
        for (int i = 0; i < len-1; i++)
        {
            h[i] = x[i+1] - x[i];
        }    

        u[0] = 0;
        lam[0] = 1;
        for (int i = 1; i < (len - 1); i++)
        {
            u[i] = h[i-1]/(h[i] + h[i-1]);
            lam[i] = h[i]/(h[i] + h[i-1]);
        }
        
        float a[len];
        float b[len];
        float c[len];
            
        float m[len][len];
        for (int i = 0; i < len; i++)
        {
            for (int j = 0; j < len; j++)
            {
                m[i][j] = 0;
            }
            if (i == 0)
            {
                m[i][0] = 2;
                m[i][1] = 1;
                
                b[0] = 2;
                c[0] = 1;
            }
            else if (i == (len - 1))
            {
                m[i][len - 2] = 1;
                m[i][len - 1] = 2;
                
                a[len-1] = 1;
                b[len-1] = 2;
            }
            else
            {
                m[i][i-1] = lam[i];
                m[i][i] = 2;
                m[i][i+1] = u[i];
                
                a[i] = lam[i];
                b[i] = 2;
                c[i] = u[i];
            }
        }

求三对角矩阵,自下而上对角线上的参数是a,b,c

当然需要得到方程组右边的值

        float g[len];
        g[0] = 3 * (y[1] - y[0])/h[0];
        g[len-1] = 3 * (y[len - 1] - y[len - 2])/h[len - 2];
        for (int i = 1; i < len - 1; i++)
        {
            g[i] = 3 * ((lam[i] * (y[i] - y[i-1])/h[i-1]) + u[i] * (y[i+1] - y[i])/h[i]);
        }

下面就是求解这个方程组了,对于三对角矩阵,使用追赶法

        //< Solve the Equations
        float p[len];
        float q[len];

        p[0] = b[0];
        for (int i = 0; i < len - 1; i++)
        {
            q[i] = c[i]/p[i];
        }

        for (int i = 1; i < len; i++)
        {
            p[i] = b[i] - a[i]*q[i-1];
        }
        
        float su[len];
        float sq[len];
        float sx[len];
        
        su[0] = c[0]/b[0];
        sq[0] = g[0]/b[0];
        for (int i = 1; i < len - 1; i++)
        {
            su[i] = c[i]/(b[i] - su[i-1]*a[i]);
        }

        for (int i = 1; i < len; i++)
        {
            sq[i] = (g[i] - sq[i-1]*a[i])/(b[i] - su[i-1]*a[i]);
        }
        
        sx[len-1] = sq[len-1];
        for (int i = len - 2; i >= 0; i--)
        {
            sx[i] = sq[i] - su[i]*sx[i+1];
        }        

求得了参数,现在就得到分段插值函数了。这种函数实际上有两个参数。
一个是所在的段位,这个是整数。另一个才是自变量。
在Objective-C里用block做一个临时的函数很是方便。

        double (^func)(int k, float vX) = ^(int k, float vX) {
            double p1 =  (ph[k] + 2.0 * (vX - px[k])) * ((vX - px[k+1]) * (vX - px[k+1])) * py[k] / (ph[k] *ph[k] * ph[k]);
            double p2 =  (ph[k] - 2 * (vX - px[k+1])) * powf((vX - px[k]), 2.0f) * py[k+1] / powf(ph[k], 3.0f);
            double p3 =  (vX - px[k]) * powf((vX - px[k+1]), 2.0f) * psx[k] / powf(ph[k], 2.0f);
            double p4 =  (vX - px[k+1]) * powf((vX - px[k]), 2.0f) * psx[k+1] / powf(ph[k], 2.0f);
            return p1 + p2 + p3 + p4;
        };

到此为止,得到了曲线的函数。当然在实际画曲线的时候,我们仍然是逐个点画,
取点的时候取得密集一些就形成曲线了。这里delta取为1。

        for (int i = 0; i < [_points count]; i++)
        {
            CGPoint pt = [[_points objectAtIndex:i] CGPointValue];
            if (i == 0)
            {
                CGPathMoveToPoint(path, NULL, pt.x, pt.y);
            }
            else
            {
                CGPoint curP = [[_points objectAtIndex:i-1] CGPointValue];
                float delta = 1.0f;
                for (float pointX = curP.x; fabs(pointX - pt.x) > 1e-5f; pointX += delta)
                {
                    float pointY = func(i-1, pointX);
                    CGPathAddLineToPoint(path, NULL, pointX, pointY);
                }
            }
        }

拟合前

拟合后

示例代码在GitHub上: Sample-CurveFit

[Demo]OmniGridView

OmniGridView Code (GitHub)

OmniGridView

This is a half-finished grid view for iOS.
Cause Apple’s UITableView only allows us to add and reuse vertical cells, we need to write our own
scroll view when we want to have horizontal cells.

HOW TO USE

Drag the OmniGridView Folder to your project. Use the OmniGridView like any other UIView.
Assign a delegate to OmniGridView and implement the OmniGridViewDelegate.

Please refer to the OmniGridTestViewController.m to see the information you need to provide.

这是一个半成品的Grid View。 因为Apple的UITableView只提供垂直滚动的表格,我们可能会需要水平滚动的表格。
这里提供一个全方向滚动包含单元格重用机制的Grid View。

如何使用

把OmniGridView文件夹加入你的工程。就像使用UIView一样使用OmniGridView。
设置OminiGridView的delegate实现OmniGridViewDelegate。

请参考OmniGridTestViewController.m的实现.

#pragma mark OmniGridViewDelegate

@implementation OmniGridTestViewController (Private)

- (OmniGridCell *)gridCellAt:(OmniGridLoc *)gridLoc inGridView:(OmniGridView *)gridView {
    OmniGridCell *gridCell = [gridView dequeueReusableGridCell];
    if (!gridCell)
    {
        gridCell = [[OmniGridCell alloc] init];
        gridCell.layer.borderColor = [UIColor blackColor].CGColor;
        gridCell.layer.borderWidth = 1.0f;
        gridCell.textLabel.textAlignment = UITextAlignmentCenter;
    }
    
    gridCell.textLabel.text = [NSString stringWithFormat:@"(%d,%d)", gridLoc.row, gridLoc.col];
    
    return gridCell;
}

- (float)gridCellHeightInGridView:(OmniGridView *)gridView {
    return 200.0f;
}

- (float)gridCellWidthInGridView:(OmniGridView *)gridView {
    return 200.0f;
}

- (int)numberOfGridCellsInRow:(int)row inGridView:(OmniGridView *)gridView {
    return 12;
}

- (int)numberOfRowsInGridView:(OmniGridView *)gridView {
    return 12;
}

@end