2013年11月25日月曜日

Projectに追加した画像ファイルの場所


UIImage の imageNamed メソッドを使うとUIImageオブジェクトがキャッシュされます。サイズが大きい画像の場合などでキャッシュに残したくない場合はimageWithContentsOfFile メソッドを使用するように書かれています。

imageNamed:
If you have an image file that will only be displayed once and wish to ensure that it does not get added to the system’s cache, you should instead create your image using imageWithContentsOfFile:.

プロジェクトに追加しイメージファイルのパスは次の方法で取得します。

NSString* imgPath =
                  [[NSBundle mainBundle] pathForResource:imgName ofType:@"png"];



設定の通知センターからアプリが消えない



Push Notificationのテスト後、Push Notificationを無効にしたが、通知センターからアプリアイコンが削除されない。アプリをアンインストールするといったん消えるが、再インストールすると再度登録されてしまう。

これはPush Notification許諾の確認が一日一回だけ行われ、いったん許諾するとその間はそのアプリが通知センターの対象となるため。

デバイスの時計を一日以上先に進めるとリセットすることができる。
手順は以下のサイト参照。

Troubleshooting Push Notifications
When I delete my iOS application push notification state remains

2013年11月24日日曜日

UIWebViewのキャッシュ



UIWebViewを使用しているときのキャッシュコントロール

//キャッシュを全て消去
[[NSURLCache sharedURLCache] removeAllCachedResponses];

//NSURLRequest作成時にポリシーを設定
NSURLRequest *request = [NSURLRequest requestWithURL:url
                                     cachePolicy:NSURLRequestReturnCacheDataElseLoad
                                     timeoutInterval:30.0];


NSURLRequestCachePolicyの値


NSURLRequestUseProtocolCachePolicy
Protocolのキャッシュポリシーに従う。デフォルト。

NSURLRequestReloadIgnoringLocalCacheData
キャッシュの有効期限にかかわらずurlへアクセス


NSURLRequestReloadIgnoringLocalAndRemoteCacheData
上に加え、proxyなどにもキャッシュ破棄を指示する。

NSURLRequestReturnCacheDataElseLoad
キャシュがあればそれを使用、なければURLへアクセス

NSURLRequestReturnCacheDataDontLoad
キャッシュのみを使用。オフラインと同等の動作。


NSURLRequestReloadRevalidatingCacheData
有効性をチェックし、有効ならキャッシュを使い、無効ならリロード。

2013年10月22日火曜日

iOS7対応: install iconが消えない



Enterprise InHouse Distributionで、iOS6では問題なくインストールが完了するが、iOS7ではインストールアイコンが残ったままになるという現象が発生しました。

私の場合はアプリのBundle Identifierと、インストールサイトにあるplistのそれとが一致していなかったのが原因でした。

Ad Hocのインストールで同様の問題が発生するという書き込みが結構あります。
同じ原因の場合もありましたが、他にprovisioning, certificateを作り直して解決した場合も報告されています。これも、なんらかの不整合が解消したためでしょう。

iOS6ではインストールが完了するため、かえって基本的な設定ミスを見逃していることがありそうです。

参考:
http://forums.macrumors.com/showthread.php?t=1626848
https://devforums.apple.com/message/904282#904282

2013年10月5日土曜日

Build番号の自動インクリメント



Build番号の自動インクリメントを仕掛けました。

このページに画面キャプチャー付きで手順が載っており、そのとおりに行えば設定できます。


・ProjectのBuild PhaseにRun Sricptを追加する。
・Run Sricptは末尾に追加され、ビルドの最後で実行される。(ドラッグで順序を変更可能)
・Run Scriptニシェルスクリプトを登録する。

Xcode5ではBuild Phase追加はメニューから行います。

"How to auto-increment Bundle Version in Xcode 4?"ではスクリプトファイルを保存し、それを実行するため、プロジェクトを他の環境でビルドする場合は各々の環境にスクリプトファイルを保存する必要があります。それが面倒な場合はRun Scriptに直接スクリプトを書くこともできます。


buildPlist=${INFOPLIST_FILE}
newVersion=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "$buildPlist" | /usr/bin/perl -pe 's/(\d+\.\d+\.)(\d+)/$1.($2+1)/eg'`

この場合はVersion、Build ともに数字3つのをピリオドで区切ったものとなります。
例:Version=1.1.0 、Build=1.1.123

Versionを 1.1.0 、Buildを 1.1.0.123 のように設定する場合はPerlの正規表現を次のように変更します。("\d+\."を追加)
's/(\d+\.\d+\.\d+\.)(\d+)/$1.($2+1)/eg'

TARGETS/Generalの設定は次のようになります。

プログラム中で値を取得する例
(リリース時はVersion、デバッグ時はBuild):
#ifdef DEBUG
    NSString *buildNumber = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
#else
    NSString *buildNumber = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
#endif

2013年9月14日土曜日

iOS7対応: iOS6/7 delta



iOS7ではStatusBar、NavigationBarとアプリのViewがオーバラップして表示されるため、Xcode5のstoryboardで配置したViewの位置がiOS6/7で違ってしまう。

ViewのframeのルートViewに対するframe.origin.yの値が0の場合、iOS6ではStatusBar、NavigationBarの下に配置されるが、iOS7ではスクリーンの左上に配置される。

autolayoutの場合は調整してくれるようだが、そうでない場合は調整が必要になる。
Interface BuilderのサイズインスペクタにあるiOS6/7 deltaで行うことができる。

Xcode5ではこまめ表示/非表示ができるので、上のキャプチャーの少し上にあるViewのhideをクリックすると、この部分がすっかり隠れてしまいます。

iPhoneのUITableViewでは、iOS6と同じ設定だとiOS7ではViewの上にNavigationBar分の空きができる。この場合はiOS7の場合にcontentInsetのtopを-44にするとiOS6と同じレイアウトになる。

参考: Interface Builder: What are the UIView's Layout iOS 6/7 Deltas for?

2013年9月13日金曜日

iOS7対応: UIButtonの属性



UIButtonTypeRoundedRectのUIButtonをプログラムで作っている場合、iOS7では背景色なし、ボーダーなし、文字色シアンのため、iOS6から移行するとき調整が必要なことが多くなりそうです。

//背景色
myButton.backgroundColor = [UIColor lightGrayColor];

//文字色
//[myButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];

//border
[[myButton layersetBorderWidth:1.0f];
[[myButton layer] setBorderColor:[UIColor darkGrayColor].CGColor];



iOS7対応: StatusBarが非表示にならない



iOS7: StatusBarが非表示にならない

//ルートのNavigationControllerに追加。
- (void)viewDidLoad
{
    [super viewDidAppear:animated];
    // iOS 7
    if ([self respondsToSelector:@selector(setNeedsStatusBarAppearanceUpdate)]) {
        [self setNeedsStatusBarAppearanceUpdate];
    }
}

//StatusBarを非表示にしたいViewControllerに次のメソッドを追加する。
//defaultの値はNO
- (BOOL)prefersStatusBarHidden
{
    return YES;
}

NavigationControllerが複数ある場合は、各々で
[self setNeedsStatusBarAppearanceUpdate]
を呼ぶ必要があるかもしれません。

iPadでUISplitViewControllerを使う場合は振る舞いが異なる。
この場合。UISplitViewControllerにprefersStatusBarHiddenを実装、YESを返すようにすると全てのViewControllerでStatusBarが非表示になる。
個別にStatusBarの表示/非表示を切り替える方法は試していない。

2013年9月8日日曜日

UITextFieldのleftView追加例



UITextFieldの左側にundoボタンを追加する例です。
leftViewModeをUITextFieldViewModeWhileEditingとしたのでは表示されなかったので、becomeFirstResponder、resignFirstResponderをオーバライドし、表示/非表示を切り替えています。

//UITextFieldのサブクラスを作る。
@implementation MyTextFieldWithLeftView

//Storyboardで設定する場合の初期化
//leftViewにUIButtonをセット。
//初期状態は非表示。
- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        UIButton *btn=[UIButton buttonWithType:UIButtonTypeCustom];
        btn.frame = CGRectMake(6,0,30,30);
        [btn setImage:[UIImage imageNamed:@"undo"]
                forState:UIControlStateNormal];
        self.leftView = btn;
    }
    return self;
}

//leftViewtarget/actionをセットする。
//複数のTextFieldがある場合、どれか区別がつくようにtagを設定する。
//部品が配置されるViewContollerなどから呼び、ボタンのtarget/actionをセットする。
- (void)addLeftButtonAction:(id)target action:(SEL)selector tag:(NSInteger)tag;
{
    [(id)self.leftView addTarget:target action:selector
                forControlEvents:UIControlEventTouchUpInside];
    self.leftView.tag = tag;
}

//firstResponderになるときにleftViewを表示。
- (BOOL)becomeFirstResponder
{
    self.leftViewMode = UITextFieldViewModeAlways;
    return [super becomeFirstResponder];
}

//firstResponderでなくなるときにleftViewを非表示。
- (BOOL)resignFirstResponder
{
    self.leftViewMode = UITextFieldViewModeNever;
    return [super resignFirstResponder];
}

2013年9月4日水曜日

Archive作成時の header file not found エラー



アプリとstatic libraryのProjectがあるworkspaceで、Archive作成時にヘッダファイルが見つからないエラーが発生しました。
エラーメッセージは次のようなものです。
Lexical or Processor Issue
/Users/.../AppWorkspace/LibA/LibA/MyClassA.h:10:9:
  'LibB/MyClassB.h' file not found

いろいろバリエーションがあるようですが、基本的にはヘッダのサーチパスの設定に由来するようです。
私の場合はworkspaceにアプリのプロジェクトとstatic libraryがふたつあり、そのひとつが他方に依存しているという環境で発生しました。
App - libA、libBに依存
libA - libBに依存
libB

この場合、libAのビルドに失敗しているので、libAのBuild Settingsに設定を追加します。
Build Settings > Serach Paths > User Search Paths > Release
"$(PROJECT_DIR)/.."  recursive

recurciveの設定は入力フィールドをダブルクリックすると表示されるダイアログの右端にある、non-recursive/recursive切り替えスイッチで行います。

次のサイトを参考にしましたが、私の場合はUser Search Pathsの追加だけで解決しています。これだけではダメな場合は参照してみてください。

http://stackoverflow.com/questions/5413338/xcode-4-and-nested-projects-header-files-not-found

http://stackoverflow.com/questions/5584317/compile-build-or-archive-problems-with-xcode-4-and-dependencies

2013年8月16日金曜日

PDFサムネイル表示改良版



PDFサムネイル表示の例では全ページのサムネイルを作り終えるまで応答しなくなる、メモリ不足になっても回復不能でアプリが落ちるという問題があります。その改善策の例です。

@implementation WebViewWithThumnails

//WebViewWithThumnailsではプレイスホルダーとなるThumbnailViewの配置だけ
//行い、サムネイルイメージはセットしない。
- (void)addThumbnailsWithData:(NSData *)pdfData
{
    //画面下にUIScrollViewを追加
    _thumbnailScrollView =
        [[ThumbnailScrollView alloc] initWithWebView:self height:80];
    [_thumbnailScrollView addThumbnailViews:pdfData];
    //この行を削除。
    //[_thumbnailScrollView setPdfThumbnailImage:pdfData];
}

@implementation ThumbnailScrollView

//layoutSubviewsをオーバライドし、表示領域内にあるThumbnailViewに
//サムネイルイメージをセットする。
- (void)layoutSubviews
{
    float x1 = self.contentOffset.x;
    float x2 = x1 + self.frame.size.width;
    NSMutableArray *views = [NSMutableArray arrayWithCapacity:16];
    for(ThumbnailImageView *v in self.subviews) {
        if ([v isMemberOfClass:[ThumbnailImageView class]]) {
            if (v.frame.origin.x >= x1 - v.frame.size.width && v.frame.origin.x <= x2
                && v.subviews.count < 2) {
                [views addObject:v];
            }
        }
    }
    //すぐにサムネイルを作らず、performSelector:withObject:afterDelay:でキュー
    //に入れる。cancelPreviousPerformRequestsWithTarget:を呼ぶことで、
    //layoutSubviewsが続けて呼ばれた場合に最後のものだけが実行される。
    if (views.count > 0) {
        [NSObject cancelPreviousPerformRequestsWithTarget:self];
        [self performSelector:@selector(setImagesWithViewArray:) withObject:views afterDelay:0.1];
    }
    [super layoutSubviews];
}

- (void)setImagesWithViewArray:(NSArray *)views
{
    //残りメモリが不足していたらメモリ空き容量を増やす。
    if (_memMonitor.freeMemory < 50000000) {
        [self removeThumbnailImages];
        //十分メモリが空いたかわからないのでreturn。
        return;
    }
    //表示領域内のサムネイル画像を表示する。
    for(ThumbnailImageView *v in views) {
        CGPDFPageRef page = CGPDFDocumentGetPage(_pdf, v.pageNum);
        [v setImageWithPdfPage:page];
    }
}

//この例では既に作成されているサムネイル画像を全て解放する。
- (void)removeThumbnailImages
{
    for(UIView *v in self.subviews) {
        if ([v isMemberOfClass:[ThumbnailImageView class]]) {
            [(ThumbnailImageView *)v removeThumbnailImage];
        }
    }
}

2013年8月15日木曜日

webView:didFailLoadWithError: error.codeリファレンス



UIWebViewDelegate の webView:didFailLoadWithError: で渡される error のerror.code の内容が書かれているドキュメント。

CFNetwork Error Codes Reference

Organizer - Documentationでこの名前で検索する。

内容の一部

enum CFNetworkErrors {
   kCFHostErrorHostNotFound      = 1,
   kCFHostErrorUnknown           = 2,
   
/* SOCKS errors */
   kCFSOCKSErrorUnknownClientVersion = 100,
   kCFSOCKSErrorUnsupportedServerVersion = 101,
   
/* SOCKS4-specific errors*/
   kCFSOCKS4ErrorRequestFailed   = 110,
   kCFSOCKS4ErrorIdentdFailed    = 111,
   kCFSOCKS4ErrorIdConflict      = 112,
   kCFSOCKS4ErrorUnknownStatusCode = 113,

   ......

/* CFURL and CFURLConnection Errors */
   kCFURLErrorUnknown   = -998,
   kCFURLErrorCancelled = -999,
   kCFURLErrorBadURL    = -1000,
   kCFURLErrorTimedOut  = -1001,
   kCFURLErrorUnsupportedURL = -1002,
   kCFURLErrorCannotFindHost = -1003,
   kCFURLErrorCannotConnectToHost    = -1004,
   kCFURLErrorNetworkConnectionLost  = -1005,
   kCFURLErrorDNSLookupFailed        = -1006,
   kCFURLErrorHTTPTooManyRedirects   = -1007,
   kCFURLErrorResourceUnavailable    = -1008,
   kCFURLErrorNotConnectedToInternet = -1009,
   ......

2013年8月11日日曜日

PDFサムネイル表示例



PDFのサムネイルを表示する一例です。

以下の実装ではページ数の多いPDFでは全ページのサムネイルを作り終えるまで応答がなくなるので操作性に問題があり、またサムネイル作成中にメモリ不足になっても回復不能でアプリが落ちてしまいます。この改善方法についてはPDFサムネイル表示改良版を参照してください。

手順概要
  • 次のクラスを追加する。
    • WebViewWithThumnails UIWebViewのサブクラス
      サムネイル画像表示機能を追加するWebView
    • PdfThumbnailSrollVewi  UIScrollViewのサブクラス
      WebViewWithThumnailsに追加するサムネイル用スクロールビュー
    • PdfImageView UIControlのサブクラス
      サムネイルを表示し、タップに応答するビュー
  • MyViewControllerのviewをWebViewWithThumnailsに変更
    • WebViewWithThumnailsのdelegateにMyViewControllerをセット
  • MyViewControllerにgoogleなどの検索画面を表示
  • MyViewControllerのwebView:shouldStartLoadWithRequest:navigationType:メソッドでURLをチェックし、pdfファイルの場合は処理をトラップ
  • PDFファイルダウンロードが完了したら
    • WebViewWithThumnailsにPDFを表示
    • WebViewWithThumnailsにPdfThumbnailSrollVewi追加
    • PdfThumbnailSrollVewiにサムネールイメージをセット
実装例

PDFファイルをダウンロードする手順はダウンロードファイルをNSDataで取得する方法を参照。
「受信終了」で呼ぶ [self  self loadPdf]以降を例示。

@implementation MyViewController
//PDFファイルダウンロード完了
- (void)connectionDidFinishLoading:(NSConnection*)connection;
{
    _connection = nil;
    [self loadPdf];
}

//受信終了で呼び、ダウンロードした_dataをWebViewにロードする。
- (void)loadPdf
{
    //PDFUIWebViewに表示する
  [ (WebViewWithThumbnails *)self.view loadData:_data
                                                            MIMEType:@"application/pdf"
                                               textEncodingName:@"utf-8"
                                                              baseURL:nil];
}
@end

@interface WebViewWithThumbnails : UIWebView 
- (void)addThumbnailsWithData:(NSData *)pdfData;
@end

@implementation WebViewWithThumbnails
//オーバライド、superを呼んだ後にサムネールをセットする。
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)encodingName baseURL:(NSURL *)baseURL
{
    [self  removeThumbnailScrollView];
    [super loadData:data MIMEType:MIMEType textEncodingName:encodingName baseURL:baseURL];
    [self addThumbnailsWithData:data];
}

//PDF以外の場合に呼ばれる。thumbnailScrollViewをremoveする。
- (void)loadRequest:(NSURLRequest *)request
{
    [self  removeThumbnailScrollView];
    [super loadRequest:request];
}

//subviewsの中にThumbnailScrollViewオブジェクトがあればremoveする。
- (void)removeThumbnailScrollView
{
    for(UIView *v in self.subviews) {
        if ([v isMemberOfClass:[ThumbnailScrollView class]]) {
            [v removeFromSuperview];
            break;
        }
    }
}

//サムネールセット
- (void)addThumbnailsWithData:(NSData *)pdfData
{
    [self  removeThumbnailScrollView];
    //画面下にUIScrollViewを追加
    ThumbnailScrollView *thumbnailScrollView =
        [[ThumbnailScrollView allocinitWithWebView:self height:80];
    //各ページサイズに合わせたThumbnailViewを追加
    [thumbnailScrollView addThumbnailViews:pdfData];
    //ThumbnailViewにイメージをセット
    [thumbnailScrollView setPdfThumbnailImage:pdfData];
}
@end

@interface ThumbnailScrollView : UIScrollView
- (ThumbnailScrollView *)initWithWebView:(UIWebView *)superview height:(float)height;
- (void)addThumbnailViews:(NSData *)pdfData;
- (void)setPdfThumbnailImage:(NSData *)pdfData;
@end

@implementation ThumbnailScrollView
{
    //イメージセットが完了するまで保持する。
    CGPDFDocumentRef _pdf;
}

//保持していた_pdfをreleaseする。
//setPdfThumbnailImageでイメージセット完了時にも実行しているが、
//その前にdeallocされる可能性があるのでここでも実行する。
- (void)dealloc
{
    if (_pdf) CGPDFDocumentRelease(_pdf);
    _pdf = nil;
}

//superviewUIScrollViewを追加。frameはsuperviewに合わせて設定する。
- (ThumbnailScrollView *)initWithWebView:(UIWebView *)webView height:(float)height
{
    //画面下10pt上の位置に設定。高さはパラメータの値、幅はsuperviewと同じ。
    CGRect r = CGRectMake(0, webView.frame.size.height - height - 10,
                          webView.frame.size.width, height);
    self = [super initWithFrame:r];
    if (self) {
        [webView addSubview:self];
    }
    return self;
}

//ScrollView内に、PDFページに対応するThumbnailImageViewを配置する。
//ここではまだイメージはセットしないが、矩形領域は表示されるようになる。
- (void)addThumbnailViews:(NSData *)pdfData
{
    CGDataProviderRef provider =
        CGDataProviderCreateWithCFData((__bridge CFDataRef)pdfData);
    _pdf = CGPDFDocumentCreateWithProvider(provider);
    //providerは同じメソッド内でreleaseしないとメモリリークの原因になる。
    CFRelease(provider);
    
    int numPages = CGPDFDocumentGetNumberOfPages(_pdf);
    float x = 10;
    float h = self.frame.size.height;
    float pagePos = 0;
    UIScrollView *webScrollView = ((UIWebView *)self.superview).scrollView;
    for (int p  = 1; p <= numPages; p++) {
        //CGPDFPageRefautoreleaseされるのでreleaseする必要なし。
        CGPDFPageRef pageRef = CGPDFDocumentGetPage(_pdf, 1);
        CGRect pageRect = CGPDFPageGetBoxRect(pageRef, kCGPDFCropBox);
        float w = pageRect.size.width * (h / pageRect.size.height);
        CGRect imageRect = CGRectMake(x, 0, w, h);
        ThumbnailImageView *v = [[ThumbnailImageView alloc] initWithFrame:imageRect pageNum:p pagePos:pagePos];
        v.alpha = 0.5;
        [self addSubview:v];
        //タップ時のアクションをセット
        [v addTarget:self action:@selector(jumpToPage:) forControlEvents:UIControlEventTouchUpInside];
        x += w + 10; //サムネイル間に10ptの間隔を置く。
        pagePos += pageRect.size.height * (webScrollView.frame.size.width-5) / pageRect.size.width;
        //contentSize調整
        self.contentSize = CGSizeMake(self.contentSize.width + w + 10, self.contentSize.height);
    }    
    [self setNeedsDisplay];
}

//配置したThumbnailImageViewにイメージをセット
- (void)setPdfThumbnailImage:(NSData *)pdfData
{
    size_t numPages = CGPDFDocumentGetNumberOfPages(_pdf);
    for(int p=1; p<=numPages; p++) {        
        CGPDFPageRef pageRef = CGPDFDocumentGetPage(_pdf, p);
        ThumbnailImageView *v = [self.subviews objectAtIndex:p-1];
        [v setImageWithPdfPage:pageRef];
    }
    //ループが無事完了したら_pdfをリリース
    CGPDFDocumentRelease(_pdf);
    _pdf = nil;
    [self setNeedsDisplay];
}

//タップされたサムネールのページ位置に移動
- (void)jumpToPage:(ThumbnailImageView *)sender
{
    UIScrollView *scrollView = ((UIWebView *)self.superview).scrollView;
    CGPoint point = CGPointMake(0, sender.pagePos * scrollView.zoomScale);
    [scrollView setContentOffset:point animated:YES];
}

@end

2013年7月2日火曜日

ダウンロードファイルをNSDataで取得する方法



htmlのリンクでダウンロードするデータをNSDataで取得する一例です。ここでは説明を簡単にするためにPDFファイルとしていますが、それ以外の場合にも適用できます。

手順概要
  • UIWebViewDelegateのwebView:shouldStartLoadWithRequest:navigationType:メソッドでURLをチェック
  • PDFファイルの場合はNOを返し、UIWebViewから直接リクエストを送らないようする。(YESを返しても動作するが同じファイルを同時にリクエストすることになる?)
  • NSURLRequestを使ったリクエスト送信のメソッドを非同期で実行する。
  • NSURLRequestの受信データをNSDataに蓄積する。
  • NSURLRequestの受信完了でNSDataを使用する処理を実行する
実装例

storyboardでMyViewControllerのviewのクラスをUIWebViewに変更
viewのoutlets delegateをMyViewControllerに接続

//MyViewControllerにUIWebViewDelegateプロトコルを追加
@interface MyViewController : UIViewController <UIWebViewDelegate>

//インスタンス変数
@implementation FirstViewController
{
    NSURLConnection *_connection;
    NSMutableData *_data;
}

- (void)dealloc
{
    [_connection cancel];
    _connection = nil;
    _data = nil;
}

- (void)didReceiveMemoryWarning
{  
    [_connection cancel];
    _connection = nil;
    _data = nil;
    [super didReceiveMemoryWarning];
}

//画面表示時に検索ページを表示
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSString *urlStr =
        [NSString stringWithFormat:@"http://www.google.co.jp/search?q=pdf"];  
    [self sendHttpRequest:[NSURL URLWithString:urlStr]];
}

//webView:shouldStartLoadWithRequest:navigationType:実装
- (BOOL)webView:(UIWebView *)webView
     shouldStartLoadWithRequest:(NSURLRequest *)request
                 navigationType:(UIWebViewNavigationType)navigationType
{
    //URLにPDFオープンパラメータが付いている場合を考慮
    NSRange range = [request.URL.absoluteString rangeOfString:@"\\.pdf(#.+|$)"
            options:NSRegularExpressionSearch|NSRegularExpressionCaseInsensitive];
    //PDFの場合はデータ取得
    if (range.location != NSNotFound) {
        [self sendHttpRequest:request];
        return NO;
    }
    //その他はUIWebViewにまかせる
    return YES;
}

//リクエスト送信
- (void)sendHttpRequest:(NSURLRequest *)request
{
//まだ受信が完了していない_connectionがあるかもしれないのでcancelしておく。
[_connection cancel];
    _connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    if (_connection) {
        //受信データを格納するNSData
        _data = [NSMutableData data];
    } else {
        [(UIWebView *)self.view loadHTMLString:@"接続エラー" baseURL:nil];
    }
}

//レスポンス受信 - 念のため_dataを空にする
//Multipartの場合に複数回呼ばれる可能性あり
- (void)connection:(NSURLConnection *)connection
        didReceiveResponse:(NSURLResponse *)response
{
     [_data setLength:0];
}

//受信データの結合 - 複数回呼ばれる
- (void)connection:(NSURLConnection *)connention didReceiveData:(NSData *)data;
{
    [_data appendData:data];
}

//エラー発生時に呼ばれる
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
{
    _data = nil;
    _connection = nil;
}

//受信終了
- (void)connectionDidFinishLoading:(NSConnection*)connection;
{
    _connection = nil;
    //PDFをUIWebViewに表示する
    [self loadPdf];


2013年6月20日木曜日

iPadでresignFirstResponderが機能しない



iPadでUIModalPresentationFormSheetを使ってModal Viewを表示した場合、UITextFiledのタップでキーボードを表示したあと、textFieldShouldReturn:でresignFirstResponderを行ってもキーボードが消えないという現象があります。
続けて入力を行う可能性を考慮し、ひとつのコントロールがresignFirstResponderしてもいちいちキーボードを隠さないという仕様だそうです。
それでも直ぐにキーボードを消したい場合はUIViewControllerに次のようなコードを追加します。

- (BOOL)disablesAutomaticKeyboardDismissal {
    return NO;
}

iPadでは画面構成によってはUIViewControllerがいくつも登場するため、どれにしかければ有効なのか悩まされました。
Modal Viewの場合はそれを表示するUINavigationControllerをサブクラス化し、それに上記メソッドを実装すると解決します。

参照:resignFirstResponder Don't work?

2013年6月14日金曜日

All tests did not finish



Xcodeでテストを実行すると"All tests did not finish"と警告がでる。
Application Testの場合、Application起動中にTestが終了してまうのが原因だそうで、書き込みどおりsleepさせてみたら警告がでなくなった。
Some of my unit tests tests are not finishing in XCode 4.4

ということで、テスト項目が少ないときにこの警告が出ても気にしなくてよいようです。

2013年6月10日月曜日

Base Localizationの追加


古いバージョンで作ったプロジェクトにBase Localizationの追加する手順

Projectを選択
Localizationの"Use Base Internationalization"をクリック。

ベースとなる言語を選択

Finishをクリック。


------------------------------
メニューからの操作する場合
 PROJECTを選択
 Editorメニューの"Add Localization"をクリック
 言語選択サブメニューの"Other" > "Base(Base)"をクリック
------------------------------

Base.lprojが追加され、en.lprojフォルダからstoryboardが削除され
infoPlist.stringsだけになる。

同様にEditorメニューの"Add Localization"で"Japanese"を追加する。
ja.lprojが追加される。
MainStoryboard.strings、Localizable.stringsを追加する。
MainStoryboardの部分はStoryboardと一致させる。
StoryboardのUI部品はMainStoryboard.stringsに、NSLocalisedStringで参照するものはLocalizable.stringファイルに設定する。
MainStoryboard.stringsは後述のibtool で作成するものでよい。

storyboadへUI部品を追加したときのstringsファイルの更新
Terminalを起動
Base.jprojへ移動
ibtool MainStoryboard.storyboard --generate-strings-file NewStuff.strings
NewStuff.stringsを開き、追加された行をMainStoryoard.stringsへコピーする。

毎回コマンドを入力するのは面倒なので、コマンドのaliasを作っておくと便利です。
alias設定の方法は"mac osx terminal alias"などで検索してみてください。
例:
alias newstrings="cd (プロジェクトのパス)/Base.lproj;lbtool Mainstoryboard_iPhone.storyboard --generate-string-file new.strings"

参照:
Internationalize Your App


2013年6月9日日曜日

iOS WorkspaceでFramework(Library) Projectを作る方法



XCodeでWoekspaceを作り、Applicarion ProjectとFrameowork Projectを関連づける方法です。
検索するといくつも出てはきますが、バージョンによって手順が異なっており、XCode 4.6.1にズバりのものが見つかりません。
一番近かったのはIntroduction to Using Static Libraries in iOSで、これは既存プロジェクトからFrameworkを分離する方法ですが、これに沿った手順で次の要領で新規に作ることができます。
  1. 新規Workspaceを作る。


  2. Frameworkプロジェクトを作る。

    Workspaceに追加する前にプロジェクトを閉じる。
  3. WorkspaceにFrameworkプロジェクトの.xcodeprojを追加する。
    File>New>またはWorkspaceの左ペイン右クリックからAdd FIles to ...をクリック。
     

    同様にApplicationプロジェクトの.xcodeprojを追加する。
  4. Workspaceで各々のプロジェクト内のtargetを選択、Workspaceでbuildする。
    正しくbuildされれるとTestFrameworkの下のproduct内のlibTestFramework.aの赤字が黒に変わる。赤のままのときはWorkspaceのProject選択をチェック。もし別にTestFrameworkプリジェクトが開いたら閉じる。
  5. Workspace でApplicationプロジェクトのtargetを選択。Build Phase>Link Binary With LibrariesでFrameworkの.aを追加する。



  6. Framework内のクラスを使うファイルに#importを追加。
    "TestFramework/TestFramework.h"
    "TestFramework"はtargetの"Product Name"に設定されている名前。"TestFramework.h"は使用するクラスのヘッダファイル。

  7. Build Phases 公開するヘッダの設定
    プロジェクト作成時に追加されるヘッダは予め含まれているが、その後プロジェクトに追加するヘッダファイルを公開する場合は、手動で追加する。
Framework Project のBuild Setting リンカオプション(-ObjC)は自動的に設定されますが、Application Projectには設定されていません。このままでも問題に場合が多いと思いますが、Framework に Category が含まれている場合はApplication Projectにもこのオプションを追加する必要があります

 "selector not recognized" が発生する場合は多分これが原因です。

Building Objective-C static libraries with categories



2013年6月8日土曜日

uncaught exceptionのstackのアドレスをシンボル表示する方法



たまにstackのアドレスだけがありシンボル表示がなく、どこで発生しているエラーかわからないメッセージで終了することがあります。

エラーメッセージの例:
2013-06-08 14:27:49.160 Test[29884:11303] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM removeObjectAtIndex:]: index 1 beyond bounds [0 .. 0]'
*** First throw call stack:

(0x1c90012 0x10cde7e 0x1c321c4 0x20bb 0xacb5b3 0x1c4f376 0x1c4ee06 0x1c36a82 0x1c35f44 0x1c35e1b 0x1bea7e3 0x1bea668 0x11ffc 0x1b4d 0x1a75)

こんな場合にuncaught exceptionの内容を表示する方法が紹介されていました。
Xcode 4.2 debug doesn't symbolicate stack call

次のコードはこのページからの引用です。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{
    .....
    NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
}


void uncaughtExceptionHandler(NSException *exception) {
    NSLog(@"CRASH: %@", exception);
    NSLog(@"Stack Trace: %@", [exception callStackSymbols]);

}

出力例:
2013-06-08 14:27:49.153 Test[29884:11303] Stack Trace: (
0   CoreFoundation                      0x01c9002e __exceptionPreprocess + 206
1   libobjc.A.dylib                     0x010cde7e objc_exception_throw + 44
2   CoreFoundation                      0x01c321c4 -[__NSArrayM removeObjectAtIndex:] + 212
3   Test                                0x000020bb -[MyViewController remove] + 91
4   Foundation                          0x00acb5b3 __NSFireDelayedPerform + 380
.....
)

エラーによっては手がかりになる情報も含まれてきます。仕込んでおいて損はないでしょう。

追記
swiftの場合
        NSSetUncaughtExceptionHandler{ exception in
            print(exception)
            print(exception.callStackSymbols)

        }
参照リンク:

How should I use NSSetUncaughtExceptionHandler in Swift


2013年4月19日金曜日

iOS Developer Program 更新後の作業



iOS Developer Program 更新後の作業
  1. Apple Developer Program Activation Codeが届く
  2. メールのアクティベーションコードをクリックし、Activation手続きを実行
  3. Provisioning Portalへ
    >Member Center
    >Certificates, Identifiers & Profiles
    >iOS Provisioning Profiles
  4. Certificate追加
    ダウンロードし、ダブルクリックでキーチェーンに追加
    更新前の同名のCertificateをキーチェーンから削除する。
  5. Provisioning Profile追加
    ダウンロードし、XCodeにドラッグ&ドロップ
関連エラー
  1. Certificate identity 'iPhone Developer: MyName (XXXXXXXXXX)' appears more than once in the keychain. The codesign tool requires there only be one.

    更新前の同名Certificateがキーチェーンに残っていると発生。

  2. Provisioning Profile 'MyApp Development' specifies the Application Identifier 'MyApp' which doesn't match the current setting 'MyApp'Provisioning Profiles

    Certificate、Provisioning Profileの一方だけが更新されている場合に発生。

  3. No unexpired provisioning profiles found that contain any of the keychain's signing certificates

    build対象のdeviceのprovisioning profilesに有効なものがない。
    • simulatorが対象の場合はOKだが、iPhoneなどの外部deviceの場合にエラーが発生する場合
      Organizerで当該deviceのprovisioning profilesをチェックする
      不要なものを削除、必要なものを追加する
    • buildできるアプリとできないアプリがある場合
      ProjectのBuildSettingでCode Settingをチェック
      選択されているprofileのApp IDをProvisioning Portalでチェック
      それに当該アプリのApp IDがマッチしているかチェック
      当該アプリのApp IDにマッチするprovisioning profileを更新、または追加する
      ProjectのCode Settingの設定が自動の場合、それまで選択されていたprofileが削除または無効になった場合に有効なものに置き換えられ、そのApp IDが当該アプリにマッチしなくなることがある。
  4. The entitlements specified in your application’s Code Signing Entitlements file do not match those specified in your provisioning profile. (0xE8008016).

    何らかの理由でアプリに適用されたProvisioningでは対象Deviceにアプリを転送することが許されていない場合、といえるようです。

    私の場合は追加したProvisioningにエラーが発生するdeviceを含めてなかったため発生しました。
    特定のDeviceでだけ発生するような場合は、Provisioningの設定にそのDeviceが含まれているかチェックしてみてください。

    様々な理由があるようで、いくつかの対処策が書き込まれています。
    http://stackoverflow.com/questions/5240801/the-entitlements-specified-profile-0xe8008016-error-ios-4-2

    よくわからない場合はアプリに適用されているProvisioningをいったん削除し、再度作り直してみるのが手っ取り早そうです。

2013年3月20日水曜日

Undefined symbols for architecture armv7: "_CMTimeMake", referenced from:



CMTimeMakeを使おうとしたら次のエラーが発生しました。
Undefined symbols for architecture armv7:  "_CMTimeMake", referenced from:

CoreMedia frameworksを追加すると解消します。

2013年3月17日日曜日

Interface Builder was unable to determine the type of ”xxxxx.storyboard“



変なエラーに遭遇しました。
Interface Builder was unable to determine the type of ”xxxxx.storyboard“

結果的には、そのstoryboardが入っているlprojフォルダをいったんdelete=>Remove Referencesで削除し、Add Filesで再度プロジェクトに追加することで解消しました。
snapshotで以前の状態にもどしても同じエラーが発生しました。どうもXcodeがコンパイル対象としている内容とソースコードが違っているように思えるのですが、よくわかりません。

2013年3月2日土曜日

システム定義済みのドキュメントタイプ(UTI)登録


アプリケーションがオープンすることのできるファイルの種類は、plistにUTI(Uniform Type Identifiers)を登録することで設定できます。

mp3、m4aを登録する場合は、plistをProperty Listとしてオープンし、次のように設定します。

この設定を行うとm4a再生アプリ選択肢にこのアプリが追加されます。
例えばメールに添付されたm4aファイルをプレスするとファイルを開くアプリを選択することができますが、これに自分のアプリが含まれるようになります。

上記の設定のplistのSource Codeは次のようになります。
<key>CFBundleDocumentTypes</key>
<array>
    <dict>
        <key>LSItemContentTypes</key>
        <array>
            <string>public.mpeg-4-audio</string>
        </array>
        <key>CFBundleTypeName</key>
        <string>Audio</string>
        <key>CFBundleTypeExtensions</key>
        <array>
            <string>m4a</string>
        </array>
        <key>LSHandlerRank</key>
        <string>Default</string>
    </dict>
</array>

外部アプリから起動またはアクティブにされた場合は、UIApplicationDelegateの次のメソッドが呼ばれので、このファイルをオープンするように準備をします。

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
  sourceApplication:(NSString *)sourceApplication annotation:(id)annotation;

定義済みUTIリスト



2013年2月16日土曜日

Could not instantiate class named NSLayoutConstraint



iOS SDK 6のstoryboardを使用したアプリをiOS 5で動かすと次のようなエラーが発生することがあります。
*** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: 'Could not instantiate class named NSLayoutConstraint'

stroyboardのプロパティーで"Use Autolayout"のチェックを外すと解消します。

NSLayoutConstraintはiOS 6で追加されものなので、それ以前のバージョンにも対応する必要がある場合は使用できません。

[参考リンク]
http://stackoverflow.com/questions/11252057/nslayoutconstraint-crashes-viewcontroller

2013年2月8日金曜日

safari / iPhone5のホーム画面スクリーンサイズ



iPhone5のsafariの画面をホーム画面におくと、スクリーンサイズが360x480になってしまう場合があります。これはそう遠くないうちに改善されると思いますが、当面の対策がBreaking the Mobile Webに紹介されていました。

ポイントはmetaタグで width=device-width または width=32 が設定されているとスクリーンサイズが360x480に設定されてしまうということで、次のように替えることで回避できます。

<meta name="viewport" content="initial-scale=1.0">
<meta name="viewport" content="width=320.1">

4インチ画面の場合だけこの設定にするためには、2行目について次のJavaScriptで適用します。

if (window.screen.height==568) { // iPhone 4"
    document.querySelector("meta[name=viewport]").content="width=320.1";
}


この他iOS 6のHTML、CSS関連の情報が多く紹介されているのでBreaking the Mobile Webを訪問してみてください。


2013年1月5日土曜日

CoreData error コード



CoreDataのModelを変更すると、往々にして次のようなエラーに出くわします。

persistentStoreCoordinator: Error Error Domain=NSCocoaErrorDomain Code=134100 "The operation couldn’t be completed. (Cocoa error 134100.)"

エラーコードのみ表示され詳細がわからないメッセージですが、エラーコードの定義は"Core Data Constants Reference"にまとめられています。
これまで次のエラーが発生しました。

Cocoa error 1560
NSValidationMultipleErrorsError
Error code to denote an error containing multiple validation errors.

このエラー自体は複数の項目でバリデーションエラーが発生したということですが、私の場合は追加した項目のoptionalにチェックが入っていなかったため必須項目となり、既存レコードでは値がnilであるため発生しました。その他データ型の不一致などでも発生するでしょう。

Cocoa error 134100.
NSPersistentStoreIncompatibleVersionHashError
Error code to denote that entity version hashes in the store are incompatible with the current managed object model.

既存レコードとModelの定義に不整合が生じている場合に発生します。
既存レコードを残しModelの定義を変更する場合は"Add Model Version ..."でModelを追加し、既存レコードをコンバートする必要があります。
Core Data 自動マイグレーションを参照してください。