Communitygithub.com

JordanCoin/ios-skills-collection

显示和操作 PDF 文档,使用 PDFKit。当需要嵌入 PDFView 显示 PDF 文件、创建或修改 PDFDocument 实例、添加标注(高亮、笔记、签名)、使用 PDFSelection 提取文本、导航页面、生成缩略图、填写 PDF 表单或将 PDFView 包装为 SwiftUI 视图时使用。

ios-skills-collection 是什么?

该技能专为需要在 iOS 应用中处理 PDF 文档的开发者设计,支持 Swift 6.3 / iOS 26+。它提供了一套完整的 PDFKit 操作指南,涵盖从基础的 PDF 显示(使用 PDFView)、文档加载(包括密码保护文档)到高级功能如页面导航、文本搜索与选择、标注添加(高亮、注释、链接等)、缩略图生成以及 SwiftUI 集成。技能文档强调了常见错误(如强制解包、坐标系混淆、线程问题)并提供了检查清单,确保开发者能编写健壮的代码。该工具适用于需要原生 PDF 交互功能的应用,涵盖 iOS、macOS、tvOS 和 visionOS 平台。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/JordanCoin/ios-skills-collection/tree/HEAD/skills/dpearson2699--pdfkit

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

PDFKit

使用 PDFView、PDFDocument、PDFPage、PDFAnnotation 和 PDFSelection 显示、导航、搜索、标注和操作 PDF 文档。目标平台:Swift 6.3 / iOS 26+。

目录

设置

PDFKit 无需任何授权或 Info.plist 条目。

import PDFKit

平台可用性: iOS 11+、iPadOS 11+、Mac Catalyst 13.1+、macOS 10.4+、tvOS 11+、visionOS 1.0+。

显示 PDF

PDFView 是 UIView 的子类,它渲染 PDF 内容,并内置处理缩放、滚动、文本选择和页面导航。

import PDFKit
import UIKit

class PDFViewController: UIViewController {
    let pdfView = PDFView()

    override func viewDidLoad() {
        super.viewDidLoad()
        pdfView.frame = view.bounds
        pdfView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(pdfView)

        pdfView.autoScales = true
        pdfView.displayMode = .singlePageContinuous
        pdfView.displayDirection = .vertical

        if let url = Bundle.main.url(forResource: "sample", withExtension: "pdf") {
            pdfView.document = PDFDocument(url: url)
        }
    }
}

显示模式

模式行为
.singlePage一次显示一页
.singlePageContinuous页面垂直堆叠,可滚动
.twoUp两页并排显示
.twoUpContinuous双页模式并连续滚动

缩放与外观

pdfView.autoScales = true
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.maxScaleFactor = 4.0

pdfView.displaysPageBreaks = true
pdfView.pageShadowsEnabled = true
pdfView.interpolationQuality = .high

加载文档

PDFDocument 可以从 URL、Data 加载,或者创建空文档。

let fileDoc = PDFDocument(url: fileURL)
let dataDoc = PDFDocument(data: pdfData)
let emptyDoc = PDFDocument()

受密码保护的 PDF

guard let document = PDFDocument(url: url) else { return }
if document.isLocked {
    if !document.unlock(withPassword: userPassword) {
        // 显示密码提示
    }
}

保存与页面操作

document.write(to: outputURL)
document.write(to: outputURL, withOptions: [
    .ownerPasswordOption: "ownerPass", .userPasswordOption: "userPass"
])
let data = document.dataRepresentation()

// 页面(0 为起始)
let count = document.pageCount
document.insert(PDFPage(), at: count)
document.removePage(at: 2)
document.exchangePage(at: 0, withPageAt: 3)

页面导航

PDFView 提供内置导航,并支持历史记录。

// 跳转到指定页面
if let page = pdfView.document?.page(at: 5) {
    pdfView.go(to: page)
}

// 顺序导航
pdfView.goToNextPage(nil)
pdfView.goToPreviousPage(nil)
pdfView.goToFirstPage(nil)
pdfView.goToLastPage(nil)

// 检查导航状态
if pdfView.canGoToNextPage { /* ... */ }

// 历史导航
if pdfView.canGoBack { pdfView.goBack(nil) }

// 跳转到页面的指定点
let destination = PDFDestination(page: page, at: CGPoint(x: 0, y: 500))
pdfView.go(to: destination)

监听页面变化

NotificationCenter.default.addObserver(
    self, selector: #selector(pageChanged),
    name: .PDFViewPageChanged, object: pdfView
)

@objc func pageChanged(_ notification: Notification) {
    guard let page = pdfView.currentPage,
          let doc = pdfView.document else { return }
    let index = doc.index(for: page)
    pageLabel.text = "第 \(index + 1) 页,共 \(doc.pageCount) 页"
}

文本搜索和选择

同步搜索

let results: [PDFSelection] = document.findString(
    "搜索词", withOptions: [.caseInsensitive]
)

异步搜索

对大型文档进行后台搜索时使用 PDFDocumentDelegate。实现 didMatchString(_:) 以接收每个匹配结果,并使用 documentDidEndDocumentFind(_:) 获取完成通知。

增量搜索和查找交互

// 从当前选择中查找下一个匹配项
let next = document.findString("搜索词", fromSelection: current, withOptions: [.caseInsensitive])

// 系统查找栏(iOS 16+)
pdfView.isFindInteractionEnabled = true

文本提取

let fullText = document.string                          // 整个文档
let pageText = document.page(at: 0)?.string             // 单页
let attributed = document.page(at: 0)?.attributedString  // 带格式

// 基于区域的提取
if let page = document.page(at: 0) {
    let selection = page.selection(for: CGRect(x: 50, y: 50, width: 400, height: 200))
    let text = selection?.string
}

高亮搜索结果

let results = document.findString("重要", withOptions: [.caseInsensitive])
for selection in results { selection.color = .yellow }
pdfView.highlightedSelections = results

if let first = results.first {
    pdfView.setCurrentSelection(first, animate: true)
    pdfView.go(to: first)
}

标注

使用 PDFAnnotation(bounds:forType:withProperties:) 创建标注并添加到 PDFPage。

高亮标注

func addHighlight(to page: PDFPage, selection: PDFSelection) {
    let highlight = PDFAnnotation(
        bounds: selection.bounds(for: page),
        forType: .highlight, withProperties: nil
    )
    highlight.color = UIColor.yellow.withAlphaComponent(0.5)
    page.addAnnotation(highlight)
}

文本备注标注

let note = PDFAnnotation(
    bounds: CGRect(x: 100, y: 700, width: 30, height: 30),
    forType: .text, withProperties: nil
)
note.contents = "这是一条便签。"
note.color = .systemYellow
note.iconType = .comment
page.addAnnotation(note)

自由文本标注

let freeText = PDFAnnotation(
    bounds: CGRect(x: 50, y: 600, width: 300, height: 40),
    forType: .freeText, withProperties: nil
)
freeText.contents = "添加的注释"
freeText.font = UIFont.systemFont(ofSize: 14)
freeText.fontColor = .darkGray
page.addAnnotation(freeText)

链接标注

let link = PDFAnnotation(
    bounds: CGRect(x: 50, y: 500, width: 200, height: 20),
    forType: .link, withProperties: nil
)
link.url = URL(string: "https://example.com")
page.addAnnotation(link)

// 内部页面链接
link.destination = PDFDestination(page: targetPage, at: .zero)

移除标注

for annotation in page.annotations {
    page.removeAnnotation(annotation)
}

标注子类型参考

子类型常量用途
高亮.highlight文本标记(黄色高亮)
下划线.underline文本标记(下划线)
删除线.strikeOut文本标记(删除线)
文本.text便签图标
自由文本.freeText行内文本块
墨水.ink手绘路径
链接.linkURL 或页面目的地
线条.line带端点的直线
方形.square矩形形状
圆形.circle椭圆形
印章.stamp橡皮图章(如“已批准”)
控件.widget表单元素(文本字段、复选框)

缩略图

PDFThumbnailView

PDFThumbnailView 显示与 PDFView 关联的页面缩略图条。

let thumbnailView = PDFThumbnailView()
thumbnailView.pdfView = pdfView
thumbnailView.thumbnailSize = CGSize(width: 60, height: 80)
thumbnailView.layoutMode = .vertical
thumbnailView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(thumbnailView)

程序化生成缩略图

let thumbnail = page.thumbnail(of: CGSize(width: 120, height: 160), for: .mediaBox)

// 所有页面
let thumbnails = (0..<document.pageCount).compactMap {
    document.page(at: $0)?.thumbnail(of: CGSize(width: 120, height: 160), for: .mediaBox)
}

SwiftUI 集成

将 PDFView 包装为 UIViewRepresentable 以在 SwiftUI 中使用。

import SwiftUI
import PDFKit

struct PDFKitView: UIViewRepresentable {
    let document: PDFDocument

    func makeUIView(context: Context) -> PDFView {
        let pdfView = PDFView()
        pdfView.autoScales = true
        pdfView.displayMode = .singlePageContinuous
        pdfView.document = document
        return pdfView
    }

    func updateUIView(_ pdfView: PDFView, context: Context) {
        if pdfView.document !== document {
            pdfView.document = document
        }
    }
}

使用

struct DocumentScreen: View {
    let url: URL

    var body: some View {
        if let document = PDFDocument(url: url) {
            PDFKitView(document: document)
                .ignoresSafeArea()
        } else {
            ContentUnavailableView("无法加载 PDF", systemImage: "doc.questionmark")
        }
    }
}

关于带页面跟踪、标注点击检测和协调器模式的交互式包装器,请参阅 references/pdfkit-patterns.md。

页面覆盖(iOS 16+)

PDFPageOverlayViewProvider 将 UIKit 视图放置在单个页面之上,用于标准标注之外的交互控件或自定义渲染。

class OverlayProvider: NSObject, PDFPageOverlayViewProvider {
    func pdfView(_ view: PDFView, overlayViewFor page: PDFPage) -> UIView? {
        let overlay = UIView()
        // 添加自定义子视图
        return overlay
    }
}

pdfView.pageOverlayViewProvider = overlayProvider

常见错误

不要:强制解包 PDFDocument 初始化

PDFDocument(url:) 和 PDFDocument(data:) 是可能失败的初始化方法。

// 错误
let document = PDFDocument(url: url)!

// 正确
guard let document = PDFDocument(url: url) else { return }

不要:忘记为 PDFView 设置 autoScales

不设置 autoScales,PDF 将以原始分辨率渲染。

// 错误
pdfView.document = document

// 正确
pdfView.autoScales = true
pdfView.document = document

不要:忽略标注中的 PDF 坐标系

PDF 页面坐标原点在左下角,Y 轴向上——与 UIKit 相反。

// 错误:UIKit 坐标
let bounds = CGRect(x: 50, y: 50, width: 200, height: 30)

// 正确:PDF 坐标(原点左下角)
let pageBounds = page.bounds(for: .mediaBox)
let pdfY = pageBounds.height - 50 - 30
let bounds = CGRect(x: 50, y: pdfY, width: 200, height: 30)

不要:在后台线程修改标注

PDFKit 类不是线程安全的。

// 错误
DispatchQueue.global().async { page.addAnnotation(annotation) }

// 正确
DispatchQueue.main.async { page.addAnnotation(annotation) }

不要:在 UIViewRepresentable 中使用 == 比较 PDFDocument

PDFDocument 是引用类型。应使用身份比较(!==)。

// 错误:总是替换文档
func updateUIView(_ pdfView: PDFView, context: Context) {
    pdfView.document = document
}

// 正确
func updateUIView(_ pdfView: PDFView, context: Context) {
    if pdfView.document !== document {
        pdfView.document = document
    }
}

审查清单

  • PDFDocument 初始化使用可选绑定,而非强制解包
  • 为正确初始显示设置 pdfView.autoScales = true
  • 访问页面索引前检查 pageCount
  • 根据设计配置 displayMode 和 displayDirection
  • 标注使用 PDF 坐标空间(原点左下角,Y 向上)
  • 所有 PDFKit 变更在主线程上执行
  • 使用 isLocked / unlock(withPassword:) 处理受密码保护的 PDF
  • SwiftUI 包装器在 updateUIView 中使用 !== 身份检查
  • 观察 PDFViewPageChanged 通知以跟踪页面
  • PDFThumbnailView.pdfView 与主 PDFView 关联
  • 大型文档搜索使用异步 beginFindString 和代理
  • 保存文档时在需要加密时使用 write(to:withOptions:)

参考

Individual skills in this repo

This repo contains 11 individual skills — each has its own dedicated page.

JordanCoin/ios-skills-collection

Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries. Use when the user asks about GitHub issues, pull requests, workflows, or wants to interact with GitHub repositories from the command line — including tasks like check CI status, create PR, list issues, or query the GitHub API.

JordanCoin/ios-skills-collection

Create media playback experiences using AVKit. Use when adding video players with AVPlayerViewController, enabling Picture-in-Picture, routing media with AirPlay, using SwiftUI VideoPlayer views, configuring transport controls, displaying subtitles and closed captions, or integrating AVFoundation playback with system UI.

JordanCoin/ios-skills-collection

Integrate and optimize Core ML models in iOS apps for on-device machine learning inference. Covers model loading (.mlmodelc, .mlpackage), predictions with auto-generated classes and MLFeatureProvider, compute unit configuration (CPU, GPU, Neural Engine), MLTensor, VNCoreMLRequest, MLComputePlan, multi-model pipelines, and deployment strategies. Use when loading Core ML models, making predictions, configuring compute units, or profiling model performance.

JordanCoin/ios-skills-collection

Implement, review, or improve maps and location features in iOS/macOS apps using MapKit and CoreLocation. Use when working with Map views, annotations, markers, polylines, user location tracking, geocoding, reverse geocoding, search/autocomplete, directions and routes, geofencing, region monitoring, CLLocationUpdate async streams, or location authorization flows. Also use when working with maps, coordinates, addresses, places, directions, distance calculations, or location-based features in Swift apps.

JordanCoin/ios-skills-collection

Implement, review, or improve in-app tips and onboarding using Apple's TipKit framework. Use when adding feature discovery tooltips, onboarding flows, contextual tips, first-run experiences, coach marks, or working with Tip protocol, TipView, popoverTip, tip rules, tip events, or feature education UI.

JordanCoin/ios-skills-collection

When the user wants to optimize their Google Play Store listing — title, short description, full description, keywords, ratings, or Play Store-specific features. Use when the user mentions "Google Play", "Android", "Play Store", "Play Console", "short description", "full description indexed", "Google Play ASO", or wants Google Play-specific keyword, creative, or ratings strategy. For iOS App Store optimization, see aso-audit and metadata-optimization.

JordanCoin/ios-skills-collection

When the user wants to implement, optimize, or use App Clips for app discovery and conversion. Use when the user mentions "App Clip", "app clip code", "mini app", "instant app", "App Clip card", "App Clip link", "no download required", "instant experience", or wants to understand how App Clips appear in App Store search. For general App Store discoverability, see aso-audit. For marketing campaigns, see ua-campaign.

JordanCoin/ios-skills-collection

When the user wants to plan a launch strategy for a new app or major update. Also use when the user mentions "app launch", "launch plan", "launch checklist", "pre-launch", "launch day", or "how to launch my app". For ongoing ASO after launch, see aso-audit. For paid acquisition during launch, see ua-campaign.

JordanCoin/ios-skills-collection

When the user wants to analyze their own app's actual performance data from App Store Connect — real downloads, revenue, IAP, subscriptions, trials, or country breakdowns synced via Appeeky Connect. Use when the user asks about "my downloads", "my revenue", "how is my app performing", "ASC data", "sales and trends", "my subscription numbers", "App Store Connect metrics", or wants to compare periods or top markets. For third-party app estimates, see app-analytics. For subscription analytics depth, see monetization-strategy.

JordanCoin/ios-skills-collection

When the user wants a full ASO health audit, review their App Store listing quality, or diagnose why their app isn't ranking. Also use when the user mentions "ASO audit", "ASO score", "why am I not ranking", "listing review", or "optimize my app store page". For keyword-specific research, see keyword-research. For metadata writing, see metadata-optimization.

JordanCoin/ios-skills-collection

Always use when working on any iOS, macOS, Swift, SwiftUI, Xcode, or Apple platform project. Routes to the correct specialized skills based on development phase and task context. This collection contains 210 skills from 28 authors covering the full iOS lifecycle.

相关技能