关于PDFKit实现的iBook

2,992 阅读3分钟

前言

iOS 11 后苹果在iOS平台开放了PDFKit SDK,该框架主要是用来显示和操作PDF文件的,本次通过学习PDFKit实现了展示PDF内容、缩略图、目录、收藏和搜索等功能。

官方文档

代码 iBook,纯Swift编写,想学习PDFKit,可以点一下Star学习学习。

PDFKit

PDFKit中的文档说明

// Views
PDFView: PDFView用来展示pdf文件
PDFThumbnailView: 通过PDFThumbnailView可以获取到pdf的缩略图
// Model
PDFDocument: 表示PDF数据或PDF文件,并定义写入,搜索和选择PDF数据的方法。
PDFPage: 表示PDF中的页,可以通过PDFPage获取每一页中的相关信息。
PDFOutline: 表示PDF文档结构的树结构层次结构中的元素,可以获取PDF的大纲目录信息。
PDFSelection: 标识PDF文档中文本的连续或非连续选择的文字,表示选择一段文字,在搜索模块可以搜索文字。
// Annotations
PDFAnnotation: 表示PDF文档中的注释。
PDFAction: 激活PDF注释或单击大纲目录时执行的跳转操作
PDFDestination: 描述PDF页面上的跳转目录,在跳转页中使用。
PDFBorder: 可选的注释边框,完全在注释矩形内绘制。

iBook

书库页面获取PDF相关数据, 可以通过KVC获取。

  • PDF书名
if let title = documentAttributes["Title"] as? String {
    cell.title = title
}
  • PDF作者
if let author = documentAttributes["Author"] as? String {
	 cell.author = author
}
  • 获取第一页作为封面,封面已经做了缓存
let thumbnailCache = NSCache<NSURL, UIImage>()
let downloadQueue = DispatchQueue(label: "com.jovins.pdfview.thumbnail")
if let page = document.page(at: 0), let key = document.documentURL as NSURL? {     
    cell.url = key
    if let thumbnail = thumbnailCache.object(forKey: key) {
        cell.image = thumbnail
    } else {
        downloadQueue.async {
            let imgWidth: CGFloat = (UIScreen.main.bounds.width - 48)/2 - 24
            let imgHeight: CGFloat = 190
            let thumbnail = page.thumbnail(of: CGSize(width: imgWidth, height: imgHeight), for: .cropBox)
            self.thumbnailCache.setObject(thumbnail, forKey: key)
            if cell.url == key {
                DispatchQueue.main.async {
                    cell.image = thumbnail
                }
            }
        }
    }
}

初始化一个DocumentModel模型存储PDFDocument数据。

struct DocumentModel {
    
    var title: String = ""
    var author: String = ""
    var coverImage: UIImage?
    var url: URL?
}
class BookManager {
    
    static let shared = BookManager()
    func getDocument(_ pdfDoc: PDFDocument?) -> DocumentModel {
        
        var model = DocumentModel()
        if let document = pdfDoc, let documentAttributes = document.documentAttributes {
            if let title = documentAttributes["Title"] as? String {
                model.title = title
            } else {
                model.title = "No Title"
            }
            
            if let author = documentAttributes["Author"] as? String {
                model.author = author
            } else {
                model.author = "No Author"
            }
            
            if document.pageCount > 0, let page = document.page(at: 0) {
                let imgWidth: CGFloat = (UIScreen.main.bounds.width - 48)/2 - 24
                let imgHeight: CGFloat = 190
                let thumbnail = page.thumbnail(of: CGSize(width: imgWidth, height: imgHeight), for: .cropBox)
                model.coverImage = thumbnail
            }
            
            if let url = document.documentURL {
                model.url = url
            }
        }
        return model
    }
}

PDF浏览

图一是PDF浏览详情页

private lazy var pdfView: PDFView = {  
    let view = PDFView()
    view.backgroundColor = Device.bgColor
    view.autoScales = true
    view.displayMode = .singlePage
    view.displayDirection = .horizontal
    view.usePageViewController(true, withViewOptions: [UIPageViewController.OptionsKey.spineLocation: 20])
    return view
}()
self.pdfView.document = self.document

图二是该PDF的所有页面

/// 该页面的核心代码
if let doc = self.document, let page = doc.page(at: indexPath.item) {   
    let pageNumber = indexPath.item
    cell.pageNumber = pageNumber
    let key = NSNumber(value: pageNumber)
    if let thumbnail = self.thumbnailCache.object(forKey: key) {
        cell.image = thumbnail
    } else {
        let cellSize = CGSize(width: (UIScreen.main.bounds.width - 16 * 4)/3, height: 140)
        downloadQueue.async {
            let thumbnail = page.thumbnail(of: cellSize, for: .cropBox)
            self.thumbnailCache.setObject(thumbnail, forKey: key)
            if cell.pageNumber == pageNumber {
                DispatchQueue.main.async {
                    cell.image = thumbnail
                }
            }
        }
    }
}

图三是该PDF的目录

/// 该页面核心代码
let outline = self.lines[indexPath.item]
cell.titleString = outline.label
cell.pageString = outline.destination?.page?.label

var indentationLevel = -1
var parent = outline.parent
while let _ = parent {
    indentationLevel += 1
    parent = parent?.parent
}
cell.indentationLevel = indentationLevel

图四是浏览时收藏的页面。

/// 获取收藏页
if let documentURL = self.document?.documentURL?.absoluteString, let bookmarks = UserDefaults.standard.array(forKey: documentURL) as? [Int] {
    self.bookmarks = bookmarks
    self.collection.reloadData()
}
// 显示收藏
let pageNumber = self.bookmarks[indexPath.item]
if let page = self.document?.page(at: pageNumber) {
    cell.pageNumber = pageNumber
    let key = NSNumber(value: pageNumber)
    if let thumbnail = self.thumbnailCache.object(forKey: key) {
        cell.image = thumbnail
    } else {
        let cellSize = CGSize(width: (UIScreen.main.bounds.width - 16 * 4)/3, height: 140)
        downloadQueue.async {
            let thumbnail = page.thumbnail(of: cellSize, for: .cropBox)
            self.thumbnailCache.setObject(thumbnail, forKey: key)
            if cell.pageNumber == pageNumber {
                DispatchQueue.main.async {
                    cell.image = thumbnail
                }
            }
        }
    }
}

打印,直接调取系统方法。

let printInteractionController = UIPrintInteractionController.shared
printInteractionController.printingItem = self.document?.dataRepresentation()
printInteractionController.present(animated: true, completionHandler: nil)

阅读历史

在PDF浏览页存储历史记录

private func storgeHistoryList() {
    if let documentURL = self.document?.documentURL?.absoluteString {
        let cache = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].absoluteString
        let key = cache.appending("com.jovins.ibook.storgeHistory")
        if var documentURLs = UserDefaults.standard.array(forKey: key) as? [String] {
            if !documentURLs.contains(documentURL) {
                // 不存在则存储
                documentURLs.append(documentURL)
                UserDefaults.standard.set(documentURLs, forKey: key)
            }
        } else {
            // 第一次存储
            UserDefaults.standard.set([documentURL], forKey: key)
        }
    }
}

获取已浏览的记录

fileprivate func refreshData() {
        
    let cache = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].absoluteString
    let key = cache.appending("com.jovins.ibook.storgeHistory")
    if let documentURLs = UserDefaults.standard.array(forKey: key) as? [String] {
        var urls: [URL] = []
        for str in documentURLs {
            if let url = URL(string: str) {
                urls.append(url)
            }
        }
        self.documents = urls.compactMap { PDFDocument(url: $0) }
        self.tableView.reloadData()
    }
}

搜索

通过PDFDocument设置代理,然后调用beginFindString可以实现搜索功能。

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    let searchText = searchBar.text!.trimmingCharacters(in: CharacterSet.whitespaces)
    if searchText.count < 3 {
        return
    }
    self.searchResults.removeAll()
    self.tableView.reloadData()
    if let document = self.document {
        document.cancelFindString()
        document.delegate = self
        document.beginFindString(searchText, withOptions: .caseInsensitive)
    }
}
/// 代理实现的方法,可以获取到搜索结果
func didMatchString(_ instance: PDFSelection) {
    self.searchResults.append(instance)
    self.tableView.reloadData()
}

未来

未来会继续增加txtepub格式的相关功能。