Terry On Sunday, May 16, 2010
Is there anyone who isn't aware of the Apple store "In-App Purchase" (IAP) model?  I'm going to assume you have at least heard of it, and would like to integrate it into your app.

The attached code provides you with everything you need to get IAP working in your application.  No big deal there, there is lots of code scattered around the web that does the same thing.

What this code does, that I haven't seen anywhere else, is allow you to test out the Apple Storekit functionality in the simulator.  Let's examine the typical programming scenarios I face in developing an IAP capable iPhone/iPad app:
  1. I had code which is only supposed to function if the app hasn't been purchased, i.e., "Lite" functionality.  I needed to test that this worked on multiple runs.
  2. I had code which is only supposed to function if the app has been purchased, i.e., "Full" functionality.  Once you've got the Lite code working, you need to test that Full works just as well.
  3. I exercise my apps a lot in order to test various branches, so it must be easy to reset the state of the purchases.  Most of the StoreKit examples around the web record the app purchase in NSUserDefaults and to clear that you must delete the app from your device and re-load it.  I wanted the code to be able to reset the purchase flag just by being re-run.  While this didn't preclude the use of NSUserDefaults, I at least needed a mechanism to reset the flag.
  4. I want to run the code in the simulator.  One reason is that the NSZOMBIE code can only be run on the simulator.  Another reason was I needed to monitor the application's document directory while running, which you can't easily do on an actual device.
  5. Until I had the code relatively mature, I wanted the code to act as if a purchase occurred without actually making the "real" calls.  Otherwise the App store would have remembered that I had purchased the app on subsequent runs, and not allowed me to test certain branches, such as failed transactions.  This same approach came in handy for letting my early Beta testers "purchase" the app without actually doing so.  

The attached code does all that.  One caveat is that I made no attempt to support "subscription" IAPs.  Mostly because I don't use them, and they require a large support infrastructure to make them work.  I think the vast majority of small-to-medium apps will only ever use consumable, and non-consumable IAPs.

To use the code, add in the files under the group "Simple Store" to your project.  Modify your app delegate to call [Purchase setup] in "didFinishLaunchingWithOptions" and have a purchase button somewhere in your app that loads the PurchaseScreen controller, as the example app does.  Modify Purchase class to use your purchase ID, and you are finished.

With both SimpleStore DEBUG flags to YES your app will start up thinking it isn't purchased, and (on the simulator) you can exercise your Lite code, then "purchase" it and test your Full code.

When you are happy with the app code, and want to run a final end-to-end test, set both DEBUG flags to NO.

The solution provided includes the store class (SimpleStore) and several support classes.  I have provided the class in a project that demonstrates how the GUI (controller) classes should ONLY perform "controller" operations, while non-GUI code implements "model" logic.  One of my pet peeves with iPhone developers is that they all pay a lot of lip service to the MVC pattern while breaking most of it's rules by putting "model" logic into "controller" classes.  This isn't a stylistic issue,  this is a testability issue.  But then again, most iPhone developers don't test their code rigorously  :)

So let's make that our first Rule of Evolved Software.
Rule #1:  Keep "model" logic out of "controller" classes.


A related idea is the concept of modularity and abstraction.  If you think of classes as representing inter-related processing algorithms, one thing you want to do is try to keep the classes as tightly focused on solving a specific problem as you can.  This is represented in the provided solution in the following way:

The SimpleStore class wraps around the StoreKit and simplifies the programming API as far as the rest of the app is concerned.  Ideally we want to write this code once and then NEVER modify it again!  Because once we have tested it, and have some confidence that it works, any changes to the processing logic may introduce bugs.  So this code doesn't know anything about the app it is working in -- it is a generic solution to the difficulties associated with working with StoreKit.

The PurchaseScreen (which is a view controller) handles the GUI.  This, of course, is application specific.

But there is a little bit of model code that isn't Controller related, but isn't generic either.  This is model code which is specific to this application.  It shouldn't go into SimpleStore (because we NEVER TOUCH IT, right?) and it shouldn't go into PurchaseScreen because it isn't controller code.  Solution?  Purchase class.    Yes, the class is tiny.  Yes, the class doesn't do much.  That isn't the point.  It needs to be there.  If you "optimize" it away by moving the code into the other classes, you are learning bad habits that you will eventually regret, if you ever graduate to writing bigger systems and working with other people to develop and maintain code.

I'm only going to talk about one other thing, and that's the code to detect when we are running in the simulator.  If you google for solutions to this problem, you will find a lot of suggestions that you use preprocessor macros to determine the runtime environment, such as this:

#if TARGET_IPHONE_SIMULATOR
// This code will only appear for the simulator
#else
// This code will only appear for a real device
#endif


I'm not going to go into all the reasons to avoid #ifdef and #if, and please don't flame me just because you may have a specific situation where it is justified.  I will simply state that introducing conditional code compilation into your project will usually make it more difficult to maintain in the long run.  Avoid it whenever you can.  Searching for alternative solutions is time well spent.

In this case, there is a perfectly valid alternative approach which I've encapsulated in the RuntimeInfo class:  check to see if the name of the runtime environment includes the string "simulator".

Why is this better?  Well, what happens if you decide to make an iPad version?  Does the conditional compilation solution, as written, produce a simulator version if you target an iPad device then run it in the simulator?  I don't think it will.  At the expense of just a few more minutes we have written runtime code that should be correct, regardless of your target and regardless of your device.  We have written code which is far more likely to handle new situations better.

That's what I'm talking about.  :)

Terry

Here is a link to the project:  http://simplestoreevolved.googlecode.com/files/SimpleStoreEvolved.zip

No comments:

Post a Comment