-
Notifications
You must be signed in to change notification settings - Fork 15
/
SpreadMeasurementsExample.swift
246 lines (208 loc) · 8.3 KB
/
SpreadMeasurementsExample.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
//
// Copyright © 2020-2024 PSPDFKit GmbH. All rights reserved.
//
// The PSPDFKit Sample applications are licensed with a modified BSD license.
// Please see License for details. This notice may not be removed from this file.
//
import PSPDFKit
import PSPDFKitUI
/// Swift code from https://pspdfkit.com/guides/ios/customizing-pdf-pages/adding-auxiliary-or-decorative-views/
///
/// For an in-depth explanation of the classes and structs, please read this article, as well as its companion guide
/// “Customizing Interactions with an Annotation Type”.
class SpreadMeasurementsExample: Example {
override init() {
super.init()
title = "Measurements on Pages/Spreads"
contentDescription = "Shows how to add auxilary views to spreads/pages"
category = .viewCustomization
}
private var manager: MeasuringPDFControllerManager?
override func invoke(with delegate: ExampleRunnerDelegate) -> UIViewController? {
manager = .init()
manager?.document = AssetLoader.document(for: .quickStart)
return manager?.documentViewController
}
}
// MARK: - Measurement and Datasource:
protocol SpreadMeasurement {
var pageRange: NSRange { get }
var path: CGPath { get }
var value: Measurement<Dimension> { get }
}
protocol DocumentMeasurementDatasource: AnyObject {
func measurements(at pageIndex: Int) -> [SpreadMeasurement]
}
// MARK: - View and Extension:
private extension SpreadMeasurement {
var isArea: Bool {
value.unit is UnitArea
}
}
private class SpreadMeasurementView: UIView, AnnotationPresenting {
var pdfScale: CGFloat {
didSet {
if oldValue != pdfScale, let measurement {
// The transform for PDF to page view coordinates just changed, so we have to adapt
updateFrameAndLayer(measurement: measurement, scale: pdfScale)
}
}
}
var zoomScale: CGFloat {
didSet {
// make sure the label is always crisp
let viewScale = window?.traitCollection.displayScale ?? 1
dimensionLabel.contentScaleFactor = zoomScale * viewScale
}
}
func prepareForReuse() {
measurement = nil
}
var measurement: SpreadMeasurement? {
didSet {
guard let measurement else {
dimensionLabel.isHidden = true
shapeLayer.path = nil
return
}
dimensionLabel.isHidden = false
dimensionLabel.text = formatter.string(from: measurement.value)
updateFrameAndLayer(measurement: measurement, scale: pdfScale)
if measurement.isArea {
shapeLayer.fillColor = UIColor(white: 0.2, alpha: 0.4).cgColor
shapeLayer.lineDashPattern = nil
} else {
shapeLayer.fillColor = nil
shapeLayer.lineDashPattern = [5, 3, 2, 3]
}
}
}
private func updateFrameAndLayer(measurement: SpreadMeasurement, scale: CGFloat) {
guard scale > 0 else {
return
}
let path = measurement.path
var transform = CGAffineTransform(scaleX: scale, y: scale)
let boundingBox = path.boundingBox.applying(transform)
frame = boundingBox
// The layer is in coordinates of the bounds so we need to account for the offset, too
transform.tx = -boundingBox.origin.x
transform.ty = -boundingBox.origin.y
shapeLayer.path = path.copy(using: &transform)
setNeedsLayout()
}
override init(frame: CGRect) {
pdfScale = 0
zoomScale = 0
shapeLayer = .init()
shapeLayer.lineWidth = 1
shapeLayer.bounds.size = frame.size
shapeLayer.strokeColor = UIColor.systemRed.cgColor
dimensionLabel = UILabel()
dimensionLabel.translatesAutoresizingMaskIntoConstraints = false
dimensionLabel.backgroundColor = UIColor.systemRed.withAlphaComponent(0.6)
dimensionLabel.textColor = .white
formatter = .init()
formatter.unitOptions = .providedUnit
formatter.unitStyle = .short
formatter.numberFormatter.maximumFractionDigits = 2
super.init(frame: frame)
clipsToBounds = false
layer.addSublayer(shapeLayer)
addSubview(dimensionLabel)
NSLayoutConstraint.activate([
dimensionLabel.topAnchor.constraint(equalToSystemSpacingBelow: bottomAnchor, multiplier: 1),
dimensionLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("NSCoding is not supported")
}
private let shapeLayer: CAShapeLayer
private let dimensionLabel: UILabel
private let formatter: MeasurementFormatter
}
// MARK: - Page View:
private class MeasurementDisplayingPageView: PDFPageView {
private var measureViewReusePool = [SpreadMeasurementView]()
private var visibleMeasureViews = [SpreadMeasurementView]()
override func prepareForReuse() {
visibleMeasureViews.forEach { view in
view.isHidden = true
view.prepareForReuse()
}
measureViewReusePool.append(contentsOf: visibleMeasureViews)
visibleMeasureViews.removeAll(keepingCapacity: true)
super.prepareForReuse()
}
func dequeueMeasureView() -> SpreadMeasurementView {
let view = measureViewReusePool.popLast() ?? SpreadMeasurementView()
view.isHidden = false
// Ensure the measure view is added to the annotation container view - PDFPageView.prepareForReuse()
// removes it from the view hierarchy. Also, make sure the view has the correct scales set, so it
// displays correctly.
annotationContainerView.addSubview(view)
visibleMeasureViews.append(view)
view.pdfScale = scaleForPageView
view.zoomScale = zoomView?.zoomScale ?? 1
return view
}
func markForReuse(measureView: SpreadMeasurementView) {
measureView.prepareForReuse()
measureView.isHidden = true
visibleMeasureViews.removeAll {
$0 === measureView
}
measureViewReusePool.append(measureView)
}
}
// MARK: - Integration:
private class MeasuringPDFControllerManager: NSObject, PDFViewControllerDelegate {
var measurementsSource: DocumentMeasurementDatasource?
let documentViewController: PDFViewController
var document: Document? {
get { documentViewController.document }
set {
if newValue == nil {
measurementsSource = nil
documentViewController.document = nil
} else if documentViewController.document !== newValue {
// <# create a new measurements datasource for this document here! #>
measurementsSource = newValue.map(MeasurementStore.init(document:)) // this line is removed from guide
// then:
documentViewController.document = newValue
}
}
}
override init() {
documentViewController = PDFViewController { builder in
builder.pageTransition = .scrollContinuous
builder.pageMode = .double
builder.scrollDirection = .vertical
builder.overrideClass(PDFPageView.self, with: MeasurementDisplayingPageView.self)
}
super.init()
documentViewController.delegate = self
}
func pdfViewController(_ pdfController: PDFViewController, didConfigurePageView pageView: PDFPageView, forPageAt pageIndex: Int) {
guard
let page = pageView as? MeasurementDisplayingPageView,
let allMeasurements = measurementsSource?.measurements(at: pageIndex)
else {
return
}
for measurement in allMeasurements
// a measurement can span multiple pages => make sure we don’t add one to more than one page at once
where measurement.pageRange.location == pageIndex {
let view = page.dequeueMeasureView()
view.measurement = measurement
}
/*
Ensure the second page in a spread is always below the first one in the hierarchy, to allow measurements to
reach across the page binding.
*/
if pdfController.configuration.pageMode == .double && pageIndex % 2 == 0 {
page.superview?.sendSubviewToBack(page)
}
}
}