#Objective-C唯一のiOSファイルパスライブラリ、YKFileとは
まず唯一かどうかは定かではない。
もしかしたら嘘をついていたら本当にすまない。
しかし自分が探す限り、現在Cocoapods内では他に同等なライブラリは見つからなかった。
YKFileとはYKFileという名のクラスのインスタンス1つが、iOSアプリが生成するファイル(Documents以下など)の1つをピックアップするためのライブラリである。
https://github.com/GeneralD/YKFile
早速使い方を説明する。
#インストール
pod "YKFile"
をPodfileに記述しインストールするのが最も手っ取り早い。
Cocoapodsの基本的な使い方については割愛したいのでQiita内の他の方の記事のリンクを貼っておく。
http://qiita.com/yaakaito@github/items/66457a0d5fe55877dea1
#使用法
##基本編
まずYKFileクラスはユーティリティ(クラスメソッド中心)ではなく基本的にはオブジェクトとして利用するためインスタンスを生成するところから始める。
その前にまずYKFileが持つプロパティを紹介しよう。
#pragma mark - Properties About File Name
@property(nonatomic, copy) NSString *fullPath;
@property(nonatomic, strong, readonly) NSString *name;
@property(nonatomic, strong, readonly) NSString *extension;
@property(nonatomic, strong, readonly) NSString *parentName;
@property(nonatomic, strong, readonly) NSArray *pathComponents;
#pragma mark - Properties About File State
@property(nonatomic, assign, readonly) BOOL exists;
@property(nonatomic, assign, readonly) BOOL isFile;
@property(nonatomic, assign, readonly) BOOL isEmptyDirectory;
@property(nonatomic, assign, readonly) BOOL isDirectory;
@property(nonatomic, assign, readonly) BOOL isHidden;
@property(nonatomic, assign, readonly) NSInteger depth;
これらプロパティの中で読み書きともに可能なのはfullPathのみである。
他のプロパティはそのfullPathが指すパスに対してその状態を取得できるものである。
従ってインスタンスの生成はnew(alloc] init])してからfullPathを代入しても良いのだが次のような便利な初期化メソッドが定義されているのでそちらを利用して欲しい。
#pragma mark - Create New Instance
+ (instancetype)fileWithPath:(NSString *)fullPath;
- (instancetype)initWithPath:(NSString *)fullPath;
- (instancetype)fileWithAppending:(NSString *)path;
- (YKFileArray *)filesWithAppending:(NSArray *)paths;
- (instancetype)parentFile;
上から2つは説明しなくともObjective-Cの一般的な命名規則をわきまえた方なら普通に仕様は理解できると思う。
3つ目のメソッドは既に作成されたYKFileのfullPathがディレクトリを指しているときにファイル名を付け足す形で新しいインスタンスを生成する。4つめのほうはYKFileのNSArrayで複数受け取り3つ目のメソッドで行ったことを複数同時に行えるというものだ。(つまり生成されるYKFileも複数のためArrayとして受け取る)
ここで出てくるYKFileArrayとはヘッダの冒頭でこう定義されている。
typedef NSMutableArray YKFileArray;
ライブラリ内でNSMutableArrayと記述するよりもYKFileの配列であることを明示的にするためだ。(可読性のため)
ちなみにMutableであるのは単に利便性のためである。
ちなみにインスタンスの生成には他にもこんな便利なものがカテゴリで宣言されている。
+ (instancetype)homeDirectory;
+ (instancetype)mainBundleDirectory;
+ (instancetype)cachesDirectory;
+ (instancetype)documentsDirectory;
+ (instancetype)temporaryDirectory;
これらは上から順に
- .appファイルやDocuments, Library, tmpなどが配置されるホームディレクトリ
- メインバンドルディレクトリ?どこだろ
- Library/Caches
- Documents
- tmp
をfullPathが指すインスタンスが生成される。
残りのプロパティについても簡単に説明しておく。
もう一度ヘッダを抜粋すると
@property(nonatomic, strong, readonly) NSString *name;
@property(nonatomic, strong, readonly) NSString *extension;
@property(nonatomic, strong, readonly) NSString *parentName;
@property(nonatomic, strong, readonly) NSArray *pathComponents;
@property(nonatomic, assign, readonly) BOOL exists;
@property(nonatomic, assign, readonly) BOOL isFile;
@property(nonatomic, assign, readonly) BOOL isEmptyDirectory;
@property(nonatomic, assign, readonly) BOOL isDirectory;
@property(nonatomic, assign, readonly) BOOL isHidden;
@property(nonatomic, assign, readonly) NSInteger depth;
上から順に、
- パスを除いた純粋なファイル名
- 拡張子
- 親ディレクトリ
- パスをスラッシュで切り分けたものの配列
- ファイルかディレクトリとして存在するパスならばYES
- ファイルとして存在していればYES
- そのディレクトリが存在し、以下にファイルやディレクトリなどが存在していなければYES
- ディレクトリとして存在していればYES
- ドットで始まるファイル名ならYES
- パスが示すファイル、ディレクトリの深さ(Documents/a/bならば3)
インスタンスが指し示しているパスを変更する
インスタンスが指し示しているパスを変更する、というのは実際のファイル名、ディレクトリ名を改名するという意味ではなく、単にfullPathの文字列を変更するということだが、方法は主に2つある。
まずはfullPathに新しいNSStringを直接代入することだ。
しかしそれをするならば新しいインスタンスを作ってもいいし、そもそもこのライブラリの利便性を生かさないことになる。
#pragma mark - 'cd' Method
- (void)changeDirectory:(NSString *)path;
このようなメソッドが定義されている。
このメソッドは定義されているメソッドの中では唯一fullPathを書き換える。
これによりUNIX系OSで言う所のcd(changeDirectory)のようなことができる。
YKFile *file = [YKFile documentsDirectory];
[file changeDirectory:@"Assets"];
のように使う。
YKFile *file = [YKFile documentsDirectory];
[file changeDirectory:@"Assets/dir1/file1.text"];
のような使い方もアリだ。
これは別にまだ存在していないディレクトリにもchangeDirectoryできるということだ。
そういう意味では「UNIX系OSで言う所のcd」とは全く意味が違うが・・・
おまけに上の例ではファイル名に対してchangeDirectoryしている。
所詮fullPathは単なる文字列で、それを現在のfullPathの文字列と与えられる引数によって書き換えるメソッドである。
もちろん存在するかわからないファイルやディレクトリをchangeDirectoryした後、existsやisFileなどで状態を確かめたりなどできる。
UNIXっぽく
それだけではない。
まだ存在しないパスを指定できるのは以下のようなメソッドを有効活用して戴くためである。
#pragma mark - Control File System
- (BOOL)makeDirectory;
- (BOOL)makeDirectories;
- (BOOL)makeDirectory:(NSString *)dirName;
- (BOOL)makeDirectories:(NSString *)dirName;
- (BOOL)remove;
- (BOOL)remove:(NSString *)name;
- (BOOL)copyTo:(YKFile *)destination;
- (BOOL)moveTo:(YKFile *)destination;
上から4つのメソッドはディレクトリの存在確認を行い、存在しなければディレクトリを生成する。
2番目と4番目の複数形になっているメソッドはパスの終端のディレクトリ名の親ディレクトリがまだ存在しなければ再帰的にディレクトリを作成する。
単数形のは存在しないディレクトリの下にディレクトリは無理に作成しない。
3,4番目のは引数を取る。
YKFile *file = [YKFile documentsDirectory];
[file changeDirectory:@"Assets"];
[file makeDirectory];
と
YKFile *file = [YKFile documentsDirectory];
[file makeDirectory:@"Assets"];
は同じようにDocuments/Assetsをディレクトリを作成するが、前者は最終的にfullPathがDocuments/Assetsになるのに対し後者はDocumentsのままである。
ここまで説明すれば残りのメソッドもあまり詳細に説明せずとも分かってもらえるだろう。
removeはファイル、ディレクトリの削除(再帰的)、copyToは指定したパスへの(実際のファイル、ディレクトリの)コピー、moveToは指定したパスへの(実際のファイル、ディレクトリの)移動である。
ところで実際に開発していると現在のディレクトリ構造がどうなっているかファインダーで速やかに確認できると便利だろう。
SimPholdersというアプリが便利だ。
http://simpholders.com
必須アイテムなので持っていない方はダウンロードすることをお勧めする。
changeDirectoryの便利な機能
「UNIXっぽく」と言っているだけあって「.」や「..」や「~」も使える。とは言っても「.」は現在のディレクトリだから何もせず消化され、「..」は親ディレクトリを指すのでスラッシュで区切られた一つ分が末端から消えることになり、「~」はホームディレクトリを指す。
ちなみに「*」は特定のディレクトリ内をリストした時に最初にヒットしたものにchangeDirectoryする。
これが非常に便利で、大人の事情で、あるディレクリ内にファイルやディレクトリが一つしかないと決まっている場面というのは多々ある。
そんな構造が続くときディレクトリ内をブロック構造でリストしまくったりするとどんどんインデントが深くなってごちゃごちゃしたコードになったりする。
例えばDocuments/abc以下にはdefというディレクトリが含まれ、その下には01というディレクトリしかなく更にその下にはtext.txtというファイルしかない場合は
YKFile *file = [YKFile documentsDirectory];
[file cd: @"abc/def/*/*"];
でそのファイルを指すことができる。
ちなみにcdは後述するが、changeDirectoryのエイリアスメソッドだ。
普通に実機で利用されるアプリでは、そのディレクトリに意図して作成されたファイルしか存在しないつもりでも、先程紹介したSimPholdersを利用しファインダーでエミュレーター内のディレクトリをファインダーで覗いた場合、Macによって.DS_Storeが作成されることがある。(おそらくファイルのサムネイルなどのキャッシュとして使われていると思う。)
それが「最初にヒットするファイル」だと開発上不便なので.DS_Storeと念のためウィンドウズでそれに当たるThumbs.dbというファイルは無視される仕様となっている。
一応コードを見た方がしっくりくるという方も多いと思うし載せておく。
#pragma mark - 'cd' Method
- (void)changeDirectory:(NSString *)path {
NSArray *components = path.pathComponents;
for (int i = 0; i < [components count]; i++) {
NSString *component = components[i];
if (i == 0 && [component isEqualToString:@"~"]) { // cd ~
_fullPath = NSHomeDirectory();
} else if ([component isEqualToString:@".."]) { // cd ..
_fullPath = self.parentFile.fullPath;
} else if ([component isEqualToString:@"."]) { // cd .
} else if ([component isEqualToString:@"*"]) { // cd to first found file or directory
YKFileArray *list = [self listFiles];
for (YKFile *file in list) {
if ([file.name isEqual:@".DS_Store"]) continue;
if ([file.name isEqual:@"Thumbs.db"]) continue;
_fullPath = file.fullPath;
break;
}
} else { // cd component
_fullPath = [_fullPath stringByAppendingPathComponent:component];
}
}
}
ファイルリスト
#pragma mark - List Files
- (NSArray *)list;
- (YKFileArray *)listFiles;
- (YKFileArray *)listRecursively:(BOOL)recursively files:(BOOL)includeFiles directories:(BOOL)includeDirs;
でインスタンスが指すディレクトリ以下のファイル構造をYKFileインスタンスにすることができる。
1番目のは存在するファイル、ディレクトリ名の一覧をパスでなく名前のみで配列で返す。
2番目のは存在するファイル、ディレクトリ名のパスをfullPathにもつYKFileインスタンスの配列を返す
3番目のは最も便利と自負するメソッドで、再帰的に探索しファイルリストを返す。引数でファイル、ディレクトリそれぞれに対し含めたくないものを指定できる。(両方NOにしないでネ〜)
応用編
パスの抜粋
パスの一部を抜粋するメソッドも揃っている。
なんの役に立つの?と思う方は読み飛ばして頂いて構わないが、開発中に必要な場面があったらこの記事を思い出して欲しい。
#pragma mark - Utilities About File Path
- (NSString *)cropPathFromDepth:(NSInteger)from toDepth:(NSInteger)to;
- (NSString *)cropPathFromDepth:(NSInteger)from;
- (NSString *)cropPathToDepth:(NSInteger)to;
- (NSString *)cropPathFromTail:(NSInteger)numOfComponents;
- (NSString *)pathComponentFromTail:(NSInteger)whatTh;
上から順に説明する。
まず前提として「深さ」とはパスの深さのことを指し、abc/cde/fghというパスにおいてabcは深さ0であり、fghは深さ2である。
- 指定された深さの範囲を抜粋(1,3 -> cde/fgh)
- 指定された深さから深い部分を抜粋 (2 -> cde,fgh)
- 指定された深さまで抜粋 (2 -> abc/def)
- パスの終端から指定された数だけ抜粋 (1 -> fgh)
- パスの終端から指定された数番目を抜粋 (1 -> cde)
パスの比較
#pragma mark - Comparator
- (NSComparisonResult)compare:(YKFile *)file pathFromDepth:(NSInteger)from toDepth:(NSInteger)to;
- (NSComparisonResult)compare:(YKFile *)file pathFromDepth:(NSInteger)from;
- (NSComparisonResult)compare:(YKFile *)file pathToDepth:(NSInteger)to;
- (NSComparisonResult)compare:(YKFile *)file pathFromTail:(int)numOfComponents;
- (NSComparisonResult)compareWithName:(YKFile *)file;
先程の抜粋メソッドと比較しながらメソッド名を見て考えてもらえれば分かるが、これらはオブジェクトをソートするときなどに用いるNSComparisonResultを結果として返す。
エイリアス
折角なので「UNIXっぽく」書くためのエイリアスメソッドも取り揃えてある。
#pragma mark - Aliases
- (void)cd:(NSString *)path;
- (void)cd;
- (BOOL)mkdir:(NSString *)dirName;
- (BOOL)mkdirs:(NSString *)dirName;
- (BOOL)mkdir;
- (BOOL)mkdirs;
- (BOOL)rm:(NSString *)name;
- (BOOL)rm;
- (BOOL)cp:(YKFile *)destination;
- (BOOL)mv:(YKFile *)destination;
おそらく説明は要らないだろう。
YKFileオブジェクトについては・・・
他にも少し幾つかメソッドがあるが、あとはソースを見てもらおう。
便利なNSMutableArray+YKFileArrayカテゴリ
前述した通り、YKFileArrayとはNSMutableArrayのこと、ここでNSMutableArrayがYKFileの配列だった場合に便利なメソッドを提供するわけだが、そもそもなんでYKFileArrayをNSMutableArrayのサブクラスにしなかったの?という質問は愚問である。
NSMutableArrayのサブクラスは作れない。
なので仕方なくカテゴリとして拡張してある。
- (void)eachFile:(void (^)(YKFile *file, NSUInteger idx))pFunction;
- (void)sortFiles:(NSComparisonResult (^)(YKFile *file1, YKFile *file2))pFunction;
- (void)removeFilesFromList:(BOOL (^)(YKFile *file))pFunction;
- (YKFileArray *)cutFilesFromList:(BOOL (^)(YKFile *file))pFunction;
またしても上から順番に説明する。
- 配列内YKFileをfor eachのようなことをする。ブロック記法
- 配列内YKFileをブロック記法でソートする
- 配列内YKFileからブロック記法で要らないものを減らす
- 配列内YKFileをブロック記法で仕分けする。元のインスタンスから取り除かれたYKFileは戻り値でリターンされるYKFileArrayに含まれる。
具体的な導入方法
ここまで説明すればこれ以上を語らずとも良い気がするが、例えば対象のパスからUIImageを生成するなら
[UIImage imageWithContentsOfFile:file.fullPath];
のようにする。
時期バージョンあたりで利便性のためimageプロパティでも作ろうかと検討中である。
ちなみに対象ファイルに書き込まれているテキストはtextというプロパティを用意しておいた。簡単にNSStringで取得できる。
現在のバージョンでは読み込み(readonly)だけだが、書き込み(readwrite)にも対応しようかと思う。
他にもurlというプロパティがあるのでそちらを使えばNSURLでパスを表せる。
将来的にはライブラリのバージョンアップで、基本的なAPI(NSString、UIImage、NSURLなど)への変換などをスマートに記述できるようなYKFileのカテゴリを作りreadwriteなプロパティを取り揃えたいと思っている。
もし当ライブラリを活用してくれる方で上記のようなYKFileの便利なカテゴリを作った方がいたら是非マージさせて欲しい。
よろしくお願いしまーす。
〆
以上ですが、YKFileにこんな機能があったらいいのに、とかYKFile使ってこんなアプリ作りましたとか、作り中とか、そういう報告を頂ければ作者は嬉しいです。