I added a canvas view using PDFPageOverlayViewProvider. When I zoom the PDFView, the drawing is scaled, but its quality becomes blurry. How can I fix this?
import SwiftUI
import PDFKit
import PencilKit
import CoreGraphics
struct ContentView: View {
var body: some View {
if
let url = Bundle.main.url(forResource: "sample", withExtension: "pdf"),
let data = try? Data(contentsOf: url),
let document = PDFDocument(data: data)
{
PDFRepresentableView(document: document)
} else {
Text("fail")
}
}
}
#Preview {
ContentView()
}
struct PDFRepresentableView: UIViewRepresentable {
let document: PDFDocument
let pdfView = PDFView()
func makeUIView(context: Context) -> PDFView {
pdfView.displayMode = .singlePageContinuous
pdfView.usePageViewController(false)
pdfView.displayDirection = .vertical
pdfView.pageOverlayViewProvider = context.coordinator
pdfView.document = document
pdfView.autoScales = false
pdfView.minScaleFactor = 0.7
pdfView.maxScaleFactor = 4
return pdfView
}
func updateUIView(_ uiView: PDFView, context: Context) {
// Optional: update logic if needed
}
func makeCoordinator() -> CustomCoordinator {
return CustomCoordinator(parent: self)
}
}
class CustomCoordinator: NSObject, PDFPageOverlayViewProvider, PKCanvasViewDelegate {
let parent: PDFRepresentableView
init(parent: PDFRepresentableView) {
self.parent = parent
}
func pdfView(_ view: PDFView, overlayViewFor page: PDFPage) -> UIView? {
let result = UIView()
let canvasView = PKCanvasView()
canvasView.drawingPolicy = .anyInput
canvasView.tool = PKInkingTool(.pen, color: .blue, width: 20)
canvasView.translatesAutoresizingMaskIntoConstraints = false
result.addSubview(canvasView)
NSLayoutConstraint.activate([
canvasView.leadingAnchor.constraint(equalTo: result.leadingAnchor),
canvasView.trailingAnchor.constraint(equalTo: result.trailingAnchor),
canvasView.topAnchor.constraint(equalTo: result.topAnchor),
canvasView.bottomAnchor.constraint(equalTo: result.bottomAnchor)
])
for subView in view.documentView?.subviews ?? [] {
subView.isUserInteractionEnabled = true
}
result.layoutIfNeeded()
return result
}
}
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Hi all,
I’m currently building a SwiftUI app that overlays a PKCanvasView onto each page of a PDFView using PDFPageOverlayViewProvider. It works well at the initial scale, but once I zoom into the PDF, the drawings on the PKCanvasView appear blurry or pixelated, even though the PDF itself remains crisp.
I’m trying to adjust canvasView.contentScaleFactor relative to pdfView.scaleFactor to preserve the drawing quality. Here’s a simplified version of the relevant code:
import SwiftUI
import PDFKit
import PencilKit
struct ContentView: View {
var body: some View {
if let url = Bundle.main.url(forResource: "sample", withExtension: "pdf"),
let data = try? Data(contentsOf: url),
let document = PDFDocument(data: data) {
PDFRepresentableView(document: document)
} else {
Text("")
}
}
}
#Preview {
ContentView()
}
struct PDFRepresentableView: UIViewRepresentable {
let document: PDFDocument
let pdfView = PDFView()
func makeUIView(context: Context) -> PDFView {
pdfView.displayMode = .singlePageContinuous
pdfView.usePageViewController(false)
pdfView.displayDirection = .vertical
pdfView.pageOverlayViewProvider = context.coordinator
pdfView.document = document
pdfView.autoScales = false
pdfView.minScaleFactor = 0.7
pdfView.maxScaleFactor = 4
NotificationCenter.default.addObserver(
context.coordinator,
selector: #selector(context.coordinator.onPageZoomAndPan),
name: .PDFViewScaleChanged,
object: pdfView
)
return pdfView
}
func updateUIView(_ uiView: PDFView, context: Context) {
// Optional: update logic if needed
}
func makeCoordinator() -> CustomCoordinator {
return CustomCoordinator(parent: self)
}
}
class CustomCoordinator: NSObject, PDFPageOverlayViewProvider, PKCanvasViewDelegate {
let parent: PDFRepresentableView
init(parent: PDFRepresentableView) {
self.parent = parent
}
func pdfView(_ view: PDFView, overlayViewFor page: PDFPage) -> UIView? {
let canvasView = PKCanvasView()
let rect = page.bounds(for: .mediaBox)
canvasView.drawingPolicy = .anyInput
canvasView.tool = PKInkingTool(.pen, color: .black, width: 10)
canvasView.translatesAutoresizingMaskIntoConstraints = true
canvasView.backgroundColor = .red.withAlphaComponent(0.1)
canvasView.frame = rect
canvasView.isScrollEnabled = false
for subView in view.documentView?.subviews ?? [] {
subView.isUserInteractionEnabled = true
}
return canvasView
}
@objc func onPageZoomAndPan() {
parent.pdfView.documentView?.subviews.forEach { subview in
if
subview.theClassName == "PDFPageView",
let pageViewPrivate = subview.value(forKey: "_private") as? NSObject,
let page = pageViewPrivate.value(forKey: "page") as? PDFPage {
subview.subviews.forEach { subview in
if let canvasView = subview as? PKCanvasView {
let zoomScale = parent.pdfView.scaleFactor
canvasView.contentScaleFactor = UIScreen.main.scale * zoomScale
canvasView.drawing = canvasView.drawing
canvasView.setNeedsDisplay()
canvasView.layoutIfNeeded()
}
}
}
}
print("Zoom changed. Current scale: \(parent.pdfView.scaleFactor)")
}
}
extension NSObject {
var theClassName: String {
return NSStringFromClass(type(of: self))
}
}
But this doesn’t seem to improve the rendered quality. The lines still appear blurry when zoomed in.
What I’ve tried:
• Adjusting contentScaleFactor and forcing redraw
• Reassigning canvasView.drawing
• Calling setNeedsDisplay() and layoutIfNeeded()
None of these approaches seem to re-render the canvas at a higher resolution or match the zoomed scale of the PDF.
My questions:
1. Is there a correct way to scale PKCanvasView content to match PDF zoom levels?
2. Should I recreate the canvas or drawing when zoom changes?
3. Is PKCanvasView just not intended to handle high zoom fidelity?
If anyone has successfully overlaid high-resolution canvas drawing on a zoomable PDFView, I’d love to hear how you managed it.
Thanks in advance!
The canvasView in PKCanvasViewDelegate only holds the drawing data up to the most recently completed stroke. I want to access the currently in-progress drawing (before the pencil is lifted) in order to measure the path’s size — but I can’t find a way to do it.
Is there any way you could help me with this?
I am using LazyVStack inside a ScrollView. I understand that lazy views are rendered only when they come into view. However, I haven’t heard much about memory deallocation.
I observed that in iOS 18 and later, when scrolling up, the bottom-most views are deallocated from memory, whereas in iOS 17, they are not (Example 1).
Additionally, I noticed a similar behavior when switching views using a switch. When switching views by pressing a button, the view was intermittently deinitialized. (Example 2).
Example 1)
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(0..<40) { index in
CellView(index: index)
}
}
}
.padding()
}
}
struct CellView: View {
let index: Int
@StateObject var viewModel = CellViewModel()
var body: some View {
Rectangle()
.fill(Color.accentColor)
.frame(width: 300, height: 300)
.overlay {
Text("\(index)")
}
.onAppear {
viewModel.index = index
}
}
}
class CellViewModel: ObservableObject {
@Published var index = 0
init() {
print("init")
}
deinit {
print("\(index) deinit")
}
}
#Preview {
ContentView()
}
Example 2
struct ContentView: View {
@State var index = 0
var body: some View {
LazyVStack {
Button(action: {
if index > 5 {
index = 0
} else {
index += 1
}
}) {
Text("plus index")
}
MidCellView(index: index)
}
.padding()
}
}
struct MidCellView: View {
let index: Int
var body: some View {
switch index {
case 1:
CellView(index: 1)
case 2:
CellView(index: 2)
case 3:
CellView(index: 3)
case 4:
CellView(index: 4)
default:
CellView(index: 0)
}
}
}
struct CellView: View {
let index: Int
@StateObject var viewModel = CellViewModel()
var body: some View {
Rectangle()
.fill(Color.accentColor)
.frame(width: 300, height: 300)
.overlay {
Text("\(index)")
}
.onAppear {
viewModel.index = index
}
}
}
class CellViewModel: ObservableObject {
@Published var index = 0
init() {
print("init")
}
deinit {
print("\(index) deinit")
}
}
--------------------
init
init
init
init
init
2 deinit
3 deinit
4 deinit
init
Topic:
UI Frameworks
SubTopic:
SwiftUI
I tried to create a Text View using attributedString. I want to set the line height using paragraphStyle and return the Text, but paragraphStyle is not being applied. Why is that?
extension Text {
init?(_ content: String, font: StyleType, color: Color = .ppBlack) {
var attributedString = AttributedString(content)
attributedString.font = Font.custom(font.fontWeight, fixedSize: font.fontSize)
attributedString.foregroundColor = color
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = 16
paragraphStyle.maximumLineHeight = 16
paragraphStyle.lineSpacing = 0
attributedString.mergeAttributes(.init([.paragraphStyle: paragraphStyle]))
self = Text(attributedString)
}
}
I am using PDFPageOverlayViewProvider from pdfkit. I would like to detect changes in the overlaid view and refresh PDFPageOverlayViewProvider. Currently, it does not refresh even when the overlaid view changes. Is there a way to refresh it?
I want to add a pkcanvasview image and change its position and size by dragging. And I want to draw a picture using a pencil on top.
The image must be able to change its position at any time, and a picture must be added on top.
If I add it to the subview of pkcanvasview, I cannot do what I want. How can I do this?
I need to apply security measures to a PDF. My goal is to prevent it from being leaked, and even if it is leaked, I want to ensure that the PDF cannot be viewed. Is it possible to use pdfkit and apply DRM to achieve this?⬤
I’m using PDFPageOverlayViewProvider with pdfview. I want to control the visibility of the overlay view using a button. However, the view updates only when it disappears and reappears. I would like the changes to be reflected immediately. How can I achieve this?
struct PDFKitView: View {
let bookId: UUID
let bookTitle: String
@State private var currentPage = "1"
@State private var isSideTab = false
@State private var selectedNote: [SelectedNote] = []
var body: some View {
let pdfDocument = openPDF(at: bookId.uuidString)
ZStack(alignment: .trailing) {
HStack(spacing: 0) {
PDFKitRepresentableView(
bookId: bookId,
selectedNote: $selectedNote,
currentPage: $currentPage,
pdfDocument: pdfDocument
)
if isSideTab {
SideView()
.transition(.move(edge: .trailing))
.zIndex(1)
.frame(maxWidth: 260)
}
}
.padding(.top, 73)
}
.onAppear {
getAllNote(bookId: bookId)
}
.customNavigationBar(back: true) {
Text("\(currentPage)/\(pdfDocument.pageCount)")
.pretendard(.CaptionRegular)
.foregroundStyle(Color.Text.primary)
} TrailingView: {
Button(action: {
withAnimation {
isSideTab.toggle()
}
}) {
Image(systemName: SFSymbol.squareStack3dDownForwardFill.icon)
.sfPro(.IconMedium)
.foregroundStyle(Color.Text.primary)
}
}
}
@ViewBuilder
func SideView() -> some View {
VStack(spacing: 16) {
HStack(spacing: 4) {
Image(systemName: SFSymbol.squareStack3dDownForwardFill.icon)
.sfPro(.IconSmall)
.foregroundStyle(Color.Text.primary)
Text(Literals.sideTabTitle)
.pretendard(.P2Bold)
.foregroundStyle(Color.Text.primary)
Spacer()
}
.padding(16)
.background {
Color.Background.white
}
ScrollView {
ForEach($selectedNote, id: \.noteId) { note in
NoteCellView(note: note)
}
}
.background {
Color.Fill.white
}
}
.background {
Color.Background.blueGray
}
}
@ViewBuilder
func NoteCellView(note: Binding<SelectedNote>) -> some View {
HStack(alignment: .top, spacing: 16) {
Image(.writingNote)
.resizable()
.scaledToFit()
.frame(width: 42, height: 60)
VStack(alignment: .leading, spacing: 8) {
Text(note.wrappedValue.noteId == bookId.uuidString ? "345" : "123")
.foregroundStyle(Color.Text.secondary)
.padding(.horizontal, 8)
.background {
Rectangle()
.strokeBorder(Color.Layout.secondary)
}
Text(bookTitle)
.lineLimit(2)
Toggle("", isOn: note.selected)
.labelsHidden()
.tint(Color.Fill.activePrimary)
}
}
.padding(EdgeInsets(top: 20, leading: 16, bottom: 16, trailing: 16))
}
}
struct PDFKitRepresentableView: UIViewRepresentable {
let bookId: UUID
@Binding var selectedNote: [SelectedNote]
@Binding var currentPage: String
let pdfDocument: PDFDocument
let pdfView = PDFView()
let toolPicker = PKToolPicker()
func makeUIView(context: Context) -> PDFView {
pdfView.displayMode = .singlePageContinuous
pdfView.usePageViewController(false)
pdfView.displayDirection = .vertical
pdfView.pageOverlayViewProvider = context.coordinator
pdfView.autoScales = true
pdfDocument.delegate = context.coordinator
pdfView.document = pdfDocument
return pdfView
}
func updateUIView(_ uiView: PDFView, context: Context) {
if
let localNote = selectedNote.first(where: {$0.noteId == bookId.uuidString}),
!localNote.selected
{
toolPicker.setVisible(false, forFirstResponder: uiView)
} else {
toolPicker.setVisible(true, forFirstResponder: uiView)
}
uiView.becomeFirstResponder()
}
func makeCoordinator() -> CanvasProvider {
return CanvasProvider(parent: self)
}
}
final class CanvasProvider: NSObject, PDFPageOverlayViewProvider, PDFDocumentDelegate {
var localNotes = [PDFPage: PKCanvasView]()
var passNotes = [PDFPage: Image]()
let parent: PDFKitRepresentableView
init(parent: PDFKitRepresentableView) {
self.parent = parent
super.init()
getDrawingDatas(
bookId: parent.bookId.uuidString,
selectedNote: parent.selectedNote,
document: parent.pdfDocument
)
}
func pdfView(_ view: PDFView, overlayViewFor page: PDFPage) -> UIView? {
var coverView: PKCanvasView? = PKCanvasView()
if
let view = localNotes[page],
parent.selectedNote.first(where: { $0.noteId == parent.bookId.uuidString })?.selected ?? false
{
view.backgroundColor = .clear
view.isOpaque = true
view.drawingPolicy = .anyInput
view.delegate = self
parent.toolPicker.addObserver(view)
coverView = view
(page as? CanvasPDFPage)?.canvasView = view
} else {
coverView = nil
}
for subView in view.documentView?.subviews ?? [] {
if subView.theClassName == "PDFPageView" {
subView.isUserInteractionEnabled = true
}
}
return coverView
}
func pdfView(_ pdfView: PDFView, willDisplayOverlayView overlayView: UIView, for page: PDFPage) { }
func pdfView(_ pdfView: PDFView, willEndDisplayingOverlayView overlayView: UIView, for page: PDFPage) { }
}
I encountered a problem while using ScrollView in SwiftUI. When I perform a refresh, the app crashes. I access the array using an index in a ForEach loop. This is done to create new data from the array in a commonly used view. The function to create data is adopted from a protocol in the view model. I access it by index because the type of the array is not specified; each view using it may have a different data type. Below is an example code. Is it possible to access data from the array using an index? Every time I refresh, I get an "index out of range" error.
import SwiftUI
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
ScrollView {
if !viewModel.testValues.isEmpty {
LazyVStack(spacing: 20) {
ForEach(Array(zip(viewModel.testValues.indices, viewModel.testValues)), id:\.1) { index, data in
test(index: index, data: data, viewModel: viewModel)
.onAppear {
if !viewModel.isLoading, viewModel.testValues.count - 2 == index {
viewModel.fetch()
}
}
}
}
} else {
Text("tesetsetsetsettse")
}
}
.onAppear {
viewModel.fetch()
}
.refreshable {
viewModel.refresh()
}
}
}
struct test: View {
let index: Int
let data: String
@ObservedObject var viewModel: ViewModel
var body: some View {
VStack(spacing: 8) {
test1(index: index, data: data, viewModel: viewModel)
Text("------------------------")
}
}
}
struct test1: View {
let index: Int
let data: String
@ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text(viewModel.testValues[index])
.font(.system(size: 12))
.foregroundStyle(Color.red)
.padding(.horizontal, 40)
.padding(.vertical, 50)
.background {
RoundedRectangle(cornerRadius: 20)
.fill(Color.blue)
}
}
}
}
class ViewModel: ObservableObject {
@Published var isLoading = false
@Published var testValues: [String] = []
func fetch() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.testValues += [
UUID().uuidString,
UUID().uuidString,
UUID().uuidString,
UUID().uuidString,
UUID().uuidString,
UUID().uuidString,
UUID().uuidString,
UUID().uuidString,
UUID().uuidString,
UUID().uuidString,
]
}
}
func refresh() {
testValues = []
fetch()
}
}
I want to create a list in the Reminders app using EventKit. For each created list, I want to create EKReminders. However, it seems that EventKit does not provide a way to create a reminder list directly. How can I achieve this?
I learned that the background appears on the header of the last section. Why does that happen.
Here is my code:
This situration does not occur unless .font is applied to the Text.
How can we interpret this issue?