黒毛和牛モモバラ切り落し100g298円

iPhoneアプリを作ってます。リリースノートとか用ブログです。

NSCachesDirectory と NSTemporaryDirectory に関するメモ

忘れそうなのでメモ。

それぞれがシステムから消されるタイミングは?

Apple のドキュメント File System Basics には、Caches ディレクトリは very low on disk space の時にシステムが消すことがあると書いてある。アプリが実行中には発生しないようなのでバックグラウンド起動中は発生するっぽい?

In iOS 5.0 and later, the system may delete the Caches directory on rare occasions when the system is very low on disk space. This will never occur while an app is running. However, be aware that restoring from backup is not necessarily the only condition under which the Caches directory can be erased.

同ドキュメントに Temporary ディレクトリの削除についても書いてあって、アプリが実行していない間に purge されると書いてある。

Use this directory to write temporary files that do not need to persist between launches of your app. Your app should remove files from this directory when they are no longer needed; however, the system may purge this directory when your app is not running.

Qiita とか stackoverflow だと、Caches はアプリ実行中に消されることがあって、Temporary は実行中に消されることは無いと書いてあることが多いんだけど、Apple のドキュメントを見る限り never occur while an app is running と when your app is not running なんで少なくともフォアグラウンド状態なら消されることは無いんじゃないですかね?

NSCachesDirectory ディレクトリのサブディレクト

Apple のドキュメント Accessing Files and Directories には、NSCachesDirectory の下に bundle_ID ディレクトリを作ってそこにファイル置いた方が良いと書かれている。

Use the Caches directory constant NSCachesDirectory, appending your <bundle_ID> directory for cached data files or any files that your app can re-create easily.

まったくもってその通りなんだけど、その辺適当にやってくれる便利メソッドあっても良いんじゃないですかねーという気が。ちなみに自分のアプリはこれやってたりやってなかったりで、ちょうどいじくってたアプリがやってなかったので OMG な感じでした。

なお bundle_ID は [[NSBundle mainBundle] bundleIdentifier] で取れる模様。

最後に全く関係ないですが、はてなブログMarkdown 使いにくいです。

追記

NSCachesDirectory の下に [[NSBundle mainBundle] bundleIdentifier] を作ると良いらしいと書いたが、実際のアプリには既にそのフォルダが存在している。何に使ってるか不明だが、そこには置かない方が良いようだ。

ブログのコメントの返信をしました

ブログに頂いたコメントについて、長い間返信していなかったのですが先ほどお返事させていただきました。
一応コメントの方は見ていましたのでラノベルに頂いたご要望はだいたい把握しておりましたが、あまり修正に割ける時間が無いもので「すぐできません」とお返事するのもアレな感じでスルーしておりました。どうも申し訳有りません。

返信したということは時間ができたのか?という話になると思いますが、そういう訳でも無かったりします。
ただiOS10対応とかそんなにすること無いやろうと思って放置してたら、今までに無いレベルでバグが出たので慌てて直したりしたのもあまりよろしく無いなーという感じでして、あと最近iPad買ったのでなろう見ようと思ったらいまいち使いにくいのでその辺なんとかしたいなーと思ってます。

そんな感じですのでアプリの更新ペースをがっつり上げれる感じでは無いのですが、ぼちぼちやっていきたいなと思ってますので気長に見守っていただければと思います。

ラノベル-1.1.17をリリースしました

ラノベルのバグ修正版をリリースしました。以下のバグを修正しています。

  1. 一部の小説を開くとクラッシュするバグの修正
  2. 小説詳細画面の位置ずれの修正

ラノベル-1.1.16をリリースしました

ラノベル-1.1.16をリリースしました。
iOS10で一覧画面が消えるバグの対応をしています。
なのですが、ちょいちょいクラッシュしているようなので今直しています。
手元の環境で再現できていないのですが、一覧でリロードしてる間に小説を開いて、(話数が減るなどで)消えた話にアクセスすると落ちるはずです。
申し訳有りませんが、安定するまで少々お待ち下さい。

ラノベル-1.1.15をリリースしました

先ほどラノベルの新バージョンをリリースしました。主にバグ修正です。
合わせて利用規約を明示するように審査で指摘されましたので、アプリ初回起動時(バージョンアップ後も含む)に規約が出るようになっています。同意を押すと普段の画面に戻り以降は表示されません。(規約改定する際にまた表示されると思います。)
また、どの時点になるか未定ですがiOS7の対応はもうじきやめようと思っております。

あとコメントの方あまりお返事できておらず申し訳ないです。
割と多くご依頼を頂いておりますカクヨム対応については今のところ予定しておりません。個人的には対応したいのですが、なろうと互換性のないサイトを対応するのはちょっと難しい現状です。
ファイル共有とバックアップ機能については検討中です。iPhone6sに変えた際に無いと不便だなとしみじみと思いましたが、喉元過ぎれば熱さ忘れるで全然作業が捗っておりません。
諸々申し訳ございませんが、よろしくお願い致します。

Xcode7 で NSFetchedResultsControllerDelegate がクラッシュする場合の対応方法

Xcode7-beta の頃から NSFetchedResultsControllerDelegate で落ちるようになりました。現在の(betaではない)Xcode7 でも同様にクラッシュするようです。私のアプリも一時期結構落ちてましてユーザーさんにはご迷惑をおかけしておりました。

当初 iOS9 固有の問題かと思っていたのですが iOS8 でも発生しているようで、iOS 起因の問題なのか Xcode 起因の問題なのかちゃんと把握しておりませんが、条件さえ揃えば確実にクラッシュしますので結構クリティカルな問題と認識しています。

いくつかのクラッシュする状態があるようですが、NSFetchedResultsChangeUpdate 時に [UITableView reloadRowsAtIndexPaths:withRowAnimation:] を呼び出すと落ちるようになったのが私の方では一番のクラッシュ原因でした。話の流れは Apple の forum や stackoverflow が詳しいのでそちらをご覧ください。

なお私が問題に遭遇した時点では、Apple の forum や stackoverflow にあった解決方法ではクラッシュが解決しなかったので、以下のようなコードで対応しました。

まず FetchedResultsControllerDelegate というクラスを用意しています。Xcode の新規プロジェクトで master/detail 型のプロジェクトを作成すると [configureCell:atIndexPath:] が UITableViewDataSource に追加されますが、私のアプリは master/detail 型として作成したものではありませんし NSFetchedResultsControllerDelegate は同じクラスを使い回していますので別オブジェクトにしてしまった方が便利かと思います。(Swift の場合は適当に変換してください。)

FetchedResultsControllerDelegate.h

#import <CoreData/CoreData.h>

@protocol FetchedResultsControllerDelegateDataSource <UITableViewDataSource>

@required
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath;

@end

@interface FetchedResultsControllerDelegate : NSObject <NSFetchedResultsControllerDelegate>

@property (weak, nonatomic) UITableView *tableView;

- (id)initWithTableView:(UITableView *)tableView;

@end

FetchedResultsControllerDelegate.m

#import "FetchedResultsControllerDelegate.h"

@implementation FetchedResultsControllerDelegate

- (id)initWithTableView:(UITableView *)tableView
{
    self = [super init];
    if (self) {
        self.tableView = tableView;
    }
    return self;
}

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
            
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeUpdate:
            break;
        case NSFetchedResultsChangeMove:
            break;
    }
}

- (BOOL)canUpdateWithTableView:(UITableView *)tableView indexPath:(NSIndexPath *)indexPath
{
    NSObject <UITableViewDataSource> *dataSource = tableView.dataSource;
    
    if (dataSource == nil || ![dataSource conformsToProtocol:@protocol(FetchedResultsControllerDelegateDataSource)]) {
        return NO;
        
    } else {
        NSInteger sections = 1;
        if ([dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {
            sections = [dataSource numberOfSectionsInTableView:tableView];
        }
        if (indexPath.section >= sections) {
            return NO;
        }
        NSInteger rows = 0;
        if ([dataSource respondsToSelector:@selector(tableView:numberOfRowsInSection:)]) {
            rows = [dataSource tableView:tableView numberOfRowsInSection:indexPath.section];
        }
        return (indexPath.row < rows);
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;
    
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
            
        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
            
        case NSFetchedResultsChangeUpdate:
            if ([self canUpdateWithTableView:tableView indexPath:indexPath]) {
                NSObject <FetchedResultsControllerDelegateDataSource> *dataSource = (NSObject <FetchedResultsControllerDelegateDataSource> *)tableView.dataSource;
                UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
                if (cell != nil) {
                    [dataSource configureCell:cell atIndexPath:indexPath];
                }
            }
            break;
            
        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView endUpdates];
}

@end

あくまでも参考として利用される場合は、NSFetchedResultsChangeUpdate の分岐で configureCell を呼び出せない場合の判定を忘れないようにしてください。うろ覚えですが Xcode6 の頃は [cellForRowAtIndexPath:] で取得できない位置のセルに対して NSFetchedResultsChangeUpdate が発生することは無かったように思うのですが、Xcode7 では発生するようですので、(未確認ですが)AppleNSFetchedResultsControllerDelegate の実装例でも落ちるような気がします。

上記クラスを使うには、まず UITableViewDataSource で以下のように NSFetchedResultsController の delegate に FetchedResultsControllerDelegate オブジェクトを指定し、

- (void)someMethod {
     FetchedResultsControllerDelegate *fetchedResultsControllerDelegate = [[FetchedResultsControllerDelegate alloc] initWithTableView:self.tableView];
     NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil];
 }

[configureCell:atIndexPath:] を定義し、[tableView:cellForRowAtIndexPath:] からもそれを呼び出すようにします。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ReuseIdentifier forIndexPath:indexPath];
     [self configureCell:cell atIndexPath:indexPath];
     return cell;
}

- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
    // セルの初期化処理
}

ラノベル-1.1.14をリリースしました

一括リロード時に落ちるバグの修正バージョンです。
バージョン1.1.10前後からのバグ修正はこれにて一区切りしたかと思います。
取り急ぎご連絡ということで、不具合等ございましたらご連絡ください。