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