iOS 开发之 pdf 文档的加载与浏览的 4 种方式

时间:2024-03-22 14:57:40

前言

在我们的开发中,有些像电子书类型的app的开发会涉及到pdf文档的加载与展示。由于笔者项目中正好涉及到这块,于是将pdf常用的几种加载方式做个总结。以供后面可能用到的同学做个参考。

正文

通常我们用到的pdf文档的加载方式有4种:

  • UIWebView加载本地或者网络pdf文档
  • QLPreviewController加载pdf文档
  • 用CGContext画pdf文档,并结合UIPageViewController展示
  • 第三方框架vfr/Reader加载pdf文档

下面就按照上面4种方式的顺序依次介绍具体的用法。

UIWebView加载本地或者网络pdf文档

UIWebView加载pdf文档比较简单,加载本地文档和网络文档用法几乎差不多。
浏览方式是上下拖动,支持放大缩小,以及选择copy等。
加载本地文档:

    //初始化myWebView
    UIWebView *myWebView = [[UIWebView alloc] init];
    myWebView.backgroundColor = [UIColor whiteColor];
    NSURL *filePath = [NSURL URLWithString:[[NSBundle mainBundle] pathForResource:@"myHome" ofType:@"pdf"]];
    NSURLRequest *request = [NSURLRequest requestWithURL: filePath];
    [myWebView loadRequest:request];
    //使文档的显示范围适合UIWebView的bounds
    [myWebView setScalesPageToFit:YES];

加载网络文档:

//初始化myWebView
    UIWebView *myWebView = [[UIWebView alloc] init];
    myWebView.backgroundColor = [UIColor whiteColor];
    NSURL *filePath = [NSURL URLWithString:@"https://www.tutorialspoint.com/ios/ios_tutorial.pdf"];
    NSURLRequest *request = [NSURLRequest requestWithURL: filePath];
    [myWebView loadRequest:request];
    //使文档的显示范围适合UIWebView的bounds
    [myWebView setScalesPageToFit:YES];

QLPreviewController加载pdf文档

在iOS 4 SDK之后苹果退出了QLPreviewControllerAPI,组件允许用户浏览许多不同的文件类型,如XLS文件,Word文档文件,PDF文件等,但是使用此API之前用户必须导入QuickLook.framework框架,使用的QLPreviewController时,你必须实现此协议QLPreviewControllerDataSource的两个代理方法。
上下滑动支持单个文档的浏览,左右滑动支持不同文档间的切换,还支持苹果自带的分享打印等。
QLPreviewControllerDataSource的两个代理方法:

/*
 *所要加载pdf文档的个数
 */
- (NSInteger)numberOfPreviewItemsInPreviewController:(QLPreviewController *)controller;

/*
 * 返回每个index pdf文档所对应的文档路径
 */
- (id <QLPreviewItem>)previewController:(QLPreviewController *)controller previewItemAtIndex:(NSInteger)index;

QLPreviewController加载pdf文档

//QLPreviewController初始化,需要导入QuickLook.framework
QLPreviewController *QLPVC = [[QLPreviewController alloc] init];
QLPVC.dataSource = self;
[self presentViewController:QLPVC animated:YES completion:nil];

#pragma mark QLPreviewControllerDataSource
- (NSInteger)numberOfPreviewItemsInPreviewController:(QLPreviewController *)controller{
    return 2;
}
- (id<QLPreviewItem>)previewController:(QLPreviewController *)controller previewItemAtIndex:(NSInteger)index{
    NSArray *arr = @[FILE_PATH,FILE_PATH1];

    return [NSURL fileURLWithPath:arr[index]];
}

用CGContext画pdf文档,并结合UIPageViewController展示

首先将pdf单页的文档画在UIView的画布上:

//CFURLRef pdfURL = CFBundleCopyResourceURL(CFBundleGetMainBundle(), CFSTR("test.pdf"), NULL, NULL);
    CFURLRef pdfURL = CFBundleCopyResourceURL(CFBundleGetMainBundle(), (__bridge CFStringRef)self.fileName, NULL, NULL);
//创建CGPDFDocument对象
CGPDFDocumentRef pdfDocument = CGPDFDocumentCreateWithURL((CFURLRef)pdfURL);

//获取当前的上下文
CGContextRef *context = UIGraphicsGetCurrentContext();
//Quartz坐标系和UIView坐标系不一样所致,调整坐标系,使pdf正立
CGContextTranslateCTM(context, 0.0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

 //获取指定页的pdf文档
CGPDFPageRef page = CGPDFDocumentGetPage(pdfDocument, pageNO);
//创建一个仿射变换,该变换基于将PDF页的BOX映射到指定的矩形中。
CGAffineTransform pdfTransform = CGPDFPageGetDrawingTransform(page, kCGPDFCropBox, self.bounds, 0, true);
CGContextConcatCTM(context, pdfTransform);
//将pdf绘制到上下文中
CGContextDrawPDFPage(context, page);

用UIPageViewController展示分页的pdf文档

//初始化PDFPageModel
pdfPageModel = [[CGContextDrawPDFPageModel alloc] initWithPDFDocument:pdfDocument];

// UIPageViewControllerSpineLocationMin 单页显示    
NSDictionary *options = [NSDictionary dictionaryWithObject:
                             [NSNumber numberWithInteger: UIPageViewControllerSpineLocationMin]
                                                        forKey: UIPageViewControllerOptionSpineLocationKey];

//初始化UIPageViewController,UIPageViewControllerTransitionStylePageCurl翻页效果,UIPageViewControllerNavigationOrientationHorizontal水平方向翻页
pageViewCtrl = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl                                              navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
                                                                 options:options];
//承载pdf每页内容的控制器
CGContextDrawPDFPageController *initialViewController = [pdfPageModel viewControllerAtIndex:1];
 NSArray *viewControllers = [NSArray arrayWithObject:initialViewController];
//设置UIPageViewController的数据源
 [pageViewCtrl setDataSource:pdfPageModel];

   //pageViewCtrl.doubleSided = YES;设置正反面都有文字
   //设置pageViewCtrl的子控制器 
   [pageViewCtrl setViewControllers:viewControllers
                           direction:UIPageViewControllerNavigationDirectionReverse
                            animated:NO
                          completion:^(BOOL f){}];
    [self addChildViewController:pageViewCtrl];
    [self.view addSubview:pageViewCtrl.view];
    //当我们向我们的视图控制器容器(就是父视图控制器,它调用addChildViewController方法加入子视图控制器,它就成为了视图控制器的容器)中添加(或者删除)子视图控制器后,必须调用该方法,告诉iOS,已经完成添加(或删除)子控制器的操作。
    [pageViewCtrl didMoveToParentViewController:self];

//CGContextDrawPDFPageModel.m
//获得pdfDocument的总页数
long pageSum = CGPDFDocumentGetNumberOfPages(pdfDocument);

#pragma mark返回pageViewController当前页前一页的代理方法(如果要每页的背面显示与正面相同的风格,而不是默认的白,需要设置pageController的doubleSide属性为YES,同时在下面的两个代理方法中设置BackViewController)
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {

    NSUInteger index = [self indexOfViewController: (CGContextDrawPDFPageController *)viewController];
    if ((index == 1) || (index == NSNotFound)) {
        return nil;
    }
    index--;
    return [self viewControllerAtIndex:index];
}
#pragma mark返回pageViewController当前页后一页的代理方法
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {

    NSUInteger index = [self indexOfViewController: (CGContextDrawPDFPageController *)viewController];
    if (index == NSNotFound) {
        return nil;
    }
    index++;
    //获取pdf文档的页数
    long pageSum = CGPDFDocumentGetNumberOfPages(pdfDocument);
    if (index >= pageSum+1) {
        return nil;
    }
    return [self viewControllerAtIndex:index];
}

也许我们在平时会注意到,一些电子书阅读器的翻页过程中会有白天模式和夜间模式,而UIPageViewController默认的翻页效果如下:

iOS 开发之 pdf 文档的加载与浏览的 4 种方式

翻页默认效果.png

如果黑夜模式也是这种默认的效果如图就会很尴尬:

iOS 开发之 pdf 文档的加载与浏览的 4 种方式

黑夜模式.jpeg

为了解决这种问题:
需要将UIPageViewController的doubleSided属性设为YES,然后将当前视图截图放在每页的背面这样翻页的过程中背面的效果就和相应的模式对应了。
主要修改两个地方:
第一:为设置背面的视图新建一个控制器,同时在控制器上加载一个UIImageView,图片设置为图书当前页的截图,具体实现如下:

- (void)updateWithViewController:(UIViewController *)viewController {
    self.backgroundImage = [self captureView:viewController.view];
}

- (UIImage *)captureView:(UIView *)view {
    CGRect rect = view.bounds;
    UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0.0f);
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGAffineTransform transform = CGAffineTransformMake(-1.0, 0.0, 0.0, 1.0, rect.size.width, 0.0);
    CGContextConcatCTM(context,transform);
    [view.layer renderInContext:context];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

第二:在UIPageViewController的dataSource的代理方法中,设置背页为放截图的控制器。

#pragma mark如果要每页的背面显示与正面相同的风格,而不是默认的白,需要设置pageController的doubleSide属性为YES,同时在下面的两个代理方法中设置BackViewController
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {

    if([viewController isKindOfClass:[CGContextDrawPDFPageController class]]) {
        self.currentViewController = viewController;

        BackViewController *backViewController = [[BackViewController alloc] init];
        [backViewController updateWithViewController:viewController];
        return backViewController;
    }

//self.currentViewController保存的是后一个CGContextDrawPDFPageController,如果直接用viewController实际指的是backViewController,而其没有indexOfViewController:等方法程序会崩掉。
    NSUInteger index = [self indexOfViewController: (CGContextDrawPDFPageController *)self.currentViewController];
    if ((index == 1) || (index == NSNotFound)) {
        return nil;
    }
    index--;
    return [self viewControllerAtIndex:index];
}

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

    if([viewController isKindOfClass:[CGContextDrawPDFPageController class]]) {
        self.currentViewController = viewController;

        BackViewController *backViewController = [[BackViewController alloc] init];
        [backViewController updateWithViewController:viewController];
        return backViewController;
    }

//self.currentViewController保存的是前一个CGContextDrawPDFPageController,如果直接用viewController实际指的是backViewController,而其没有indexOfViewController:等方法程序会崩掉。
    NSUInteger index = [self indexOfViewController: (CGContextDrawPDFPageController *)self.currentViewController];
    if (index == NSNotFound) {
        return nil;
    }
    index++;
    //获取pdf文档的页数
    long pageSum = CGPDFDocumentGetNumberOfPages(pdfDocument);
    if (index >= pageSum+1) {
        return nil;
    }
    return [self viewControllerAtIndex:index];
}

第三方框架vfr/Reader加载pdf文档

使用第三方框架vfr/Reader加载pdf文档非常简单易用,集成了打印,分享,发邮件,预览等多种功能,直接上代码如下:

//Reader初始化 加载本地pdf文件
            ReaderDocument *doc = [[ReaderDocument alloc] initWithFilePath:FILE_PATH password:nil];
            ReaderViewController *rederVC = [[ReaderViewController alloc] initWithReaderDocument:doc];
            rederVC.delegate = self;
            rederVC.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
            rederVC.modalPresentationStyle = UIModalPresentationOverFullScreen;
            [self presentViewController:rederVC animated:YES completion:nil];

#pragma mark ReaderViewControllerDelegate因为PDF阅读器可能是push出来的,也可能是present出来的,为了更好的效果,这个代理方法可以实现很好的退出
- (void)dismissReaderViewController:(ReaderViewController *)viewController{
    [self dismissViewControllerAnimated:YES completion:nil];
}