I'm trying to make an application that will draw on top of a pdf. It is required that graphic shapes be tied to a specific position on the page.
To solve the problem, I use PDFView and CALayer, listen to the events of the internal UIScrollView scrollViewDidScroll and scrollViewDidZoom to redraw the CALayer
I found two ways to draw:
I noticed that in the second case the figure lags noticeably behind the pdf document when scrolling or zooming.
The question is, what is so “magic” done in the “path” method that leads to instant redrawing? Is it possible to modify the “draw” method so that it works as synchronously as “path”? And how stable will the "path" method work in the future?
Check out the project to compile&run: https://github.com/alzhuravlev/DrawTest.git
Sample code:
class AnnotationsLayer: CAShapeLayer {
...
override func draw(in ctx: CGContext) {
guard let backgroundView else { return }
ctx.setFillColor(UIColor.red.withAlphaComponent(0.4).cgColor)
ctx.setStrokeColor(UIColor.green.withAlphaComponent(1.0).cgColor)
ctx.setLineWidth(10)
backgroundView.pages.forEach { page in
let p1 = backgroundView.convert(point: CGPoint(x: page.size.width/3, y: page.size.height/3), from: page.index)
let p2 = backgroundView.convert(point: CGPoint(x: page.size.width/3*2, y: page.size.height/3*2), from: page.index)
ctx.move(to: CGPoint(x: p1.x, y: p1.y))
ctx.addLine(to: CGPoint(x: p2.x, y: p1.y))
ctx.addLine(to: CGPoint(x: p2.x, y: p2.y))
ctx.addLine(to: CGPoint(x: p1.x, y: p2.y))
ctx.closePath()
}
ctx.drawPath(using: CGPathDrawingMode.fillStroke)
}
func rebuildShapes() {
// false - example 1
// true - example 2
if (false) {
setNeedsDisplay()
} else {
guard let backgroundView else { return }
let p = UIBezierPath()
fillColor = UIColor.red.withAlphaComponent(0.4).cgColor
strokeColor = UIColor.green.withAlphaComponent(1.0).cgColor
lineWidth = 10
backgroundView.pages.forEach { page in
let p1 = backgroundView.convert(point: CGPoint(x: page.size.width/3, y: page.size.height/3), from: page.index)
let p2 = backgroundView.convert(point: CGPoint(x: page.size.width/3*2, y: page.size.height/3*2), from: page.index)
p.move(to: CGPoint(x: p1.x, y: p1.y))
p.addLine(to: CGPoint(x: p2.x, y: p1.y))
p.addLine(to: CGPoint(x: p2.x, y: p2.y))
p.addLine(to: CGPoint(x: p1.x, y: p2.y))
p.close()
}
path = p.cgPath
}
}
...
Based on OP's comments, here is what I would use instead of overriding draw(in ctx: CGContext)
in a CAShapeLayer
:
class AnnotationsView: UIView {
var backgroundView: BackgroundView? {
didSet {
print("did set background view")
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
backgroundColor = .clear
}
func rebuildShapes() {
setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let backgroundView else { return }
let p = UIBezierPath()
let fillColor = UIColor.red.withAlphaComponent(0.4)
let strokeColor = UIColor.green.withAlphaComponent(1.0)
let lineWidth = 10
backgroundView.pages.forEach { page in
let p1 = backgroundView.convert(point: CGPoint(x: page.size.width/3, y: page.size.height/3), from: page.index)
let p2 = backgroundView.convert(point: CGPoint(x: page.size.width/3*2, y: page.size.height/3*2), from: page.index)
let r: CGRect = .init(x: p1.x, y: p1.y, width: p2.x - p1.x, height: p2.y - p1.y)
if r.intersects(rect) {
p.append(.init(rect: r))
}
}
fillColor.setFill()
p.fill()
strokeColor.setStroke()
p.lineWidth = CGFloat(lineWidth)
p.stroke()
}
}
No need to add any layers.