【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 です。基本好きに使って下さい。


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

Evernote投稿アプリStackOneの無料版が出ました(広告なし!)

2014年5月21日水曜日 | Published in | 0 コメント

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

StackOneの無料版が出ました。

広告はなしの完全無料です。
有料版との違い
・ルールの追加ができません
・フォント種類/サイズ設定ができません
・投稿画面のカラー設定ができません

それ以外は有料版とまったく同じ。「サクサク投稿」はもちろん、約40種類の予約語が使えるヘッダ・フッタのカスタマイズ、1日分の投稿まとめなどStackOneならではの機能が健在です。

有料版紹介ページ


有料版レビューより
「動作が可愛い」
「衝撃がデカイです」
「手放せません」
「サクサクです」
「ホームに置いて活躍しています」
「使いやすい」
「素晴らしいアップデート」
「最高です」
「日記を残すのが楽しくなるアプリ」


新感覚のエバーノート投稿アプリを是非お試し下さい。
(サクサクですよ)

Static cell を隠す

2014年5月13日火曜日 | Published in | 0 コメント

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

Storyboardで作った Staticな Cellを実行時に消には UITableViewDelegateの2つのメソッドを使う。

例)セクション[2]を隠す

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath.section == 2) {    // セクション[2]内おセルをすべて非表示
        cell.hidden = YES;
    }
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section == 2) {    // セクション[2]内のセルの高さをすべて0に
        return 0;
    } else {
        return [super tableView:tableView heightForRowAtIndexPath:indexPath];
    }
}


高さを0にするだけだと下のセルに重なって表示されてしまうので合わせてhiddenにもしておく。これはクリッピングすれば不要かもしれない。


追記)セクションヘッダがある場合はこれも高さ0にしてやる。
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
    if (section == 2) {
        return 0;
    } else {
        return [super tableView:tableView heightForHeaderInSection:section];
    }
}

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