Resource Bundle の作り方と CocoaPodsでの配布

2014年2月11日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

Resource Bundle とは画像やXibを所定の形式(.bundle)でまとめたもの。複数のファイルを1つにまとめられる他、ファイル名の名前空間を分離するのにも使える。用途としてはライブラリで使う画像や文字列(Localizable.strings)などを Resource Bundle にまとめて、ソースコードと一緒に配布する場面などで使われる。実際 CocoaPodsではライブラリ(のリソース)の配布形態として Resource Bundle を推奨している(Podspec Syntax Reference/resources)。

今回は Xcode で Resource Bundle を作りそれを CocoaPods で配布する方法を紹介する。
なお前提として既に Xcode のプロジェクトが存在するものとする。今回はそこへ Resource Bundle 作成用のターゲットを追加する。

※説明で取り上げているライブラリは実際に下記で見ることができる。
lakesoft / LKDateUtility
上記ライブラリでは日付フォーマットを plistファイルとして、各言語用のメッセージを Localizable.strings として添付する。今回はこれらのファイルを Resource Bundle化する。

1. ターゲットの作成


まず Resource Bundle 作成用のターゲットを新規に作成する。
iOS用には Bundle作成のテンプレートが用意されていないので OS X のものを使う。

Product Name に拡張子 .bundle がついたものが実際のファイル名となる(例では LKDateUtility-Resources.bundle となる)。


2. ターゲットへリソースを追加


ターゲット用のフォルダが作成されるので、そこへ Resource Bundle に含めたいファイルを追加する。
例では LKDateUtility-Resources 配下に LKDateTemplate.plist と Localizable.strings (English/Japanese)を追加している。追加する時はターゲットのチェックを入れておくこと(そうでないと後で空の Bundle ができてしまう)。


3. Build Settings を編集


SDK設定が OS X になっているので iOS に変更する(Latest iOS)。

次に Bundle のビルド先設定 Per-configuration Build Products Path を変更する。
デフォルトでは下記になっている。
$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
これを Resources へ変更する。

実体としての Resources フォルダも作成しておく(プロジェクトフォルダの直下)。


4. ビルド


ビルドすると Resources フォルダに LKDateUtility-Resource.bundle が作成される。

ファインダで右クリックして「パッケージの中身を見ると必要なファイルが揃っているのが確認できる。


5. 作成した bundle をライブラリ本体へ追加


作成できたものをライブラリ本体のターゲットへ追加する。
これでライブラリのソースコード(開発用プロジェクト)から参照できるようになる。開発用プロジェクトの依存ターゲットに指定しておけば、開発時に自動的に bundle をビルドできる。

github へ push すると普通のディレクトリとして表示される。


6. podspec へ bunlde 設定を追加


resource に書く。

ここまでで配布の仕組みは完成。このライブラリを利用するアプリ側で pod install/update すれば .bundleファイルも取り込まれてビルド時にアプリケーションパッケージ内に配置される。


その他


最後にライブラリ内からこの Resource Bundle 内のファイルへのアクセス方法について。これらのファイルは NSBundle 経由でアクセスできる。
NSString *path = [[NSBundle mainBundle] pathForResource:@"LKDateUtility-Resources" ofType:@"bundle"];
    NSBundle *bundle = [NSBundle bundleWithPath:path];

    // plist の読み込み
    _keywords = [NSArray arrayWithContentsOfFile:[LKDateUtilityBundle.bundle
                          pathForResource:NSStringFromClass(self) ofType:@"plist"]];

    // ローカライズ文字列の取得
    s = NSLocalizedStringFromTableInBundle(@"key", nil, bundle, nil);



- - - -
ここまで Resource Bundle を作る方法をくどくどと説明したが、実はもっと簡単に作る方法もある。
リソース管理には*.bundleがおすすめ
ファインダで .bundle ファイルを作りその中にファイルを詰めるだけでいい。特に凝ったことが必要なければこちらでいいかもしれない。


参考情報)
Resource Bundles

iOS Library With Resources

広告を使わないのに広告ライブラリがあってリジェクト

2014年2月10日月曜日 | Published in | 1 コメント

このエントリーをはてなブックマークに追加

備忘メモ。アプリは無料広告版と有料版があり、どちらも同じコードを使っているので後者の有料版でリジェクトにあった。昨年申請した時はパスしたが、今月のバージョンアップでリジェクト。

リジェクトの説明には AdSupport.framework他の広告系コードを無く指南の他、nm コマンドを使えば良いなど結構親切にかかれていた。

(参考)iOSアプリのバイナリの中身を見る & Private APIの利用チェックツールの紹介

今回の場合 AdSupport.frameworkの他、サードパーティの広告ライブラリを使っていたので、それらを有料版のターゲットから外した。


上記の KickCalPro の方が有料版(広告なし)。


コードの方は有料版のターゲットに IS_PRO というシンボルを設定し、#ifdefで制御した。


(参考)How to define Preprocessor Macros in Xcode

イメージ
#ifndef IS_PRO
 広告ありのケースの処理
  :
#endif


その後の再審査で無事通過した。

- - - -

その後、無料版のバージョンアップでも同じリジェクト。このアプリは操作結果画面に広告が出るタイプなのでレビューアがそれを見落としたらしい。それを指摘すると速攻で審査が通った。



位置情報取得のライブラリ LKLocationManager

2014年1月28日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

作った。というか古いコードをCocoaPodsで使えるようにまとめた。


使い方は最初に通知を設定
[NSNotificationCenter.defaultCenter addObserver:self
                                       selector:@selector(_updatedLocation:)
                                           name:LKLocationManagerDidUpdateLocationNotification
                                         object:nil];

[NSNotificationCenter.defaultCenter addObserver:self
                                       selector:@selector(_finishedLocation:)
                                           name:LKLocationManagerDidFinishLocationNotification
                                         object:nil];

ハンドラを書いて
- (void)_updatedLocation:(NSNotification*)notification
{
  LKLocationManager* manager = notification.object;
  CLLocation* location = manager.location;
    :
}

- (void)_finishedLocation:(NSNotification*)notification
{
  LKLocationManager* manager = notification.object;
  CLLocation* location = manager.location;
    :
}

位置取得をスタート
[LKLocationManager.sharedManager startUpdate];

十分な精度になるか一定時間が過ぎたら自動的に止まる(詳細はコードを見て!)。
更新→ LKLocationManagerDidUpdateLocationNotification
更新→ LKLocationManagerDidUpdateLocationNotification
 :
精度条件を満たした or タイムアウト
→ LKLocationManagerDidUpdateLocationNotification(最後のコール)
→ LKLocationManagerDidFinishLocationNotification
途中の状態は statusプロパティで取得できる。
typedef NS_ENUM(NSInteger, LKLocationManagerStatus) {
    LKLocationManagerStatusIdle = 0,
    LKLocationManagerStatusLocationUpdating,
    LKLocationManagerStatusLocationUpdated,
    LKLocationManagerStatusLocationCanceled,
    LKLocationManagerStatusLocationFailed
};
stopUpdateを使えば自分で止める事もできるが、最近の機種は精度の高い位置情報を取得するのに時間がかからなくなっているのであまり使う場面は無いと思われる(場所にもよるが)。


別クラスで位置情報から地名を取得する(Reverse Geocoding)クラスも同梱している。
[LKReverseGeocoder reverseGeocodeLocation:manager.location
                        completionHandler:^(NSArray *placemarks,
                                          NSString *addressString,
                                          NSDictionary *addressDictionary,
                                          NSError *error) {
                            self.place.text = addressString;
                              :
                        }];

addressString は AddressBookUI フレームワークを使ってローカライズされた住所を返す。
こんな感じ
東京都千代田区丸の内1丁目9番地1

addressDictionary はこう
{
    City = "San Francisco";
    Country = "United States";
    CountryCode = US;
    FormattedAddressLines =     (
        "Apple Store, San Francisco",
        "1800 Ellis St",
        "San Francisco, CA  94115-4004",
        "United States"
    );
    Name = "Apple Store, San Francisco";
    PostCodeExtension = 4004;
    State = CA;
    Street = "1800 Ellis St";
    SubAdministrativeArea = "San Francisco";
    SubLocality = "Union Square";
    SubThoroughfare = 1800;
    Thoroughfare = "Ellis St";
    ZIP = 94115;
}
FormattedAddressLines からも住所をが得られる(ただこちらは国まで入っている)。


NSKeyedArchiver の薄いラッパーライブラリ公開 LKArchiver

2014年1月23日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

ライブラリにするほどでも無いと思いつつ毎回キャッシュのディレクトリの取得はどうやったっけ、とかディレクトリやらファイル名の処理で毎回同じコードを書いている。Cocoapodsも使い始めたこともあり薄いラッパーを書いた。


使い方は簡単で保存先のディレクトリに応じて LKDocumentArchiver もしくは LKCachesArchiver を選んでクラスメソッドを呼び出すだけ。

アーカイブ
#import "LKDocumentArchier.h"

[LKDocumentDirectoryArchiver archiveRootObject:userList
                                        forKey:@"UserList"];
処理はいたって簡単で下記相当の処理が実行されるだけ。
// filename is equal to (Application Directory)/Documents/UserList.archive
[NSKeyedArchiver archiveRootObject:userList toFile:filename];
ファイル名は渡された key文字列に拡張子 ".archive" を付けたものになる(固定)。

アンアーカイブ
id userList = [LKDocumentDirectoryArchiver unarchiverObjectForKey:@"UserList"];

初回呼び出し時用に初期値も返せる。
id userList = [LKDocumentDirectoryArchiver unarchiveRootObject:userList
                                          forKey:@"UserList"
                                   defaultObject:^id{
                                       return @[].mutableCopy;
                                   }];
あるいは代わりに処理を走らせるなど。
id userList = [LKDocumentDirectoryArchiver unarchiveObject:userList
                                      forKey:@"UserList"
                                     failure:^{
                                        // do something
                                     }];



こちらも中身は NSKyedUnarchiverのメソッドを呼び出しているだけ。
id userList = [NSKeyedUnarchiver unarchiverObjectWithFile:filename];

なお共に @try/@catchで例外対応していて、例外発生時にはNSLogに書き出す。ただし処理は中断しない。

その他
アーカイブファイル削除
[LKDOcumentDirectoryArchiver removeArchiverForKey:@"UserList"];

アーカイブファイルの存在チェック
[LKDocumentDirectoryArchiver archiverExistsForKey:@"UserList"];

キャッシュディレクトリを使いたい場合は LKCachesDirectoryArchiverを使う。
[LKCachesDirecotryArchiver archiveRootObject:userList toFile:filename];



- - - -
コードが短すぎてわざわざライブラリにすべきか微妙ではあるが。。

アプリのできるまで(その後)KickReminder 2.0 リリース

2014年1月21日火曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

以前 アプリのできるまで KickReminder で紹介したアプリをバージョンアップした。デザインも一新して iOS7完全対応。2/1に間に合った ^^;


ちなみに以前のデザインはこう。


(当社比)3倍ほど垢抜けた。画面遷移も標準に合わせた(以前はスライド式に重なるビューを使ってた)。

- - - -
自分自身でも意外によく使うアプリの一つになったのだが、デザインが古臭くて気になってた。ユーザからの要望をきっかけに iOS7準拠のデザインに完全にビューを作りなおした。
モデルは書き直しが無かったので、こういうケースでは MVC が非常に役に立った。ビューコントローラはコピペしつつ書き直し。ただグループ名や日付のリストのコントローラは元々非ViewControllerなクラスとして作ってたのでここもモデル同様そのまま再利用できた。UITableViewDataSourceとか UITableViewDelegateの実装にあたるところ。新規開発でも試行錯誤の過程でレイアウトを変えることは頻繁にあるので最近はこのスタイルを取るようにしている。


現在セール実施中(来週末まで)
200円→100円


LKCodingObject のサブクラス対応

2014年1月19日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

早速修正

プロパティ定義だけでアーカイブできるライブラリ LKCodingObject公開

class_copyPropertyList で取得できるプロパティには親クラスのリストが含まれないことに実際のプロジェクトで使っていたら気がついた。この為、親クラスで定義されているプロパティがアーカイブ/アンアーカイブの対象にならない。

そこで再帰的に親クラスを取得してプロパティ名を取得するように手を入れた。
- (void)_propertyNamesForClass:(Class)cls propertyNames:(NSMutableArray*)propertyNames
{
    Class superClass = class_getSuperclass(cls);
    if (superClass != [NSObject class]) {
        [self _propertyNamesForClass:superClass propertyNames:propertyNames];
    }

    unsigned int count, i;
    objc_property_t *objc_properties = class_copyPropertyList(cls, &count);
    
    for(i = 0; i < count; i++) {
        objc_property_t objc_property = objc_properties[i];
        NSString* name = [NSString stringWithUTF8String:property_getName(objc_property)];
        [propertyNames addObject:name];
    }
    free(objc_properties);
    
}


サブクラスのテストケースも加えておいた。
なお誤ってタグを 1.1にしてしまった(1.0は欠番)。

プロパティ定義だけでアーカイブできるライブラリ LKCodingObject公開

| Published in | 0 コメント

このエントリーをはてなブックマークに追加

プロパティネタの第2弾。



使い方は簡単で LKCodingObject を派生しておいて、普通にプロパティを定義するだけ。
#import "LKCodingObject.h"

@interface UserInfo : LKCodingObject
@property (strong, nonatomic) NSString* name;
@property (strong, nonatomic) NSString* email;
@end

アーカイブ
UserInfo* userInfo = UserInfo.new;
userInfo.name = @"Hoge";
userInfo.email = @"hoge@xcatsan.com";
[NSKeyedArchiver archiveRootObject:userInfo toFile:@"user_info.dat"];

アンアーカイブ
UeserInfo* userInfo = [NSKeyedUnarchiver unarchiveObjectWithFile:@"user_info.dat"];

通常だと NSCodingプロトコルのメソッド initWithCoder: encodeWithCoder: を実装する必要があるが、このライブラリを使うとサブクラス化するだけでメソッドを書かずにアーカイブ・アンアーカイブできる。


ライブラリのコードは簡単で initWithCoder: encodeWithCoder: を実装しているだけ。
- (id)initWithCoder:(NSCoder*)decoder
{
    self = [super init];
    if (self) {
        for (NSString* name in self._propertyNames) {
            id value = [decoder decodeObjectForKey:name];
            [self setValue:value forKey:name];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder
{
    for (NSString* name in self._propertyNames) {
        id value = [self valueForKey:name];
        if ([value conformsToProtocol:@protocol(NSCoding)]) {
            [coder encodeObject:value forKey:name];
        }
    }
}

プロパティ名をランタイム関数から取得してきて KVCでセット・ゲットしてる。intやfloatのプロパティも適当にラップしてくれてうまく動く。

プロパティ名取得
- (NSArray*)_propertyNames
{
    NSMutableArray* propertyNames = @[].mutableCopy;
    
    unsigned int count, i;
    objc_property_t *objc_properties = class_copyPropertyList(self.class, &count);
    
    for(i = 0; i < count; i++) {
        objc_property_t objc_property = objc_properties[i];
        propertyNames[i] = [NSString stringWithUTF8String:property_getName(objc_property)];
    }
    free(objc_properties);
    return propertyNames;
}


注意点としては、NCodingに準拠していないクラスのプロパティは無視すること。エラーにならないので注意。
ここは例外を飛ばすとかしたほうがいいのかもしれない。


- - - -
CocoaPods 便利だわ。諦めてた自作コードの再利用が進む進む。特に小さなコードはコピペしてしまうのが常だったが CocoaPodsだと管理が苦にならない。ただ CocoaPods対応が若干面倒なのでこの辺りの自動化がもう少しできると良さそう。


人気の投稿(過去 30日間)