【iOS开发】打印能力调研&封装

962 阅读6分钟

零、百度网盘、WPS 等APP的实现

APP分析PDF文件Word 文档PPT文档Excel文档TXT文件打印选项打印界面
百度网盘iOS v11.24.0自身不提供打印功能,须调起小程序进行操作imgimgimgimgimgimgimg
WPS OfficeiOS v11.25.0仅不支持 PPT文档的打印,此外的 PDF、Word、Excel、TXT 等格式的文档,均通过调起系统的 AirPrint 实现打印imgimgimgimgimgimg

一、使用 “隔空打印”(AirPrint)

(一)关于“隔空打印”

  • “隔空打印”是一项 Apple 技术,在 iOS 4.2+ 可帮助您创建无损打印输出,而无需下载或安装驱动程序。
  • 通过“隔空打印”技术,您可以轻松地从 Mac、iPhone、iPad 或 iPod touch 打印无损照片和文稿,而无需安装其他软件(驱动程序)。
  • “隔空打印”的特点包括:容易发现、自动选择媒介以及企业级印后处理选项等。
  • 大多数热门打印机型号(包括官网中列出的打印机打印服务器)都内建了“隔空打印”功能。
  • 这些信息由各生产企业提供,Apple 会定期进行更新。如果您使用的机型没有在本文中列出,请联系生产企业以了解更多信息。

(二)效果演示

AirPrint 能力监测打印纯文本打印富文本打印HTML内容打印PDF文件打印网页打印TXT文件打印Word文件打印Excel文件打印PPT文件
imgimgimgimgimgimgimgimgimgimg

各演示,具体编码实现如下。

(三)编码实现

注:为精简示例,仅贴出各示例的核心代码

1、AirPrint 能力检测

if (UIPrintInteractionController.printingAvailable) {
    [MBProgressHUD showSuccessMessage:@"当前设备支持打印"];
} else {
    [MBProgressHUD showErrorMessage:@"当前设备不支持打印"];
}

2、打印纯文本

UISimpleTextPrintFormatter *printFormatter = [UISimpleTextPrintFormatter.alloc initWithText:text];
printFormatter.startPage = 0; //指定从哪一张开始打印0代表第一张
printFormatter.contentInsets = UIEdgeInsetsMake(36, 36, 36, 36); //72相当于1英寸,这样设置上下左右的边距都为0.5英寸
printFormatter.maximumContentWidth = 504; //(72x7.5)相当于打印宽度为7英寸

UIPrintInteractionController *printController = [UIPrintInteractionController sharedPrintController];
printController.delegate = weakSelf;
printController.printFormatter = printFormatter;
printController.printInfo = [UIPrintInfo bde_printInfoWithJobName:title];
[printController bde_presentFromView:sender animated:YES completionHandler:^(UIPrintInteractionController * _Nonnull printInteractionController, BOOL completed, NSError * _Nullable error) {
    [weakSelf printInteractionController:printInteractionController completed:completed withError:error];
}];

3、打印富文本

UISimpleTextPrintFormatter *printFormatter = [UISimpleTextPrintFormatter.alloc initWithAttributedText:text];
printFormatter.startPage = 0; //指定从哪一张开始打印0代表第一张
printFormatter.contentInsets = UIEdgeInsetsMake(36, 36, 36, 36); //72相当于1英寸,这样设置上下左右的边距都为0.5英寸
printFormatter.maximumContentWidth = 504; //(72x7.5)相当于打印宽度为7英寸

UIPrintInteractionController *printController = [UIPrintInteractionController sharedPrintController];
printController.delegate = weakSelf;
printController.printFormatter = printFormatter;
printController.printInfo = [UIPrintInfo bde_printInfoWithJobName:title];
[printController bde_presentFromView:sender animated:YES completionHandler:^(UIPrintInteractionController * _Nonnull printInteractionController, BOOL completed, NSError * _Nullable error) {
    [weakSelf printInteractionController:printInteractionController completed:completed withError:error];
}];

4、打印HTML内容

NSString *text = [NSString stringWithFormat:@"<!DOCTYPE html><html><body><p><h1>Hello World</h1></body></html>"];
UIMarkupTextPrintFormatter *printFormatter = [UIMarkupTextPrintFormatter.alloc initWithMarkupText:text];

UIPrintInteractionController *printController = [UIPrintInteractionController sharedPrintController];
printController.delegate = weakSelf;
printController.printFormatter = printFormatter;
printController.printInfo = [UIPrintInfo bde_printInfoWithJobName:sender.currentTitle];
[printController bde_presentFromView:sender animated:YES completionHandler:^(UIPrintInteractionController * _Nonnull printInteractionController, BOOL completed, NSError * _Nullable error) {
    [weakSelf printInteractionController:printInteractionController completed:completed withError:error];
}];

5、打印PDF文件

NSString *fileName = @"示例.pdf";
NSURL *fileURL = [NSBundle.mainBundle URLForResource:fileName withExtension:nil];

UIPrintInteractionController *printController = [UIPrintInteractionController sharedPrintController];
printController.delegate = weakSelf;
printController.printingItem = fileURL;
printController.printInfo = [UIPrintInfo bde_printInfoWithJobName:fileName];
[printController bde_presentFromView:sender animated:YES completionHandler:^(UIPrintInteractionController * _Nonnull printInteractionController, BOOL completed, NSError * _Nullable error) {
    [weakSelf printInteractionController:printInteractionController completed:completed withError:error];
}];

6、打印网页

// 当网页已加载完成
__weak __typeof__(self)weakSelf = self;
UIPrintInteractionController *printController = [UIPrintInteractionController sharedPrintController];
printController.delegate = self;
printController.printFormatter = [self.webView viewPrintFormatter];
printController.printInfo = [UIPrintInfo bde_printInfoWithJobName:self.webView.URL.absoluteString];
[printController bde_presentFromView:sender animated:YES completionHandler:^(UIPrintInteractionController * _Nonnull printInteractionController, BOOL completed, NSError * _Nullable error) {
    [weakSelf printInteractionController:printInteractionController completed:completed withError:error];
}];

7、打印TXT文件

// 1、通过 WebView 加载TXT文件内容
NSString *path = [NSBundle.mainBundle pathForResource:fileName ofType:nil];
NSURL *URL = [NSURL fileURLWithPath:path];
[self.webView loadFileURL:URL allowingReadAccessToURL:URL];

// 2、在 WebView 完成加载时 调起打印
__weak __typeof__(self)weakSelf = self;
UIPrintInteractionController *printController = [UIPrintInteractionController sharedPrintController];
printController.delegate = self;
printController.printFormatter = [self.webView viewPrintFormatter];
printController.printInfo = [UIPrintInfo bde_printInfoWithJobName:self.webView.URL.absoluteString];
[printController bde_presentFromView:sender animated:YES completionHandler:^(UIPrintInteractionController * _Nonnull printInteractionController, BOOL completed, NSError * _Nullable error) {
    [weakSelf printInteractionController:printInteractionController completed:completed withError:error];
}];

提示:可对 -loadFileURL:allowingReadAccessToURL: 方法进行封装,已处理乱码、换行 等问题

// BDEPrintWebView : WKWebView
- (WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL {
    if ([URL.lastPathComponent.pathExtension.lowercaseString isEqualToString:@"txt"]) {
        NSError *error;
        NSString *string = [NSString stringWithContentsOfURL:URL encoding:NSUTF8StringEncoding error:&error];
        if (!error && string.length) {
            string = [string stringByReplacingOccurrencesOfString:@"\n" withString:@"<br/>"];
            if (!self.jobName) self.jobName = [self jobNameFromURL:URL];
            return [self loadHTMLString:string baseURL:nil];
        }
    }
    return [super loadFileURL:URL allowingReadAccessToURL:readAccessURL];
}

8、打印Office文档

因为 AirPrint 默认仅支持 “静态图片、PDF文件” 进行打印(原文如下),对于这之外的文件,可以借用 WebView 实现,即「通过 WebView 打开,然后以 “打印网页” 的方式进行打印」。

More importantly, the print interaction controller lets your app provide the content to be printed. The UIPrintInteractionController class provides three distinct ways to print content:

  • Static images or PDFs. For simple content, you can use the print interaction controller’s printingItemor printingItems properties to provide an image (in various formats), a PDF file, or an array of images and PDF files.
  • Print formatters. If you need to print text and HTML content with automatic reflow, you can assign an instance of any of the built-in print formatter classes to the print interaction controller’s printFormatter property.
  • Page renderers. Page renderers let you provide your own drawing routines for custom content, and give you complete control over the page layout, including headers and footers. To use a page renderer, you must first write a page renderer subclass, then assign an instance of it to the print interaction controller’s printPageRenderer property.

考虑到,该过程需要创建一个 WebView 实例,并实现相关代理方法,为简化操作,故对其进行封装,具体的使用、封装实现 如下。

(1)封装的使用:打印 TXT、Word、PPT、PDF、Excel 文件

NSArray<NSString *> *fileNames = @[
    @"背影.txt",
    @"性能测试——压力测试、负载测试.docx",
    @"软件测试重点总结.ppt",
    @"常见测试点——精选推荐.pdf",
    @"21种常用测试方法汇总.xls"
];

__weak __typeof__(self)weakSelf = self;
[fileNames enumerateObjectsUsingBlock:^(NSString * _Nonnull fileName, NSUInteger idx, BOOL * _Nonnull stop) {
    [self.stackView addArrangedButton:fileName controlEvents:UIControlEventTouchUpInside block:^(UIButton *sender) {
        NSString *path = [NSBundle.mainBundle pathForResource:fileName ofType:nil];
        NSURL *URL = [NSURL fileURLWithPath:path];
        
        BDEPrintWebView *printWebView = [BDEPrintWebView.alloc init];
        [printWebView setDidFinishNavigationCallback:^(BDEPrintWebView *webView, WKNavigation *navigation) {
            UIPrintInteractionController *printController = [UIPrintInteractionController sharedPrintController];
            printController.delegate = weakSelf;
            printController.printFormatter = webView.viewPrintFormatter;
            printController.printInfo = [UIPrintInfo bde_printInfoWithJobName:webView.jobName];
            [printController bde_presentFromView:sender animated:YES completionHandler:^(UIPrintInteractionController * _Nonnull printInteractionController, BOOL completed, NSError * _Nullable error) {
                [weakSelf printInteractionController:printInteractionController completed:completed withError:error];
            }];
        }];
        [printWebView loadFileURL:URL allowingReadAccessToURL:URL];
        UIPrintInteractionController.bde_sharedPrintWebView = printWebView;
    }];
}];

(2)封装的实现

//
//  UIPrintInteractionController+BDEPrintWebView.h
//  BDEExtension
//
//  Created by 孟昕欣 on 2022/6/2.
//

#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>
@class BDEPrintWebView;

typedef void(^BDEPrintWebViewDidStartProvisionalNavigationCallback)(BDEPrintWebView *webView, WKNavigation *navigation);
typedef void(^BDEPrintWebViewDidFinishNavigationCallback)(BDEPrintWebView *webView, WKNavigation *navigation);
typedef void(^BDEPrintWebViewDidFailProvisionalNavigationCallback)(BDEPrintWebView *webView, WKNavigation *navigation);

/// 辅助打印的 WKWebView 子类
@interface BDEPrintWebView : WKWebView <WKUIDelegate, WKNavigationDelegate>
@property (nonatomic, copy, readwrite, nullable) NSString *jobName;
@property (nonatomic, copy, readwrite, nullable) BDEPrintWebViewDidStartProvisionalNavigationCallback didStartProvisionalNavigationCallback;
@property (nonatomic, copy, readwrite, nullable) BDEPrintWebViewDidFinishNavigationCallback didFinishNavigationCallback;
@property (nonatomic, copy, readwrite, nullable) BDEPrintWebViewDidFailProvisionalNavigationCallback didFailProvisionalNavigationCallback;
@end

#pragma mark -

@interface UIPrintInteractionController (BDEPrintWebView)

/// 辅助打印的 BDEPrintWebView 实例
@property (nonatomic, strong, readwrite, nullable, class) BDEPrintWebView *bde_sharedPrintWebView;

@end

//
//  UIPrintInteractionController+BDEPrintWebView.m
//  BDEExtension
//
//  Created by 孟昕欣 on 2022/6/2.
//

#import "UIPrintInteractionController+BDEPrintWebView.h"

@implementation BDEPrintWebView

#pragma mark - Init Methods

- (void)dealloc {
    
}

- (instancetype)initWithFrame:(CGRect)frame configuration:(nonnull WKWebViewConfiguration *)configuration {
    if (!configuration) {
        WKPreferences *preferences = [WKPreferences.alloc init];
        preferences.javaScriptEnabled = YES;
        preferences.javaScriptCanOpenWindowsAutomatically = YES; //不通过用户交互,是否可以打开窗口
        
        configuration = [WKWebViewConfiguration.alloc init];
        configuration.selectionGranularity = WKSelectionGranularityDynamic;
        configuration.allowsInlineMediaPlayback = NO;
        configuration.preferences = preferences;
    }
    
    if (self = [super initWithFrame:frame configuration:configuration]) {
        self.UIDelegate = self;
        self.navigationDelegate = self;
    }
    return self;
}

#pragma mark - Getters & Setters

- (NSString *)jobName {
    if (!_jobName) {
        _jobName = [self jobNameFromURL:self.URL];
    }
    return _jobName;
}

#pragma mark - Data & Networking

#pragma mark - Layout Subviews

#pragma mark - Public Methods

- (WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL {
    if ([URL.lastPathComponent.pathExtension.lowercaseString isEqualToString:@"txt"]) {
        NSError *error;
        NSString *string = [NSString stringWithContentsOfURL:URL encoding:NSUTF8StringEncoding error:&error];
        if (!error && string.length) {
            string = [string stringByReplacingOccurrencesOfString:@"\n" withString:@"<br/>"];
            if (!self.jobName) self.jobName = [self jobNameFromURL:URL];
            return [self loadHTMLString:string baseURL:nil];
        }
    }
    return [super loadFileURL:URL allowingReadAccessToURL:readAccessURL];
}

#pragma mark - Private Methods

#pragma mark - Notifications

#pragma mark - KVO

#pragma mark - Protocol

#pragma mark WKNavigationDelegate

// 页面开始加载
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation{
    !self.didStartProvisionalNavigationCallback ?: self.didStartProvisionalNavigationCallback(webView, navigation);
}

// 页面加载完成
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
    !self.didFinishNavigationCallback ?: self.didFinishNavigationCallback(webView, navigation);
}

// 页面加载失败
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation{
    !self.didFailProvisionalNavigationCallback ?: self.didFailProvisionalNavigationCallback(webView, navigation);
}

#pragma mark - Overrides

#pragma mark - Helper Methods

- (NSString *)jobNameFromURL:(NSURL *)URL {
    NSString *jobName;
    if (!URL.absoluteString.length){
        jobName = nil;
    } else if (URL.isFileURL) {
        jobName = URL.lastPathComponent;
    } else if (URL.path.length > 1) {
        jobName = URL.path.lastPathComponent;
    }
    return jobName;
}

@end


#pragma mark -


@implementation UIPrintInteractionController (BDEPrintWebView)

static BDEPrintWebView *_sharedPrintWebView;

+ (BDEPrintWebView *)bde_sharedPrintWebView {
    return _sharedPrintWebView;
}

+ (void)setBde_sharedPrintWebView:(BDEPrintWebView *)bde_sharedPrintWebView {
    _sharedPrintWebView = bde_sharedPrintWebView;
}

@end

二、调起三方APP进行打印

可以通过调起系统的分享页(UIActivityViewController),选择支持打印功能的第三方APP进行打开,并完成打印。

打印PDF文件打印TXT文件打印Word文件打印Excel文件打印PPT文件
imgimgimgimgimg

示例代码如下:

NSString *fileName = @"file.pdf";
NSURL *fileURL = [NSBundle.mainBundle URLForResource:fileName withExtension:nil];

UIActivityViewController *activityViewController = [UIActivityViewController.alloc initWithActivityItems:@[fileURL] applicationActivities:nil];
[self presentViewController:activityViewController animated:YES completion:nil];

注:目前最新版iOS端 微信(v8.0.20)在如下入口 暂不提供“打印”选项

  • 消息列表 - 长按文件 - 浮层菜单
  • 任意方式 打开文件 - 右上角“···” - 浮层菜单

附、参考文档