【Objective-C】アプリ内保存した画像のパスがpathForResourceで取れない(解決編)

【追記】2014.9.13 3:00
この方法はあくまで「アプリ内保存した画像のパス」の話で、Xcode上で最初から登録している画像には当てはまりません。パスに「Documents」ディレクトリが含まれないためです。

【Xcode5.1.1 + iOS 7.1 + MacOX10.9.4】

前回の解決編です。

JBOYSOFT JunjiSuzukiさんに「NSSearchPathForDirectoriesInDomains を使うのもいいかも」との助言を受けて、描いてみたコードがこれ。NSSearchPathForDirectoriesInDomainsでディレクトリのパスを取ってきて、保存しているファイル名と組み合わせて、自前でパスを作っている。

    NSArray *paths = NSSearchPathForDirectoriesInDomains(
                                                         NSDocumentDirectory,
                                                         NSUserDomainMask, YES);
    
    NSArray *ph = [select_name componentsSeparatedByString:@"/"];
    NSString *fn = [ph objectAtIndex:ph.count-1];
    
    NSLog(@"paths >> %@",paths);
    NSLog(@"fn >> %@",fn);
    
    path = [NSString stringWithFormat:@"%@/%@",[paths objectAtIndex:0],fn];
    
    NSLog(@"path >> %@",path);

    iconImage = [[UIImage alloc] initWithContentsOfFile:path];

これだと既存のものも、起動後に保存したものも、どちらも問題なく参照出来る!NSLogによるトレースはこちら。

paths >> (
    "/var/mobile/Applications/BB4E2366-816D-427F-8F8B-FF836EB58AB8/Documents"
)
fn >> icon20140913003532.png
path >> /var/mobile/Applications/BB4E2366-816D-427F-8F8B-FF836EB58AB8/Documents/icon20140913003532.png

気になったので、前回描いたコードで取得されるパスを確認してみる。前回のコードはコレ。

    NSArray *phrases = [imageName componentsSeparatedByString:@".png"];
    NSString *filename = [phrases objectAtIndex:0];
    NSString *path= [[NSBundle mainBundle] pathForResource:filename ofType:@"png"];
    
    //アプリ内保存した直後の画像は、path が nullになる
    NSLog(@"path >> %@",path);
    
    //保存直後の画像はNSBundleでパスが取れないための対処(アプリが再起動しないとダメみたい)
    UIImage *iconImage;
    
    if (path == NULL) {
        iconImage = [UIImage imageNamed:imageName];
        
    } else {
        iconImage = [[UIImage alloc] initWithContentsOfFile:path];
    }

※ imageNameには、”../Documents/icon20140912171442.png”のようなパスが入っている。

アプリ起動前に保存されている画像について、NSLogで表示されるパスを見ると・・・

path >> /var/mobile/Applications/BB4E2366-816D-427F-8F8B-FF836EB58AB8/xxxxxx.app/../Documents/icon20140913003017.png

と、アプリ名(xxxxxx.app/../)がはさまってる。この違いは何?NSBundleだとこうなるの?

更に、冒頭の修正コードをアプリ名(xxxxxx.app/../)がはさまってるパスに変更して参照してみる。

    NSArray *paths = NSSearchPathForDirectoriesInDomains(
                                                         NSDocumentDirectory,
                                                         NSUserDomainMask, YES);
    
    NSArray *ph = [select_name componentsSeparatedByString:@"/"];
    NSString *fn = [ph objectAtIndex:ph.count-1];
    
    NSLog(@"paths >> %@",paths);
    NSLog(@"fn >> %@",fn);
    
    NSArray *paths2 = [[paths objectAtIndex:0] componentsSeparatedByString:@"Documents"];
    path = [NSString stringWithFormat:@"%@iconFaceCam.app/%@",[paths2 objectAtIndex:0],select_name];
    
    NSLog(@"path >> %@",path);

    iconImage = [[UIImage alloc] initWithContentsOfFile:path];

NSLogの出力結果がこれ。

paths >> (
    "/var/mobile/Applications/BB4E2366-816D-427F-8F8B-FF836EB58AB8/Documents"
)
fn >> icon20140913012801.png
path >> /var/mobile/Applications/BB4E2366-816D-427F-8F8B-FF836EB58AB8/xxxxxx.app/../Documents/icon20140913012801.png

動作としては、こちらでも問題なく動きました。

で、結論としては、アプリ内保存のファイル参照では・・・

・NSSearchPathForDirectoriesInDomainsでドキュメントディレクトリを取得する。
・ドキュメントディレクトリ+ファイル名でパスを生成する。
・生成したパスを使って、initWithContentsOfFileで参照する。

ということになるんだけれど、そもそも何で・・・

[[NSBundle mainBundle] pathForResource:filename ofType:@”png”]

でパスが取得できないのか、がわからずモヤモヤします…。

とりあえずは余計なifとか要らなくなったので、よしとして先に進みます。
JBOYSOFT JunjiSuzukiさんに感謝。

【参考】
iOS でデータを永続化する方法 – A Day In The Life

【Objective-C】アプリ内保存した画像のパスがpathForResourceで取れない

【Xcode5.1.1 + iOS 7.1 + MacOX10.9.4】

以前、iOSアプリ内に画像を保存するという記事を書いて、その後、保存した画像の参照を[UIImage imageNamed]から、[[NSBundle mainBundle] pathForResource:]に変更したら、保存直後の画像参照ができなくなった。

で、いろいろテストしてみると、一度アプリを終了してからの画像は参照出来るので、NSBundleはアプリ起動時の情報しか持っていないのではないかと思われます。とりあえず動かすために、こんなコードを書いて回避してるんだけど、NSBundleを強制的に更新するとか、何かいい方法はないでしょうか?NSBundleを調べてみたけどよくわからなかったので。

    NSArray *phrases = [imageName componentsSeparatedByString:@".png"];
    NSString *filename = [phrases objectAtIndex:0];
    NSString *path= [[NSBundle mainBundle] pathForResource:filename ofType:@"png"];
    
    //アプリ内保存した直後の画像は、path が nullになる
    NSLog(@"path >> %@",path);
    
    //保存直後の画像はNSBundleでパスが取れないための対処(アプリが再起動しないとダメみたい)
    UIImage *iconImage;
    
    if (path == NULL) {
        iconImage = [UIImage imageNamed:imageName];
        
    } else {
        iconImage = [[UIImage alloc] initWithContentsOfFile:path];
    }

※ imageNameには、”../Documents/icon20140912171442.png”のようなパスが入っている。

【参考】
NSBundleクラス | Second Flush

【Objective-C】特定のUIGestureRecognizerを削除する

【Xcode5.1.1 + iOS 7.1 + MacOX10.9.4】

一時的にUIGestureRecognizerをセットして、不要になったら削除するという場合のメモ。例えば・・・

・UITapGestureRecognizerはデフォルトで常に設定
・ある条件で、UIPanGestureRecognizerを追加
・不要になったら、UIPanGestureRecognizerだけを削除

というような場合。

    for(UIGestureRecognizer *gesture in [selectIconView gestureRecognizers]) {
        NSLog(@" isClass UITapGestureRecognizer!! %d", [gesture isKindOfClass:[UITapGestureRecognizer class]]);
        NSLog(@" isClass UIPanGestureRecognizer!! %d", [gesture isKindOfClass:[UIPanGestureRecognizer class]]);
    }

※ selectIconViewは、UIGestureRecognizerを設定したUIImageView。

こんな感じで「isKindOfClass」で判定可能。

で、実際にやりたかったのは、こんな感じ。

    for(UIGestureRecognizer *gesture in [selectIconView gestureRecognizers]) {
        
        // UIPanGestureRecognizerのみ削除
        if([gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
            [selectIconView removeGestureRecognizer:gesture];
        }
    }

【参考】
aideal.jp UIGestureRecognizer
gesture recognizerを全削除する – 日々精進
UIGestureRecognizerクラス | Second Flush
Objective-C – UIViewからUIGestureRecognizerを削除 – Qiita
isKindOfClassとisMemberOfClassってなにが違うねん | S4U -smile for you-

【Objective-C】UITableViewCellに入れたUISwitchから親セルを参照

【Xcode5.1.1 + iOS 7.1 + MacOX10.9.4】

UITableViewCellをカスタマイズして、UISwitchを入れてみた。これはそんなに難しくない。こんな感じで。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
    
    // cellを選択不可にする
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    
    // UISwitchを作成
    UISwitch *sw = [[UISwitch alloc] initWithFrame:CGRectZero];
    
    // UISwitchのタップ動作を指定
    [sw addTarget:self action:@selector(tapSwich:) forControlEvents:UIControlEventTouchUpInside];
    
    // UISwitchをaccessoryViewに代入
    cell.accessoryView = sw;
    
    cell.textLabel.text = sw_name;
    
    return cell;
}

【参考】
[XCODE] UITableViewCellに、様々なコントロール要素をのせて表示する方法 – YoheiM.NET
UITableViewCell の accessoryView を使うと少し楽 (フェンリル | デベロッパーズブログ)

で、UISwitchをタップしたときに呼ばれるtapSwichで「何番目のセルか?」(indexPath.row)を取得しようとしてハマった。
最初は「UITableViewCellに入れたんだから、その親でしょ?」とこんな感じに書いたけど取得できず。

    UITableViewCell *cell = (UITableViewCell *)[sw superview];
    int row = (int)[self.tableView indexPathForCell:cell].row;

そこで階層を調べるために、こんなふうに書いてみた。

    NSObject *t = (UITableViewCell *)[sw superview];
    NSObject *tt = (UITableViewCell *)[[sw superview] superview];
    NSObject *ttt = (UITableViewCell *)[[[sw superview] superview] superview];
    NSObject *tttt = (UITableViewCell *)[[[[sw superview] superview] superview] superview];
    
    NSLog(@"--switch  >> %@ , \n%@ , \n%@, \n%@",t,tt,ttt,tttt);

その出力結果がコレ(見やすいように一部省略)

2014-09-02 01:50:00.953 iconFaceCam[3017:60b] --switch  >> 
<UITableViewCellScrollView: ...> , 
<UITableViewCell: ...> , 
<UITableViewWrapperView: ...>, 
<UITableView: ...>

UISwitchとUITableViewCellの間に、UITableViewCellScrollViewというのが入ってるらしい。
そこで、こんな感じに書いて参照出来ました。

-(void)tapSwich:(id)sender
{
    UISwitch *sw = (UISwitch *)sender;
    
    UITableViewCell *cell = (UITableViewCell *)[[sw superview] superview];
    int row = (int)[self.tableView indexPathForCell:cell].row;
    
    // UISwitch:初期値を指定
    switch (row) {
        case 0:
            .....;            
            break;
        case 1:
            .....
            break;
        case 2:
            break;
            
        default:
            break;
    }
}

調べてみたら、この階層はiOS7になって変更されているらしいので、iOS6ではまた違うらしい。
詳しくは以下のリンクへ。

iOS7 では UITableViewCell の subviews階層が変更されている | objc-Lovers

【Objective-C】日付を24時間表示で取得する際の注意点

【Xcode5.1.1 + iOS 7.1 + MacOX10.9.4】

日付を24時間表示で取得するには、LocaleをUSに設定しないと端末設定で結果が変わるらしい。

で、こんな感じで関数を書いてみた。

//現在の年月日時分秒を取得
-(NSString *)getCurrentTime
{
    NSDate *nowdate = [NSDate date];
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"yyyyMMddHHmmss"];
    
    //24時間表示にするためLocaleはUSにする
    [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"US"]];
    NSString *dateString = [formatter stringFromDate:nowdate];
    
    return dateString;
}

【参考】
NSDateで現在の日付を取得する | イリテク株式会社
NSDateFormatter – 文字列と日付の変換 – 強火で進め
日本語環境では、NSDateFormatterでフォーマットした日付がおかしい – 24/7 twenty-four seven

【Objective-C】ModalとNavigationの画面遷移方法の違い

【Xcode5.1.1 + iOS 7.1 + MacOX10.9.4】

Storyboardの画面リンク(Segue)を使わずに、コードで画面遷移を書いていてハマったのでメモ。

・ナビゲーション(Navigation/UITableViewControllerなどで使う、NavigationBarが出てくる画面遷移)

    //画面を遷移(※confVCは表示するViewCOntroller)
    [self.navigationController pushViewController:confVC animated:YES];

    //元画面に戻る
    [self.navigationController popViewControllerAnimated:YES];

・モーダル(Modal/現在のViewに重なるように表示する画面遷移)

    //画面を遷移(※confVCは表示するViewCOntroller)
    [self presentViewController:configurationVC animated:YES completion:nil];

    //元画面に戻る
    [self dismissViewControllerAnimated:YES completion:nil];

この2つをごっちゃにしていてハマりました。どのViewControllerでも、ナビゲーション(Navigation)の方法ならきちんとNavigationBarが表示されて戻るボタンが自動でつきます。

最初はUITableViewControllerだけNavigationBarが出なくて調べていたら、こんな基本的なことだたっという・・。

【参考】
iOS アプリの画面開発の基礎を理解する – A Day In The Life
iPhoneプログラミング入門「モーダルビュー1」
Objective-C – モーダルビューを出したり消したり – Qiita
yuki0n0 高校生アプリ開発者のブログ: storyboardで作った画面をコードで画面遷移

だんだん作り込んでくると、基本的な画面のレイアウト(固定部分)はStoryboardを使って、画面遷移やボタン配置など可変になる部分はコードで制御する方がいいかなと思っていて、Storyboardの画面リンク(Segue)を全部削除してコードで書き換えてます。

【Objective-C】NSStringを特定の文字列で分割して配列化

【Xcode5.1.1 + iOS 7.1 + MacOX10.9.4】

まあ、ActionScriptでいうところの、String.split(“/”)をやりたかったのですが、意外に探すのに手こずったのでメモ。

// ファイルパスを「/」で分割して、削除ファイル名を取り出す
NSString *test = @"../Documents/icon20140819002020.jpg";
NSArray *phrases = [test componentsSeparatedByString:@"/"];
NSString *filename = [phrases objectAtIndex:phrases.count-1];
NSLog(@" --delete file >> %@",filename);

で、結果がこちら。

--delete file >> icon20140819002020.jpg

参考:YoheiM.NET|[XCODE] NSStringを特定の文字で分割する方法

【Objective-C】UICollectionViewCellの選択表示

【Xcode5.1.1 + iOS 7.1 + MacOX10.9.4】

UICollectionViewCellで選択表示をさせようと思ってハマったのでメモ。

参考:Collection View プログラミングガイド(iOS用)【PDF】
※P25〜P28あたり。

選択状態をコードで制御するには・・・

たとえば、セルの選択状態をアプリケーション側で描画したい場合は、selectedBackgroundViewプロパティをnilにし、デリゲートオブジェクトに、外観を変更する処理を組み込んでください。
collectionView:didSelectItemAtIndexPath:メソッドに外観を変更する処理、
collectionView:didDeselectItemAtIndexPath:メソッドに元の状態に戻す処理を実装します。

ということらしいので、コードを書いてみた。

//UICollectionViewCell:編集
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell;
    
    cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell1" forIndexPath:indexPath];
    cell.backgroundColor = [UIColor whiteColor];
    cell.selectedBackgroundView = nil;
    
    //(中略)

    return cell;
}

//
-(void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"選択した %@",indexPath);
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
    cell.backgroundColor = [UIColor redColor];
}

-(void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"選択解除した %@",indexPath);
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
    cell.backgroundColor = [UIColor whiteColor];
}

これだとタップしている間は、背景色が赤に変わるけれど、指を離すと白に戻ってしまう。

で、更に資料を読んでみると・・・

図 2-3に、未選択状態のセルにユーザがタッチしたときに発生する、一連の動作を示します。最初の「タッチダウン」イベントを受けて、コレクションビューはセルの「強調表示」状態をYESにします。もっとも、自動的にセルの外観に反映されるわけではありません。その後、「タッチアップ」イベントが発生すると、「強調表示」状態はNOに戻り、「選択」状態はYESに変わります。ユーザが選択状態を変更すると、コレクションビューはセルのselectedBackgroundViewプロパティに設定されているビューを表示します。コレクションビューが関与してセルの外観を変えるのは、この場合に限ります。ほかの視覚効果はデリゲートオブジェクトが行わなければなりません。

つまり、指を離すと選択状態にはなるものの didUnhighlightItemAtIndexPathが呼ばれて、表示が戻ってしまう・・・ということらしい。

結局、didHighlightItemAtIndexPathとdidUnhighlightItemAtIndexPathは使わず、以下の様に書いて解決。

//UICollectionViewCell:編集
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell;
    
    cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell1" forIndexPath:indexPath];
    
    //通常の背景
    UIView* backgroundView = [[UIView alloc] initWithFrame:cell.bounds];
    cell.backgroundColor = [UIColor whiteColor];
    cell.backgroundView = backgroundView;
    
    //選択時の背景
    UIView* selectedBGView = [[UIView alloc] initWithFrame:cell.bounds];
    selectedBGView.backgroundColor = [UIColor redColor];
    cell.selectedBackgroundView = selectedBGView;
    
    //(中略)
    
    return cell;
}

最初のコードで、選択時のイベントハンドラ(didSelectItemAtIndexPath)で選択済表示に変えることもできるけど、それだと選択済項目の表示を戻してやる処理が必要になるので、単一選択ではこれが一番簡単かなと。

【Objective-C】UIBarButtonItemの右ボタンを動的に差し替える

【Xcode5.1.1 + iOS 7.1 + MacOX10.9.4】

カメラロールでサムネイル表示したときに、右上に出る「選択」>「キャンセル」の切替と同じやつです。これってViewは切り換えてないよなあと思ったので、ボタンの差し替えをテストしてみました。

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    //削除ボタンを追加
    UIBarButtonItem *delButton = [[UIBarButtonItem alloc]
                                  initWithBarButtonSystemItem:UIBarButtonSystemItemTrash
                                                                               target:self
                                                                               action:@selector(delButtonDidPush:)];
    

    self.navigationItem.rightBarButtonItem = delButton;
}

-(void)delButtonDidPush:(UIBarButtonItem *)button
{
    NSLog(@" -- trash!!");
    
    //
    UIBarButtonItem *canelButton = [[UIBarButtonItem alloc]
                                    initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
                                    target:self
                                    action:@selector(cancelButtonDidPush:)];
    self.navigationItem.rightBarButtonItem = canelButton;
}

-(void)cancelButtonDidPush:(UIBarButtonItem *)button
{
    NSLog(@" -- cancel!!");
    
    //削除ボタンを追加
    UIBarButtonItem *delButton = [[UIBarButtonItem alloc]
                                  initWithBarButtonSystemItem:UIBarButtonSystemItemTrash
                                  target:self
                                  action:@selector(delButtonDidPush:)];
    
    
    self.navigationItem.rightBarButtonItem = delButton;
    
}

これでちゃんと動いてるし、タップのイベントも片方しか出てこないので、たぶんいいはず。
UIToolBarについては、こんな記事が。

なんとなく|UINavigationController の UIToolBar で Button 表示を切り替えたいメモ

たぶんnavigationItem.rightBarButtonItemは1つしか置けないので、重なったりすることはないのでは?と推測。

【Xcode】ARC配下でのメモリ管理関連リンク

ARC配下でのメモリ管理関連リンク。あとで読む用。

Second Flush ARCへの移行のリリースノート
infinite loop 技術ブログ Objective-Cのメモリ管理おさらいと解放tips
私的メモ [Objective-C] メモリリークをチェックする
ARCを使用したメモリ管理の基礎
ARC環境でメモリが枯渇するケース
sirochro Objective-C: これで解決!ARCを使用しReceived memory warningが出た時の対処方法
Developers.IO [Xcode 5] Xcode 5のデバッグ機能 “Debug gauges”
Instruments ユーザガイド[PDF]
【iOS/Mac開発】超サクサクアプリへの必須ツール Instruments を使いこなそう

AS,Objective-C,Javascript,その他諸々の備忘録