ラベル ライブラリ の投稿を表示しています。 すべての投稿を表示
ラベル ライブラリ の投稿を表示しています。 すべての投稿を表示

【Library】写真のグルーピングやフィルタリングが可能な ALAssetsLibrary用ライブラリ

2014年5月31日土曜日 | Published in | 0 コメント

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

ALAassetsLibraryから取得した写真を月ごとにグルーピングしたり、JPEGだけあるいはスクリーンショットだけを抜き出すといった処理が行えるライブラリを作りました。このライブラリではモデルクラスのみを提供しています(ビューはありません)。



基本


クラス図はこんな感じ。

左の3クラス(LKAssetsLibrary,LKAssetsGroup,LKAsset)がALAssetsLibraryの主要クラスのラッパーとなっていて便利メソッドが用意されている。

LKAssetの定義
@interface LKAsset : NSObject

// Properties (Status)
@property (assign, nonatomic, readonly) BOOL deleted;

// Properties (Image)
@property (weak  , nonatomic, readonly) UIImage* thumbnail;
@property (weak  , nonatomic, readonly) UIImage* aspectRatioThumbnail;
@property (weak  , nonatomic, readonly) UIImage* fullScreenImage;
@property (weak  , nonatomic, readonly) UIImage* fullResolutionImage;

// Properties (Date number)
@property (assign, nonatomic, readonly) NSTimeInterval timeInterval;
@property (assign, nonatomic, readonly) NSInteger dateTimeInteger;  // yyyyMMddHH

// Properties (ALAsset property)
@property (strong, nonatomic, readonly) NSURL* url;
@property (strong, nonatomic, readonly) CLLocation* location;
@property (strong, nonatomic, readonly) NSDate* date;
@property (strong, nonatomic, readonly) NSString* fileExtension;    // upper string JPG, PNG, ...
@property (assign, nonatomic, readonly) CGSize size;
@property (assign, nonatomic, readonly) LKAssetType type;

// Properties (Filter)
@property (assign, nonatomic, readonly) BOOL isJPEG;
@property (assign, nonatomic, readonly) BOOL isPNG;
@property (assign, nonatomic, readonly) BOOL isScreenshot;
@property (assign, nonatomic, readonly) BOOL isPhoto;
@property (assign, nonatomic, readonly) BOOL isVideo;
  :
ちなみに fullScreenImageとfullResolutionImageは回転している写真を適正に補正してくれる。

使い方の例。
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(_assetsLibraryDidSetup:)
                                                 name:LKAssetsLibraryDidSetupNotification
                                               object:nil];
    self.assetsLibrary = [LKAssetsLibrary assetsLibrary];
    [self.assetsLibrary reload];
}

ALAssetsLibraryと異なり blocksではなく通知ベースで使う。アセットの取得が済むと LKAssetsLibraryDidSetupNotification が通知されるので、後は LKAssetsGroupとLKAssetを取り出して使っていく。

- (void)_assetsLibraryDidSetup:(NSNotification*)notification
{
    [self.tableView reloadData];
}

グループのタイトルとサムネイルをテーブルのセルへ表示する例
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"GroupCell" forIndexPath:indexPath];

    LKAssetsGroup* assetsGroup = self.assetsLibrary.assetsGroups[indexPath.row];   
    cell.imageView.image = assetsGroup.posterImage;
    cell.textLabel.text = assetsGroup.description;
    return cell;
}

アセット(画像)のサムネイルをUICollectionViewCellへ表示する例
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    AssetCell* cell = (AssetCell*)[collectionView dequeueReusableCellWithReuseIdentifier:@"AssetCell"
                                                                           forIndexPath:indexPath];
    LKAsset* asset = [self.assetsCollection assetForIndexPath:indexPath];
    cell.imageView.image = asset.thumbnail;
    return cell;
}


コレクション


さてこのライブラリの売りは基本で紹介したクラスを利用して作られた LKAssetsCollectionにある。LKAssetsCollectionは様々な条件を満たすアセットの集合を取り扱うことができる。例えばLKAssetsCollectionを使うと「月ごと」に分類された 「JPEGとPNG画像」を「日付降順でソート」したアセットの配列を作ることができる。

LKAssetsCollection
   .entries
   |
   |--LKAassetsCollectionEntry(2014年5月)
   |   .assets
   |    |
   |    |--LKAsset(JPEG画像 2014/05/21 10:00)
   |    |--LKAsset(PNG 画像 2014/05/11 10:00)
   |    |--LKAsset(JPEG画像 2014/05/01 10:00)
   |    :
   |--LKAassetsCollectionEntry(2014年4月)
   |   .assets
   |    |
   |    |--LKAsset(JPEG画像 2014/04/21 10:00)
   |    |--LKAsset(PNG 画像 2014/04/11 10:00)
   |    |--LKAsset(JPEG画像 2014/04/01 10:00)
   |    :
   :

LKAssetsCollectionの配下には LKAssetsCollectionEntryの配列があり、これがさらに LKAssetsの配列を持つ。

一旦コレクションが作れれば後は indexPathで末端の LKAssetを取り出せるので UITableView や UICollectionView で簡単に使うことができる。
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    AssetCell* cell = (AssetCell*)[collectionView dequeueReusableCellWithReuseIdentifier:@"AssetCell"
                                                                           forIndexPath:indexPath];
    LKAsset* asset = [self.assetsCollection assetForIndexPath:indexPath];
    cell.imageView.image = asset.thumbnail;
    return cell;
}

LKAssetsCollectionのインターフェイス。
@interface LKAssetsCollection : NSObject

@property (nonatomic, weak  , readonly) LKAssetsGroup* group;
@property (nonatomic, strong, readonly) NSArray* entries;   // 

@property (nonatomic, strong, readonly) id <LKAssetsCollectionGrouping> grouping;
@property (nonatomic, strong) id <LKAssetsCollectionFilter> filter;
@property (nonatomic, strong) id <LKAssetsCollectionSorter> sorter;

+ (instancetype)assetsCollectionWithGroup:(LKAssetsGroup*)group grouping:(id <LKAssetsCollectionGrouping>)grouping;

@end

@class LKAsset;
@interface LKAssetsCollection (NSIndexPath)
- (LKAsset*)assetForIndexPath:(NSIndexPath*)indexPath;
@end

コレクションに含めるアセットを決める条件は3種類指定することができる。

・グルーピング  (例)年ごと、月ごと、日ごと、時ごと
・フィルター   (例)JPEGのみ、PNGのみ、スクリーンショットのみ、ほか
・ソーター    (例)日付昇順・降順

上記の処理を定義するためにそれぞれプロトコルが定義されている。
 LKAssetsCollectionGrouping
 LKAssetsCollectionFilter
 LKAssetsCollectionSorter
これらを実装するインスタンスを LKAssetsCollectionへ渡すことで条件に合致するアセットの集合ができあがる。

初期化の例
LKAssetsCollectionGrouping* grouping = [LKAssetsCollectionGrouping assetsCollectionGroupingWithType:self.groupingType];
   
self.assetsCollection = [LKAssetsCollection assetsCollectionWithGroup:self.assetsGroup
                          grouping:[LKAssetsCollectionDateGrouping groupingWithType:self.groupingType]];
self.assetsCollection.filter = [LKAssetsCollectionGenericFilter filterWithType:LKAssetsCollectionGenericFilterTypeJPEG|LKAssetsCollectionGenericFilterTypePNG];
self.assetsCollection.sorter = [LKAssetsCollectionDateSorter sorterAscending:NO];

LKAssetsCollectionDateGrouping や LKAssetsCollectionGenericFilter、LKAssetsCollectionDateSorter は先程のプロトコルの実装クラスになる。これらは標準で提供される。なおグルーピングと、ソースとなる LKAssetsGroupは初期設定しかできず後からは変更できない(immutable)。一方、フィルターとソーターは後から変更を適用することが可能。

LKAssetsCollectionDateGrouping で指定可能なグルーピング単位
typedef NS_ENUM(NSInteger, LKAssetsCollectionGroupingType) {
    LKAssetsCollectionGroupingTypeAll      = 0,
    LKAssetsCollectionGroupingTypeYearly   = 11,          // 年ごと
    LKAssetsCollectionGroupingTypeMonthly  = 12,          // 月ごと
    LKAssetsCollectionGroupingTypeWeekly   = 13,          // 週ごと(月曜始まり〜日曜までを1週間とみなす)
    LKAssetsCollectionGroupingTypeDaily    = 14,          // 日ごと
    LKAssetsCollectionGroupingTypeHourly   = 15,          // 時ごと
};

LKAssetsCollectionGenericFilter で指定可能なフィルタ条件。ビットORで複数指定が可能。
typedef NS_ENUM(NSUInteger, LKAssetsCollectionFilterType) {
    LKAssetsCollectionFilterTypePhoto        = (1 << 0),
    LKAssetsCollectionFilterTypeVideo        = (1 << 1),
    LKAssetsCollectionFilterTypeJPEG         = (1 << 2),
    LKAssetsCollectionFilterTypePNG          = (1 << 3),
    LKAssetsCollectionFilterTypeScreenShot   = (1 << 4),
    LKAssetsCollectionFilterTypeAll          = 0xFFFFFFFF,
};

コレクションクラスは、グルーピング・フィルター・ソーターの各プロトコルに準拠したインスタンスを利用するので、標準以外の処理を行わせたい場合はプロトコルに適合したクラスを用意すればいい(もちろん標準提供のこれらのクラスをサブクラス化しても良い)。


その他

グループやアセットの変更通知

グループやアセットが追加されたり、変更や削除された場合、LKAssetsLibraryはそれを検出して内部で持っているグループをそれに合わせて更新する。アセットの増減があった場合はコレクションにも変更が自動的に反映される。

また処理後に独自の通知も出す。
// Notifications (Update)
// store updated group into userInfo[LKAssetsLibraryGroupsKey]
extern NSString * const LKAssetsLibraryDidInsertGroupsNotification;
extern NSString * const LKAssetsLibraryDidUpdateGroupsNotification;
extern NSString * const LKAssetsLibraryDidDeleteGroupsNotification;

// Notifications (keys)
extern NSString * const LKAssetsLibraryGroupsKey;

NSNotificationの userInfoから LKAssetsLibraryGroupsKeyをキーにして追加・変更のあった(あるいは削除された)LKAssetsGroupインスタンスを取得できる。

- (void)_assetsLibraryDidInsertGroup:(NSNotification*)notification
{
    NSArray* groups = notification.userInfo[LKAssetsLibraryGroupsKey];
    NSLog(@"%s|inserted: %@", __PRETTY_FUNCTION__, groups);
}

iCloud上の写真

iCloud上で作成した共有アルバムの写真は、サムネイルは入手できるものの実体画像は取得できない場合がある。これは明示的にダウンロードする必要がある為。標準の写真アプリで閲覧(ダウンロード)してある場合はそのキャッシュが使える(表示できる)。最初のバージョンではこのケースに対応できていない(デモプログラムでもその場合、サムネイルは表示されても詳細が表示できない)。フォトストリームは自動的にダウンロードされるので大丈夫なようだ。



デモプログラム


付属のデモプログラムを実行すると最初にグループの一覧が表示される。
これは LKAssetsLibrary.assetsGroup(NSArray)を並べたもの。

グループを1つ選んでタップすると中の写真が一覧される。
グルーピング(ALL | Year | Month | Week | Day | Hour)、並び順(Ascending | Descending)、種別(All | JPEG | Screen | Video)が選べて、その場で表示が変わる。ここで先ほど説明した LKAssetsCollection を使っている。

なおビデオはサムネイルは表示されるが、詳細では表示されない(ビデオ再生を実装していないだけ)。


その他


グルーピングの Weeklyの処理はいまいちイケていないので少し遅い。大量の写真がある場合、デモでも他のグループに比べて一瞬遅れるのが分かるかもしれない。


インストール


CocoaPodsを使う。

$ pod install LKAssetsLibrary

今までプラベートなリポジトリしか使っていなかったので CocoaPodsの公式へ登録するのは初めて。先日公開された Trunkを早速使った。この辺りは下記のブログが参考になった。

CocoaPods Trunkを利用したライブラリの追加方法


ライセンス


MIT です。基本好きに使って下さい。


- - - -
ようやくモデルができた。次は選択画面を作る。これは複数選択可能なイメージピッカーみたいなやつになる予定。

【Library】Task Completion ライブラリ

2014年4月29日火曜日 | Published in | 0 コメント

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

Task Completionが数行で使える小さなライブラリ。



Task Completion や実装については下記を参考のこと。


iOS7からは10分→3分と短くなっている。本格的な処理はiOS7から導入されたバックグラウンド送受信を使い、こちらはそこまで大げさではないちょっとした処理の延長で使う位置づけになったのだと思う。

使い方は簡単で基本1行のみ。AppDelegateの中で setupメソッドを1回呼ぶ。
#import "LKTaskCompletion.h"

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [LKTaskCompletion.sharedInstance setup];
    :
}

バックグラウンドのタスクが終了した時はできれば下記を呼んでおく。
[LKTaskCompletion.sharedInstance endBackgroundTask];

バックグラウンド処理を走らせたくないときは enabledプロパティを NOにしておく。
LKTaskCompletion.sharedInstance.enabled = NO;


サンプルプログラムがついていて、実行すると1秒単位で1行を消しこむ処理が走る。

残件がバッジに表示されていて、ホーム画面へ戻っても処理がバックグラウンドで行われているのがわかるようになっている。


CocoaPods対応 & MITライセンス





位置情報取得のライブラリ 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];



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

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対応が若干面倒なのでこの辺りの自動化がもう少しできると良さそう。


LKUserDefaults 公開

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

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

前回(Cocoaの日々: NSUserDefaults の値をプロパティアクセスできるようにする)のコードをライブラリ化して公開した。



使い方は、まずサブクラスを作りプロパティを書く。
#import "LKUserDefaults.h"

@interface UserDefaults : LKUserDefaults
@property (strong, nonatomic) NSString* name;
@property (strong, nonatomic) NSString* email;
@end

後はプロパティへアクセスするだけで自動的に NSUserDefaultsへの読み書きが行われる。
UserDefaults* defaults = [UserDefaults sharedInstance];
defaults.name = @"Hoge";        // キー "name" で NSUserDefaults へ書き込み
    :
NSString* name = defaults.name; // キー "name" の値を NSUserDefaults から読み出し

初期値を設定しておくこともできる。
- (void)registerDefaults
{
    self.name = @"unkown";
    self.name = @"unkown@xcatsan.com";
}

上記を定義しておくと、未設定の値を読みだした時にこの値が使われる。

定義可能なプロパティの型は NSUserDefaults と同じ。
@property (assign, nonatomic) BOOL boolValue;
@property (assign, nonatomic) NSInteger integerValue;
@property (assign, nonatomic) float floatValue;
@property (assign, nonatomic) double doubleValue;

@property (strong, nonatomic) NSString* stringValue;
@property (strong, nonatomic) NSArray* arrayValue;
@property (strong, nonatomic) NSArray* stringArrayValue;
@property (strong, nonatomic) NSDictionary* dictionaryValue;
@property (strong, nonatomic) NSData* dataValue;
@property (strong, nonatomic) NSURL* urlValue;
@property (strong, nonatomic) NSDate* dateValue;


オブジェクト型のプロパティの定義では ARC属性を strongにしておく。これは上記 registerDefaults の値を保持しておくため。

- - - -
実装のポイントは前回解説のように NSProxy をかましてプロパティへのアクセスをフックしているところにある。

前回記事:Cocoaの日々: NSUserDefaults の値をプロパティアクセスできるようにする

ライブラリでは汎用性のある部分を抽出して LKKeyValuStore と LKPropertyHook というクラスを作り、それを LKUserDefaults と LKUserDefaultsProxy が継承するようにしている。この為、若干読みづらいコードになっているかもしれない。今回は用意していないが NSUbiquitosKeyValueStore 用のクラスを用意すればiCloudの値も同じように使うことができる(たぶん)。


ハマったところとしては NSURL の格納部分。NSURL自体は NSuserDefaults のファイル形式である Property List には入れられない型だが NSUserDefaults では専用のメソッド(setURL:forKey:, URLforKey:)を用意していて、内部的に NSDataに変換して Property List へ入れている。この為、プロパティアクセスをフックしている今回の場合はプロパティの型情報を見てNSURLの時だけ適宜 NSDataへ変換してやる必要がある。この型判定がくせものでランタイム時に提供される情報(NSMethoSignatureなど)では引数/戻り値のクラスを知る方法を見つけることができなかった(オブジェクト=idであることしかわからなかった)。setterの場合は引数の型でそれが判断できるが getterの場合 Property List から取り出した NSData から判断するしかない。この為今回は NSUserDefaults から取得したオブジェクトが NSData の場合は無理やり URLforKey: を使って変換を掛けて、変換に成功したら NSURL として返し、失敗したら例外をキャッチして NSData のまま返すようにしている。ちょっとむりくりだけど。

なお形態としてはシングルトンになるが、複数クラスを定義したり、それらを継承関係にしたりすることができるようにしてある。ただしアクセス対象の NSuserDefaults は一つだけ(standardUserDefaults)。


課題・エンハンス案など
・現在はプロパティの先頭が小文字で始まることを前提にしている
・プロパティ名を途中で変更した場合、旧名の値が残り続ける(もしくは引き継げない)
・クラス定義毎に NSuserDefaults を作ることができるようにする
・擬似的な名前空間を作れるようにする(例えば VolumeSetting.name のようなキーで登録できるようにする)

何かあれば pull request をどうぞ。



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