【Objective-C】UIPageControlのページ切替タイミング

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

前回の投稿絡みで、自前でUIPageControlを追加したときに、ページ数表示更新のタイミングでハマったのでメモ。

要は・・・

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed;
{
  ...
}

このページ切替アニメーションの終了タイミングで更新するのが吉、ということ。

自前のスクリプト載せようと思ったら、要らなくなったんで消した後でした・・・。

【参考】
これからiOS(iPhone)アプリを開発する人々へ: UIPageViewControllerについて

【Objective-C】UIPageViewControllerでUIPageControlが表示されない

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

UIPageViewControllerを使った時に、下記の条件を満たせば自動的にUIPageControl(iPhoneのホーム画面にもある、現在位置を示す○)が表示されるらしい。

・UIPageViewControllerを生成するとき、nitWithTransitionStyle:UIPageViewControllerTransitionStyleScrollになっている。

    self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
                               navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
                               options:nil];

※self.pageViewControllerは、UIPageViewControllerです。

・ページが戻ったときに呼ばれるハンドラ

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
    .....
{

・ページが進んだときに呼ばれるハンドラ

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
    .....
{

【参考】
UIPageViewControllerの画面下部に表示されているPageControlを隠す – iOSアプリ開発の逆引き辞典
UIPageViewControllerの使い方 -Tips- – hyoromoのブログ

が、実際に自分で作ってみたら、何も表示されない。散々いろんなケースを疑い、果てはないなら自分でUIPageControlを実装してみたりして試行錯誤した結果、わかったことは・・・

「UIPageControlは表示されているが見えないだけ」

であったことが判明。

UIPageControlはざっくりいうと、

・親ViewにUIPageViewControllerを生成して、subViewとして追加。
・追加されたUIPageViewController.viewに、実際に表示する子Viewを入れ替えることで切替を表示する。

という仕組みになっている。

で、UIPageControlも親Viewに表示されるはずですが、親ViewのbackgroundColorが白(whiteColor)になっていたことが原因でした。
UIPageControlの初期値についての情報は得られなかったのですが、色々試した限りでは。。。

・[UIPageControl currentPageIndicatorTintColor](現在ページの○)>白
・[UIPageControl pageIndicatorTintColor](選択されてないページの○)>白+アルファ値(50%?)
・[UIPageControl backgroundColor](背景色)>白

となっているようです。よって親ViewのbackgroundColorが白だと何も見えない。ただ、あるはずの場所をタップすると動きます(見えないのでタップが可能ということ自体に気づいたのもだいぶ後になってからですが…)。

要するに色を替えて見えるようにしてやればいいけど、UIPageViewについてググっても、UIPageView+UIScrollViewをセットで使うときの話題ばかり。で、ようやく見つけたのがコレ。UIPageViewControllerの中にあるUIPageControlを参照すればいいようです。

    //ページコントロール(色を変更する)
    UIPageControl* proxy = [UIPageControl appearanceWhenContainedIn:[self.pageViewController class], nil];
    
//    NSLog(@"setPageIndicatorTintColor >> %@",[proxy pageIndicatorTintColor]);
//    NSLog(@"currentPageIndicatorTintColor >> %@",[proxy currentPageIndicatorTintColor]);
//    NSLog(@"backgroundColor >> %@",[proxy backgroundColor]);
    
    [proxy setPageIndicatorTintColor:[UIColor lightGrayColor]];
    [proxy setCurrentPageIndicatorTintColor:[UIColor blackColor]];
    [proxy setBackgroundColor:[UIColor whiteColor]];

※self.pageViewControllerは、UIPageViewControllerです。

ちなみにコメントになっているNSLogでデフォルトの色を確認しようとしたのだけれど、(null)しか返ってきませんでした。何でだろう?

【参考】
storyboard – iOS Page based template, regarding UIPageViewController and UIPageControl color of the dots – Stack Overflow

これでようやくUIPageControlが表示されました。左右スクロールによる表示制御やタップしたときの挙動は、デフォルトで設定されるようで、UIPageControlについて書いたコードは、上の色替えの部分のみです。

更に、デフォルトだとUIPageControlの位置がかなり下になるので、UIPageViewController.viewの高さを削ってやると、もうちょっと上の位置になります。

- (void)viewDidLoad
{
    ....
    // Change the size of page view controller
    self.pageViewController.view.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height - 30);
    ....
{

最後に一応、コードを載せておきます。
ここでは親クラス(HelpViewController)と子クラス(DetailViewController)を使っています。
子クラス(DetailViewController)には・・・

@property NSUInteger pageIndex;
@property NSString *titleText;
@property NSString *captionText;
@property NSString *imageFile;

のプロパティがあって、生成時に表示する情報を渡しています。

・HelpViewController.h


#import <UIKit/UIKit.h>
#import "DetailViewController.h"

@interface HelpViewController : UIViewController<UIPageViewControllerDataSource,UIPageViewControllerDelegate>

@property (strong, nonatomic) UIPageViewController *pageViewController;
@property (strong, nonatomic) NSArray *pageTitles;
@property (strong, nonatomic) NSArray *pageImages;

@end

・HelpViewController.m

#import "HelpViewController.h"

@interface HelpViewController ()
{
    NSMutableArray *captionArray;
    NSMutableArray *imageArray;
    NSInteger currentPageIndex;
}

@end

@implementation HelpViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    //----
    
    //タイトル設定
    
    self.navigationItem.title = NSLocalizedString(@"conf_use", NULL);
    
    // テキストを設定する
    captionArray = [[NSMutableArray alloc] init];
    [captionArray addObject:NSLocalizedString(@"use_caption1", NULL)];
    [captionArray addObject:NSLocalizedString(@"use_caption3", NULL)];
    [captionArray addObject:NSLocalizedString(@"use_caption4", NULL)];
    [captionArray addObject:NSLocalizedString(@"use_caption5", NULL)];
    [captionArray addObject:NSLocalizedString(@"use_caption6", NULL)];
    [captionArray addObject:NSLocalizedString(@"use_caption7", NULL)];
    [captionArray addObject:NSLocalizedString(@"use_caption8", NULL)];
    [captionArray addObject:NSLocalizedString(@"use_caption9", NULL)];
    
    //
    // 画像ファイル名名を設定する
    imageArray = [[NSMutableArray alloc] init];
    [imageArray addObject:NSLocalizedString(@"help_img01", NULL)];
    [imageArray addObject:NSLocalizedString(@"help_img03", NULL)];
    [imageArray addObject:NSLocalizedString(@"help_img04", NULL)];
    [imageArray addObject:NSLocalizedString(@"help_img05", NULL)];
    [imageArray addObject:NSLocalizedString(@"help_img06", NULL)];
    [imageArray addObject:NSLocalizedString(@"help_img07", NULL)];
    [imageArray addObject:NSLocalizedString(@"help_img08", NULL)];
    [imageArray addObject:NSLocalizedString(@"help_img09", NULL)];

    // UIPageViewControllerを生成
    self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
                               navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
                               options:nil];
    
    self.pageViewController.dataSource = self;
    self.pageViewController.delegate = self;
    
    DetailViewController *startingViewController = [self viewControllerAtIndex:0];
    NSArray *viewControllers = @[startingViewController];
    [self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
    
    // UIPageViewControllerのサイズを変更(UIPageControlを上に表示する為)
    self.pageViewController.view.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height - 30);
    
    [self addChildViewController:_pageViewController];
    [self.view addSubview:_pageViewController.view];
    [self.pageViewController didMoveToParentViewController:self];
    
    //ページコントロール(色を変更する)
    UIPageControl* proxy = [UIPageControl appearanceWhenContainedIn:[self.pageViewController class], nil];
    
    [proxy setPageIndicatorTintColor:[UIColor lightGrayColor]];
    [proxy setCurrentPageIndicatorTintColor:[UIColor blackColor]];
    [proxy setBackgroundColor:[UIColor whiteColor]];
    
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (DetailViewController *)viewControllerAtIndex:(NSUInteger)index
{
    if (([captionArray count] == 0) || (index >= [captionArray count])) {
        return nil;
    }
    
    // 新しいページのviewを生成
    DetailViewController *DetailViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"DetailViewController"];
    DetailViewController.imageFile = imageArray[index];
    DetailViewController.captionText = captionArray[index];
    DetailViewController.titleText = NSLocalizedString(@"conf_use", NULL);

    DetailViewController.pageIndex = index;
    
    return DetailViewController;
}

#pragma mark - Page View Controller Data Source

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
    NSUInteger index = ((DetailViewController*) viewController).pageIndex;
    
    if ((index == 0) || (index == NSNotFound)) {
        return nil;
    }
    
    index--;

    return [self viewControllerAtIndex:index];
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
    NSUInteger index = ((DetailViewController*) viewController).pageIndex;
    
    if ((index >= [captionArray count]) || (index == NSNotFound)) {
        return nil;
    }
    
    index++;
    if (index == [captionArray count]) {
        return nil;
    }
    
    return [self viewControllerAtIndex:index];
}

- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController
{
    return [captionArray count];
}

- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
{
    return 0;
}

-(void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
    if(completed){
        DetailViewController *vc =[self.pageViewController.viewControllers lastObject];
        currentPageIndex =  vc.pageIndex;
    }
}


@end

・DetailViewController.h

#import <UIKit/UIKit.h>

@interface DetailViewController : UIViewController
    @property (weak, nonatomic) IBOutlet UITextView *caption;
    @property (weak, nonatomic) IBOutlet UIImageView *helpImage;

@property NSUInteger pageIndex;
@property NSString *titleText;
@property NSString *captionText;
@property NSString *imageFile;

@end

・DetailViewController.m

#import "DetailViewController.h"

@interface DetailViewController ()

@end

@implementation DetailViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    _helpImage.image = [UIImage imageNamed:self.imageFile];
    self.title = self.titleText;
    _caption.text = self.captionText;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

あまりに単純な話ですが、同じような目に遭う人もいるかもしれないのでメモとして。

【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