PDFKit은(는) 무엇을 하나요?
PDFView, PDFDocument, PDFPage, PDFAnnotation, PDFSelection을 사용하여 PDF 문서를 표시, 탐색, 검색, 주석 및 조작합니다.
Swift 6.3 / iOS 26+를 대상으로 합니다.
목차
설정
PDFKit은 entitlements나 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-based)
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(
"search term", withOptions: [.caseInsensitive]
)
비동기 검색
방대한 문서의 백그라운드 검색에는 PDFDocumentDelegate를 사용하세요.
각 일치 항목을 수신하려면 didMatchString(_:)를 구현하고,
완료를 처리하려면 documentDidEndDocumentFind(_:)를 구현합니다.
증분 검색 및 찾기 상호 작용
// 현재 선택 항목에서 다음 일치 항목 찾기
let next = document.findString("term", 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("important", 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 | 자유형 그리기 경로 |
| 링크 | .link | URL 또는 페이지 대상 |
| 선 | .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 통합
SwiftUI에서 사용하기 위해 PDFView를 UIViewRepresentable로 래핑합니다.
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는 참조 타입입니다. !==(identity)를 사용하세요.
// 잘못된 방법: 항상 문서를 교체
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 변경 작업이 메인 스레드에서 수행됨
- 암호로 보호된 PDF가
isLocked/unlock(withPassword:)로 처리됨 - SwiftUI 래퍼가
updateUIView에서!==identity 검사를 사용함 -
PDFViewPageChanged알림이 페이지 추적을 위해 관찰됨 -
PDFThumbnailView.pdfView가 메인PDFView에 연결됨 - 대용량 문서 검색이 델리게이트와 함께
beginFindString을 사용함 - 저장된 문서가 암호화가 필요할 때
write(to:withOptions:)를 사용함
참고 자료
- 확장 패턴 (폼, 워터마크, 병합, 인쇄, 오버레이, 개요, 사용자 지정 그리기): references/pdfkit-patterns.md
- PDFKit 프레임워크
- PDFView
- PDFDocument
- PDFPage
- PDFAnnotation
- PDFSelection
- PDFThumbnailView
- PDFPageOverlayViewProvider
- PDF 문서에 위젯 추가하기
- PDF에 사용자 지정 그래픽 추가하기