๐ PDFKit
โญ Difficulty: โญโญ
โฑ๏ธ Est. Time: 1-2h
๐ Graphics & Media
Complete solution for PDF display, annotations, search, and creation
iOS 11+macOS Supported
โจ PDFKit is?
PDFKit is a powerful framework for displaying, editing, and creating PDF documents. It provides everything needed for PDF work โ page navigation, zoom, search, annotations, thumbnails and more. Available in both SwiftUI and UIKit, it's essential for building document viewer apps, ebook readers, and annotation tools.
๐ก Key Features: PDF Display ยท Page Navigation ยท Search ยท Annotations ยท Thumbnails ยท PDF Generation ยท Text Selection ยท Zoom ยท Bookmarks
๐ฏ 1. PDF ํ์ ๊ธฐ๋ณธ
PDFView๋ฅผ ์ฌ์ฉํ์ฌ PDF ๋ฌธ์๋ฅผ ํ์.
PDFViewerView.swift โ Basic PDF Viewer
import SwiftUI import PDFKit struct PDFViewerView: View { let pdfURL: URL var body: some View { PDFKitView(url: pdfURL) .ignoresSafeArea() } } struct PDFKitView: UIViewRepresentable { let url: URL func makeUIView(context: Context) -> PDFView { let pdfView = PDFView() // PDF ๋ฌธ์ ๋ก๋ if let document = PDFDocument(url: url) { pdfView.document = document } // ๊ธฐ๋ณธ ์ค์ pdfView.autoScales = true pdfView.displayMode = .singlePageContinuous pdfView.displayDirection = .vertical return pdfView } func updateUIView(_ uiView: PDFView, context: Context) {} } // ์ฌ์ฉ ์์ struct ContentView: View { var body: some View { if let url = Bundle.main.url(forResource: "sample", withExtension: "pdf") { PDFViewerView(pdfURL: url) } } }
๐ 2. ํ์ด์ง ๋ค๋น๊ฒ์ด์
PDF ํ์ด์ง ๊ฐ ์ด๋๊ณผ ์ธ๋ค์ผ ํ์ implementation.
PDFNavigationView.swift โ Page Navigation
import SwiftUI import PDFKit struct PDFNavigationView: View { let url: URL @State private var pdfView = PDFView() @State private var currentPage: Int = 1 @State private var totalPages: Int = 0 @State private var showThumbnails = false var body: some View { NavigationStack { VStack(spacing: 0) { PDFViewWrapper( pdfView: $pdfView, url: url, currentPage: $currentPage, totalPages: $totalPages ) // ํ์ด์ง ์ปจํธ๋กค HStack { Button { goToPreviousPage() } label: { Image(systemName: "chevron.left") } .disabled(currentPage <= 1) Text("\(currentPage) / \(totalPages)") .font(.subheadline) .frame(maxWidth: .infinity) Button { goToNextPage() } label: { Image(systemName: "chevron.right") } .disabled(currentPage >= totalPages) } .padding() .background(Color(uiColor: .systemBackground)) .border(Color.gray.opacity(0.2), width: 1) } .navigationTitle("PDF ๋ทฐ์ด") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { showThumbnails.toggle() } label: { Image(systemName: "square.grid.2x2") } } } .sheet(isPresented: $showThumbnails) { PDFThumbnailView(pdfView: pdfView) } } } func goToPreviousPage() { guard let currentPage = pdfView.currentPage, let previousPage = pdfView.document?.page(at: currentPage.pageRef!.pageNumber - 2) else { return } pdfView.go(to: previousPage) } func goToNextPage() { guard let currentPage = pdfView.currentPage, let nextPage = pdfView.document?.page(at: currentPage.pageRef!.pageNumber) else { return } pdfView.go(to: nextPage) } } struct PDFViewWrapper: UIViewRepresentable { @Binding var pdfView: PDFView let url: URL @Binding var currentPage: Int @Binding var totalPages: Int func makeUIView(context: Context) -> PDFView { let document = PDFDocument(url: url) pdfView.document = document totalPages = document?.pageCount ?? 0 // ํ์ด์ง ๋ณ๊ฒฝ ๊ฐ์ง NotificationCenter.default.addObserver( forName: .PDFViewPageChanged, object: pdfView, queue: .main ) { _ in updateCurrentPage() } return pdfView } func updateUIView(_ uiView: PDFView, context: Context) {} func updateCurrentPage() { if let page = pdfView.currentPage, let index = pdfView.document?.index(for: page) { currentPage = index + 1 } } }
๐ 3. PDF ๊ฒ์
PDF ๋ฌธ์ ๋ด์์ ํ ์คํธ๋ฅผ ๊ฒ์.
PDFSearchView.swift โ ํ
์คํธ ๊ฒ์
import SwiftUI import PDFKit @Observable class PDFSearchManager { var searchResults: [PDFSelection] = [] var currentResultIndex: Int = 0 func search(query: String, in document: PDFDocument?) { searchResults.removeAll() guard let document = document, !query.isEmpty else { return } // ์ ์ฒด ๋ฌธ์ ๊ฒ์ document.beginFindString(query, withOptions: .caseInsensitive) // ๊ฒ์ ๊ฒฐ๊ณผ ์์ง NotificationCenter.default.addObserver( forName: .PDFDocumentDidFindMatch, object: document, queue: .main ) { notification in if let selection = notification.userInfo?["PDFDocumentFoundSelection"] as? PDFSelection { self.searchResults.append(selection) } } } func goToResult(at index: Int, in pdfView: PDFView) { guard index >= 0 && index < searchResults.count else { return } let selection = searchResults[index] pdfView.setCurrentSelection(selection, animate: true) pdfView.go(to: selection) currentResultIndex = index } } struct PDFSearchView: View { let url: URL @State private var pdfView = PDFView() @State private var searchManager = PDFSearchManager() @State private var searchQuery = "" @State private var isSearching = false var body: some View { VStack { // ๊ฒ์ ๋ฐ HStack { TextField("๊ฒ์...", text: $searchQuery) .textFieldStyle(.roundedBorder) .onSubmit { performSearch() } Button("๊ฒ์") { performSearch() } } .padding() // ๊ฒ์ ๊ฒฐ๊ณผ if !searchManager.searchResults.isEmpty { HStack { Button { navigateToPrevious() } label: { Image(systemName: "chevron.up") } Text("\(searchManager.currentResultIndex + 1) / \(searchManager.searchResults.count)") .font(.caption) Button { navigateToNext() } label: { Image(systemName: "chevron.down") } } .padding(.horizontal) } // PDF ๋ทฐ PDFKitView(url: url) .onAppear { if let document = PDFDocument(url: url) { pdfView.document = document } } } } func performSearch() { searchManager.search(query: searchQuery, in: pdfView.document) if !searchManager.searchResults.isEmpty { searchManager.goToResult(at: 0, in: pdfView) } } func navigateToNext() { let nextIndex = (searchManager.currentResultIndex + 1) % searchManager.searchResults.count searchManager.goToResult(at: nextIndex, in: pdfView) } func navigateToPrevious() { let prevIndex = (searchManager.currentResultIndex - 1 + searchManager.searchResults.count) % searchManager.searchResults.count searchManager.goToResult(at: prevIndex, in: pdfView) } }
โ๏ธ 4. PDF ์ฃผ์
Add annotations like highlights, notes, and drawings to PDFs.
PDFAnnotationView.swift โ Add Annotations
import SwiftUI import PDFKit struct PDFAnnotationView: View { let url: URL @State private var pdfView = PDFView() @State private var selectedTool: AnnotationTool = .none enum AnnotationTool { case none, highlight, note, freeText } var body: some View { VStack { // ์ฃผ์ ํด๋ฐ HStack { Button { selectedTool = .highlight addHighlight() } label: { Label("ํ์ด๋ผ์ดํธ", systemImage: "highlighter") } Button { selectedTool = .note addNote() } label: { Label("๋ ธํธ", systemImage: "note.text") } Button { selectedTool = .freeText addFreeText() } label: { Label("ํ ์คํธ", systemImage: "text.cursor") } Spacer() Button { savePDF() } label: { Label("์ ์ฅ", systemImage: "square.and.arrow.down") } } .padding() .background(Color(uiColor: .systemBackground)) // PDF ๋ทฐ PDFAnnotatableView(pdfView: $pdfView, url: url) } } func addHighlight() { guard let selection = pdfView.currentSelection, let page = selection.pages.first else { return } // ํ์ด๋ผ์ดํธ ์ฃผ์ ์์ฑ let highlight = PDFAnnotation( bounds: selection.bounds(for: page), forType: .highlight, withProperties: nil ) highlight.color = .UIColor.yellow.withAlphaComponent(0.5) page.addAnnotation(highlight) } func addNote() { guard let page = pdfView.currentPage else { return } // ๋ ธํธ ์ฃผ์ ์์ฑ let bounds = CGRect(x: 100, y: 100, width: 20, height: 20) let note = PDFAnnotation(bounds: bounds, forType: .text, withProperties: nil) note.contents = "์ ๋ ธํธ" note.color = .UIColor.yellow page.addAnnotation(note) } func addFreeText() { guard let page = pdfView.currentPage else { return } // ์์ ํ ์คํธ ์ฃผ์ let bounds = CGRect(x: 100, y: 200, width: 200, height: 50) let freeText = PDFAnnotation(bounds: bounds, forType: .freeText, withProperties: nil) freeText.contents = "ํ ์คํธ ์ ๋ ฅ" freeText.font = UIFont.systemFont(ofSize: 14) freeText.fontColor = .UIColor.black page.addAnnotation(freeText) } func savePDF() { guard let document = pdfView.document else { return } // ๋ฌธ์์ ์ฃผ์ ์ ์ฅ let data = document.dataRepresentation() // data๋ฅผ ํ์ผ๋ก ์ ์ฅํ๊ฑฐ๋ ๊ณต์ } } struct PDFAnnotatableView: UIViewRepresentable { @Binding var pdfView: PDFView let url: URL func makeUIView(context: Context) -> PDFView { pdfView.document = PDFDocument(url: url) pdfView.autoScales = true // ์ฃผ์ ํธ์ง ๊ฐ๋ฅํ๋๋ก ์ค์ pdfView.displayMode = .singlePage return pdfView } func updateUIView(_ uiView: PDFView, context: Context) {} }
๐ผ๏ธ 5. PDF ์ธ๋ค์ผ
Display page thumbnails using PDFThumbnailView.
PDFThumbnailView.swift โ Thumbnail View
import SwiftUI import PDFKit struct PDFThumbnailView: View { let pdfView: PDFView @Environment(\.dismiss) var dismiss var body: some View { NavigationStack { ThumbnailViewWrapper(pdfView: pdfView) .navigationTitle("ํ์ด์ง") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("๋ซ๊ธฐ") { dismiss() } } } } } } struct ThumbnailViewWrapper: UIViewRepresentable { let pdfView: PDFView func makeUIView(context: Context) -> PDFThumbnailView { let thumbnailView = PDFThumbnailView() thumbnailView.pdfView = pdfView thumbnailView.layoutMode = .vertical thumbnailView.thumbnailSize = CGSize(width: 100, height: 150) thumbnailView.backgroundColor = .UIColor.systemBackground return thumbnailView } func updateUIView(_ uiView: PDFThumbnailView, context: Context) {} }
๐ 6. PDF ์์ฑ
์ฝ๋๋ก ์๋ก์ด PDF ๋ฌธ์๋ฅผ ์์ฑ.
PDFGenerator.swift โ PDF ์์ฑ
import PDFKit import UIKit class PDFGenerator { // ํ ์คํธ๋ก PDF ์์ฑ func generatePDF(from text: String) -> PDFDocument? { let pdfMetaData = [ kCGPDFContextCreator: "MyApp", kCGPDFContextAuthor: "Author Name", kCGPDFContextTitle: "Generated PDF" ] let format = UIGraphicsPDFRendererFormat() format.documentInfo = pdfMetaData as [String: Any] let pageRect = CGRect(x: 0, y: 0, width: 612, height: 792) // US Letter let renderer = UIGraphicsPDFRenderer(bounds: pageRect, format: format) let data = renderer.pdfData { context in context.beginPage() // ํ ์คํธ ๊ทธ๋ฆฌ๊ธฐ let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .natural paragraphStyle.lineBreakMode = .byWordWrapping let attributes: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 14), .paragraphStyle: paragraphStyle ] let textRect = CGRect(x: 40, y: 40, width: pageRect.width - 80, height: pageRect.height - 80) text.draw(in: textRect, withAttributes: attributes) } return PDFDocument(data: data) } // ์ด๋ฏธ์ง๋ก PDF ์์ฑ func generatePDF(from images: [UIImage]) -> PDFDocument? { let pdfDocument = PDFDocument() for (index, image) in images.enumerated() { let pageRect = CGRect(origin: .zero, size: image.size) let renderer = UIGraphicsPDFRenderer(bounds: pageRect) let data = renderer.pdfData { context in context.beginPage() image.draw(in: pageRect) } if let page = PDFPage(image: image) { pdfDocument.insert(page, at: index) } } return pdfDocument } // SwiftUI View๋ฅผ PDF๋ก ๋ณํ func generatePDF(from view: UIView) -> PDFDocument? { let renderer = UIGraphicsPDFRenderer(bounds: view.bounds) let data = renderer.pdfData { context in context.beginPage() view.layer.render(in: context.cgContext) } return PDFDocument(data: data) } // PDF ์ ์ฅ func savePDF(_ document: PDFDocument, to url: URL) -> Bool { return document.write(to: url) } }
๐ฑ Complete Example
PDFReaderApp.swift โ PDF Reader App
import SwiftUI import PDFKit struct PDFReaderApp: View { @State private var pdfView = PDFView() @State private var showSearch = false @State private var showThumbnails = false @State private var displayMode: PDFDisplayMode = .singlePageContinuous let url: URL var body: some View { NavigationStack { ZStack { PDFReaderView( pdfView: $pdfView, url: url, displayMode: displayMode ) if showSearch { SearchOverlay(pdfView: pdfView, isPresented: $showSearch) } } .navigationTitle("PDF") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItemGroup(placement: .topBarTrailing) { Button { showSearch.toggle() } label: { Image(systemName: "magnifyingglass") } Button { showThumbnails = true } label: { Image(systemName: "square.grid.2x2") } Menu { Button("๋จ์ผ ํ์ด์ง") { displayMode = .singlePage updateDisplayMode() } Button("์ฐ์ ์คํฌ๋กค") { displayMode = .singlePageContinuous updateDisplayMode() } Button("๋ ํ์ด์ง") { displayMode = .twoUp updateDisplayMode() } } label: { Image(systemName: "doc.text.viewfinder") } } } .sheet(isPresented: $showThumbnails) { PDFThumbnailView(pdfView: pdfView) } } } func updateDisplayMode() { pdfView.displayMode = displayMode } } struct PDFReaderView: UIViewRepresentable { @Binding var pdfView: PDFView let url: URL let displayMode: PDFDisplayMode func makeUIView(context: Context) -> PDFView { pdfView.document = PDFDocument(url: url) pdfView.autoScales = true pdfView.displayMode = displayMode pdfView.displayDirection = .vertical return pdfView } func updateUIView(_ uiView: PDFView, context: Context) { uiView.displayMode = displayMode } } struct SearchOverlay: View { let pdfView: PDFView @Binding var isPresented: Bool @State private var searchText = "" var body: some View { VStack { HStack { TextField("๊ฒ์", text: $searchText) .textFieldStyle(.roundedBorder) .onSubmit { pdfView.document?.beginFindString(searchText, withOptions: .caseInsensitive) } Button("๋ซ๊ธฐ") { isPresented = false pdfView.document?.cancelFindString() } } .padding() .background(Color(uiColor: .systemBackground)) .shadow(radius: 5) Spacer() } } }
๐ก HIG Guidelines
- ๊ฐ๋ ์ฑ: Auto-scaling (autoScales) for comfortable reading size
- Navigation: ๋ช ํํ ํ์ด์ง ํ์ ๋ฐ ์ด๋ ์ปจํธ๋กค
- ๊ฒ์: ๋น ๋ฅธ ํ ์คํธ ๊ฒ์๊ณผ ๊ฒฐ๊ณผ ํ์ด๋ผ์ดํ
- ์ฃผ์: ์ง๊ด์ ์ธ ์ฃผ์ ๋๊ตฌ ์ ๊ณต
- ์ฑ๋ฅ: ๋์ฉ๋ PDF๋ ํ์ด์ง๋ณ ๋ก๋ฉ ๊ณ ๋ ค
- ์ ๊ทผ์ฑ: VoiceOver support and text selection enabled
๐ฏ Practical Usage
- ๋ฌธ์ ๋ทฐ์ด: ๊ณ์ฝ์, ๋ณด๊ณ ์ ์ด๋ ์ฑ
- ์ ์์ฑ ๋ฆฌ๋: PDF ์ ์์ฑ ๋ฆฌ๋
- ์ฃผ์ ๋๊ตฌ: ํ์ต์ฉ PDF ๋งํน ์ฑ
- ๋ฆฌํฌํธ ์์ฑ: ๋ฐ์ดํฐ๋ฅผ PDF ๋ฆฌํฌํธ๋ก ๋ณํ
- ๋ฌธ์ ์๋ช : ์ ์ ์๋ช ์ถ๊ฐ ์ฑ
๐ Learn More
โก๏ธ Performance Tips: ๋์ฉ๋ PDF๋
PDFView์ pageBreakMargins์ minScaleFactor/maxScaleFactor to optimize memory usage. Generate thumbnails on a background thread for smooth UI.