Xcodeで1つのプロジェクトから無料版と有料版のiOSアプリをビルドする

iOSアプリの無料版と有料版を提供するとき等に、Xcodeの1つのプロジェクトから無料版と有料版の2つのアプリをビルドしたいケースがあります。

Xcodeで2つのプロジェクトに分けてしまうと、バグ修正や新機能追加のたびに両プロジェクトをコーディングする必要があって大変なので、広告や有無や制限値の差異程度であれば1つのプロジェクトから2つのアプリをビルドすると楽です。

ここでは1つのプロジェクトの同一ソースから2つのアプリをビルドする方法を説明します。

Xcode上での作業

ここでは、有料版が既にあって、新たに無料版を追加する場合を例にして説明します。

  1. 有料版のTargetを複製して無料版を作る

    1. Projectツリーを開く
    2. Targets > 有料版アプリを右クリックする
    3. Duplicateをクリックして複製する
    4. 無料版のTarget名が「Target名 copy」となっているので無料版の名前に修正する
    5. 無料版のTarget > General > Identity > Bundle Identifierを無料版のものに修正する
  2. 無料版のプロダクト名を設定する

    1. 無料版のTarget > Build Settings > Packaging > Product Name
    2. Product Nameが「Target名 copy」となっているので無料版の名前に修正する

    f:id:slowquery:20140506201727p:plain

  3. 無料版のビルド時のマクロを設定する

    1. 無料版のTarget > Build Settings -> Apple LLVM 5.X - Preprocessing
    2. Preprocessor MacrosのDebugRelease両方に無料版のビルドフラグを追加する

       FREE_VERSION=1
      

    f:id:slowquery:20140506201738p:plain

  4. コードに#ifdef無料版と有料版の処理を分岐させる

     #ifdef FREE_VERSION
         無料版の処理
     #elif
         有料版の処理
     #endif
    

    ※注意 無料版/有料版のコードを分岐させるには以下のようなパターンがあります。

    1. シングルトンオブジェクトを作って、一度#ifdef内で有料/無料フラグを設定して、それ以降はif文で分岐する
    2. #ifdefで関数内で処理を分岐する
    3. #ifdefで無料版の関数と有料版の関数をわける

    無料版にしか存在しない広告系の関数などは3の方法で消すのがよいです。一方で、関数中のしきい値が異なる程度なら1や2の方法で分岐するのがよいかもしれません。

    #ifdefマクロは強力で便利ですが、Xcodeのシンタックスハイライトを崩すケースや無料/有料両方のコード修正が必要になるケースやリファクタリング機能がうまく使えなくなるケースなど様々なデメリットもあるため、メンテナンスコストを考えて使うと良いと思います。

MagicalRecord に mogenerator を組み合わせてよいか

CoreDataを使う際に、MagicalRecordに加えてmogeneratorを使う事を推奨しているブログ記事が散見されますが、mogeneratorには以下のようにリジェクトのリスクがあるようです。

筆者はmogeneratorを使っていた事もありましたが、現在は別の理由もあってMagicalRecord単体で使っています。リジェクトのリスクはmogeneratorに限った話しではないですが、注意した方がいいかもしれません。

以上

MagicalRecordでCoreDataをお手軽に使う

iOSアプリでデータを保存(serialize/deserialize)する方法としてCoreDataがありますが、コード量が多く慣れるまでにも時間がかかります。

ここではより簡単なMagicalRecordによって、CoreDataを使ってみます。

MagicalRecordActive Recordパターンを採用したCoreDataのラッパーです。ActiveRecordは、Martin FowlerらのPatterns of Enterprise Application Architectureで名付けられ、Ruby on Railsで普及しました。

MagicalRecordがあれば、Webアプリ開発者でもCoreDataを簡単に扱えるようになり、iOSアプリ開発がより身近に感じられるようになると思います。

前提

CocoaPodsはインストール済みとします。CocoaPodsのインストール方法は以下を参照してください。

iOSアプリのプロジェクト作成

  1. Xcodeを開いて新規プロジェクトを作成します。

     File > New > Project
    
    • Use Core Dataの指定は不要です。
      • 指定すると不要なコードが生成されるため、指定しない事をおすすめします。

MagicalRecordのインストール

  1. Podfile作成

    1. .xcodeprojがあるディレクトリにPodfileファイルを作成します。
    2. Podfileに次の1行を追記します。

      pod 'MagicalRecord'

  2. MagicalRecordインストール

    1. ターミナル上でPodfileと同じディレクトリに移動して以下を実行します。

       pod install
      
  3. 設定

    1. アプリ名/アプリ名-Prefix.pchを開いて次の1行を追記します。

       #import "CoreData+MagicalRecord.h"
      

CoreDataデータモデルの定義と作成

  1. Data Modelを追加

    1. メニューからFile > New > File > Data Modelを選択します。
    2. Model.xcdatamodeldを作成します。
  2. Entityを追加

    1. ツリー上でModel.xcdatamodeldを選択します。
    2. Add EntityをクリックしてEntityを追加します。
    3. Entityを適切な名前に変更します。
    4. Entityの属性(Attributes)や関係(Relationship)を定義します。
  3. Entityから必要なソースコードを自動生成

    1. ツリー上でModel.xcdatamodeldを選択します。
    2. メニューからEditor > Create NSManagedObject Subclass > Next > Next > Createを選択します。

      Use scalar properties for priitive data typesをチェックすると以下のようなスカラー型(定数型)のプロパティがプリミティブなデータ型に変換されます。チェック不要で構いません。

      • Date -> NSTimeInterval.
      • Double -> double.
      • Float -> float.
      • Integer 16/32/64 -> int16_t/int32_t/int64_t
      • Boolean -> BOOL

MagicalRecordの起動/終了処理

  1. 起動処理

    AppDelegete.mファイルを開いてdidFinishLaunchingWithOptionsに起動処理を追加します。

     - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
     {
         // 以下を追加
         [MagicalRecord setupCoreDataStackWithAutoMigratingSqliteStoreNamed:@"database_name.sqlite"];
         return YES;
     }
    

    初期化には以下のようなメソッドが用意されています。目的に応じて選択します。

    ローカルのSQLite

     + (void) setupCoreDataStack;
     + (void) setupAutoMigratingCoreDataStack;
     + (void) setupCoreDataStackWithInMemoryStore;
     + (void) setupCoreDataStackWithStoreNamed:(NSString *)storeName;
     + (void) setupCoreDataStackWithAutoMigratingSqliteStoreNamed:(NSString *)storeName;
    

    iCloud

     + (void) setupCoreDataStackWithiCloudContainer:(NSString *)icloudBucket localStoreNamed:(NSString *)localStore;
     + (void) setupCoreDataStackWithiCloudContainer:(NSString *)containerID contentNameKey:(NSString *)contentNameKey localStoreNamed:(NSString *)localStoreName cloudStorePathComponent:(NSString *)pathSubcomponent;
     + (void) setupCoreDataStackWithiCloudContainer:(NSString *)containerID contentNameKey:(NSString *)contentNameKey localStoreNamed:(NSString *)localStoreName cloudStorePathComponent:(NSString *)pathSubcomponent completion:(void(^)(void))completion;
    
  2. 終了処理

    AppDelegete.mファイルを開いてapplicationWillTerminateに終了処理を追加します。

     - (void)applicationWillTerminate:(UIApplication *)application
     {
         // 以下を追加
         [MagicalRecord cleanUp];
     }
    

MagicalRecordでのデータ保存

  • 同期

      Album *album = [Album MR_createEntity];
      album.title = @"Friends";
          :
    
      [[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
    

MR_saveメソッドは次期メジャーリリースの3.0からは無くなるので同じ機能を持つMR_saveToPersistentStoreAndWaitを使いましょう。

  • 非同期

      Album *album = [Album MR_createEntity];
      [MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
    
          //保存前の処理(保存したいインスタンスの作成など)
          Album *localAlbum = [album MR_inContext:localContext];
          localAlbum.title = @"NeverMind";
              :
    
      } completion:^(BOOL success, NSError *error) {
    
          //保存後の処理(全件取得や件数カウントなど)
    
      }];
    
  • その他

    MagicalRecord / Docs / Saving.md

MagicalRecordでのデータ取得

  • 全件取得

      NSArray *albums = [Album findAll];
    
  • 条件を指定して取得

      NSArray *albums = [Album MR_findByAttribute:@"genre" withValue:@"piano trio" andOrderBy:@"year" ascending:YES];
    
  • その他高度な取得

    MagicalRecord / Docs / Fetching.md

MagicalRecordでのデータ更新

  • データ取得&更新

    あらかじめAlbumのインスタンス(ここではcheckyourhead)を取得しておきます。

      Album *checkyourhead = ...;
      checkyourhead.rate = [NSNumber numberWithInt:5];
      [[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
    

MagicalRecordでのデータ削除

  • 全件削除

      [Album MR_truncateAll];
      [[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
    
  • 1件削除

    あらかじめAlbumのインスタンス(ここではmellowgold)を取得しておきます。

      Album *mellowgold = ...;
      [mellowgold MR_deleteEntity];
    

MagicalRecordでのデータカウント

  • カウント

      NSNumber *count = [Album MR_numberOfEntities];
    

注意

MagicalRecordでデータの保存、更新、削除を反映する際は必ず以下を実行します。

    [[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];

以上です。MagicalRecordを使うとCoreDataを簡単に扱えます。

CocoaPodsでiOSのライブラリを簡単にインストールする

CocoaPods

Objective-Cのライブラリを自動的にインストールするためのツールです。node.jsにおけるnpmやRubyにおけるRubyGemsと似たようなパッケージ管理ツールです。

CocoaPodインストール

$ gem install cocoapods
$ pod setup

iOSプロジェクトへのライブラリインストール

  1. XcodeiOSプロジェクトのトップディレクトリに移動します。

  2. 定義ファイルPodfileを作成します。

  3. 定義ファイルにインストールしたいライブラリを記載します。

    バージョンを指定しない場合

     pod 'ViewDeck'
    

    バージョンを指定する場合

     pod 'ViewDeck', '~> 2.2.4'
    
    
         [ViewDeck](http://www.cocoacontrols.com/controls/iiviewdeckcontroller)を使う場合
    
  4. ライブラリをインストールします。

     $ pod install
    

TouchDB (CouchDB互換の組込用NoSQL) をiOS開発で使ってみる

TouchDBとは

概要

公式サイトを見ると次のように解説されています。

TouchDB is a lightweight Apache CouchDB-compatible database engine suitable for embedding into mobile or desktop apps. Think of it this way: If CouchDB is MySQL, then TouchDB is SQLite.

訳: TouchDBはlightweightなApach CouchDB互換のDBエンジンで、モバイルやデスクトップアプリへの組み込みに適しています。CouchDBMySQLとするとTouchDBはSQLiteに相当します。

特徴

  • モバイル向けのCouchDB互換のNoSQL
  • CouchBaseのメンバーが開発
  • iOS用(Objective-C実装)とAndroid用(Java実装)がある。
  • RestAPIがほとんど一緒(ユーザ管理機能等はなし)
  • CouchDBでレプリケートできる(例えばモバイルのTouchDBとサーバのCouchDBを同期できる)
  • コードが350KB(ARM7の場合)と軽量
  • (デスクトップより)相対的に遅いモバイルCPUでも高速に起動する
  • 省メモリ

詳細は公式:couchbaselabs / TouchDB-iOSを参照してください。

CouchCocoa

CouchCocoa.frameworkのビルド

  1. ローカルリポジトリを作成します。

     git clone https://github.com/couchbaselabs/CouchCocoa
     cd CouchCocoa
     git submodule init
     git submodule update
    
  2. Xcodeでプロジェクト開いてビルドします。

    1. XCodeでプロジェクトを開く

       open CouchCocoa.xcodeproj
      
    2. 左上のScheme設定を"iOS Framework"に設定

    3. メニューでProduct > Build
  3. ビルド結果を確認します。

    1. XCode > Preferences > Locations を開く
    2. Derived Dataの下のリンクをクリック
    3. CouchCocoa-xxxxxx > Build > Products > Release-ios-universalを開いてCouchCocoa.frameworkができていることを確認

TouchDB

TouchDB.frameworkのビルド

  1. ローカルリポジトリを作成します。

     git clone git://github.com/couchbaselabs/TouchDB-iOS.git
     cd TouchDB-iOS
     git submodule init
     git submodule update
    
  2. Xcodeを開いてビルドします。

    1. プロジェクトをXCodeで開く

       open TouchDB.xcodeproj
      
    2. 左上のScheme設定を"iOS Framework"に設定

    3. メニューで`Product > Build
  3. ビルド結果を確認します。

    1. XCode > Preferences > Locations を開く
    2. Derived Dataの下のリンクをクリック
    3. TouchDB-xxxxxx > Build > Products > Release-ios-universalを開いてTouchDB.frameworkができていることを確認

iOSプロジェクトへのTouchDB, CouchCocoaインポート

  1. Xcodeのプロジェクトを開く
  2. 先ほどビルドしたCouchCocoa.frameworkをソースツリーのframeworksにドラッグ&ドロップ
  3. 先ほどビルドしたTouchDB.frameworkをソースツリーのframeworksにドラッグ&ドロップ
  4. Destination"Copy items into~"にチェック
  5. BuildSettingのOther Linker-ObjCを追加
  6. BuiodPhaseLink Binary with Libraliesに以下を追加

    • CFNetwork.framework
    • Security.framework
    • SystemConfiguration.framework
    • libsqlite3.dylib
    • libz.dylib
  7. アプリからTouchDBを利用

    didFinishLaunchingWithOptionsなどでDBを起動

    例:

       CouchTouchDBServer* server = [CouchTouchDBServer sharedInstance];
        self.database = [server databaseNamed: @"testdb"];
        NSError* error;
        if (![self.database ensureCreated: &error]) {
            NSLog(@"CouchDBが起動しました!");
        }

詳細な使い方はまた書くかもしれませんが、公式サイトを参照してください。