Skip to content

Commit 02776a8

Browse files
committed
add PDFViewer
1 parent dfdee4a commit 02776a8

19 files changed

+914
-10
lines changed

Source/XUI/Collection View Layout/InfiniteCollectionViewLayout.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// InfiniteCollectionViewLayout.swift
3-
// Pods
3+
// XUI
44
//
55
// Created by xueqooy on 2023/8/21.
66
//

Source/XUI/Collection View Layout/SnapToCenterCollectionViewLayout.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// SnapToCenterCollectionViewLayout.swift
3-
// Pods
3+
// XUI
44
//
55
// Created by xueqooy on 2023/8/21.
66
//

Source/XUI/Extensions/TimeInterval+MediaTime.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// TimeInterval+MediaTime.swift
3-
// Pods
3+
// XUI
44
//
55
// Created by xueqooy on 2024/12/2.
66
//

Source/XUI/Gizmos/ImageView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// ImageView.swift
3-
// Pods
3+
// XUI
44
//
55
// Created by xueqooy on 2024/12/4.
66
//
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
//
2+
// PDFControlBar.swift
3+
// XUI
4+
//
5+
// Created by xueqooy on 2024/12/26.
6+
//
7+
8+
import UIKit
9+
10+
class PDFControlBar: UIView {
11+
var numberOfPages: Int = 0 {
12+
didSet {
13+
guard oldValue != numberOfPages else { return }
14+
15+
updatePageLabel()
16+
updatePageSlider()
17+
}
18+
}
19+
20+
var currentPageIndex: Int = 0 {
21+
didSet {
22+
guard oldValue != currentPageIndex else { return }
23+
24+
updatePageLabel()
25+
if !pageSliderDragging {
26+
updatePageSlider()
27+
}
28+
}
29+
}
30+
31+
var minScaleFactor: CGFloat = 0 {
32+
didSet {
33+
guard oldValue != minScaleFactor else { return }
34+
35+
scaleFactorSlider.minimumValue = Float(minScaleFactor)
36+
}
37+
}
38+
39+
var maxScaleFactor: CGFloat = 0 {
40+
didSet {
41+
guard oldValue != maxScaleFactor else { return }
42+
43+
scaleFactorSlider.maximumValue = Float(maxScaleFactor)
44+
}
45+
}
46+
47+
var currentScaleFactor: CGFloat = 0 {
48+
didSet {
49+
guard oldValue != currentScaleFactor, !scaleFactorSliderDragging else { return }
50+
51+
scaleFactorSlider.value = Float(currentScaleFactor)
52+
}
53+
}
54+
55+
56+
var goPageHandler: ((Int) -> Void)?
57+
var updateScaleFactorHandler: ((CGFloat) -> Void)?
58+
var fullscreenHandler: (() -> Void)?
59+
60+
override var intrinsicContentSize: CGSize {
61+
CGSize(width: UIView.noIntrinsicMetric, height: 31)
62+
}
63+
64+
private let pageLabel = UILabel(textColor: .white, font: Fonts.caption)
65+
private let pageSlider = PDFControlSlider(type: .page)
66+
private let scaleFactorSlider = PDFControlSlider(type: .scaleFactor)
67+
private let fullscreenButton = Button(image: Icons.expand, imageSize: .square(14), foregroundColor: .white)
68+
69+
private var hapticFeedback: HapticFeedback?
70+
private var pageSliderDragging = false
71+
private var scaleFactorSliderDragging = false
72+
73+
override init(frame: CGRect) {
74+
super.init(frame: frame)
75+
76+
initialize()
77+
}
78+
79+
@available(*, unavailable)
80+
required init?(coder: NSCoder) {
81+
fatalError("init(coder:) has not been implemented")
82+
}
83+
84+
private func initialize() {
85+
backgroundColor = Colors.darkTeal
86+
87+
let stackView = HStackView(alignment: .center, spacing: .XUI.spacing2) {
88+
pageLabel
89+
pageSlider
90+
scaleFactorSlider
91+
fullscreenButton
92+
}
93+
addSubview(stackView) { make in
94+
make.left.right.equalToSuperview().inset(CGFloat.XUI.spacing2)
95+
make.top.bottom.equalToSuperview()
96+
}
97+
98+
pageSlider.addTarget(self, action: #selector(Self.startDragging(_:)), for: .touchDown)
99+
pageSlider.addTarget(self, action: #selector(Self.endDragging(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel])
100+
pageSlider.addTarget(self, action: #selector(Self.valueChanged(_:)), for: .valueChanged)
101+
102+
scaleFactorSlider.addTarget(self, action: #selector(Self.startDragging(_:)), for: .touchDown)
103+
scaleFactorSlider.addTarget(self, action: #selector(Self.endDragging(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel])
104+
scaleFactorSlider.addTarget(self, action: #selector(Self.valueChanged(_:)), for: .valueChanged)
105+
106+
fullscreenButton.touchUpInsideAction = { [weak self] _ in
107+
self?.fullscreenHandler?()
108+
}
109+
}
110+
111+
private func updatePageLabel() {
112+
pageLabel.text = "\(currentPageIndex + 1) / \(numberOfPages)"
113+
}
114+
115+
private func updatePageSlider() {
116+
if numberOfPages <= 2 {
117+
pageSlider.isEnabled = false
118+
pageSlider.maximumValue = 1
119+
pageSlider.value = 1
120+
} else {
121+
pageSlider.isEnabled = true
122+
// numberOfPages: 3 4 5 6 7 8 9 10
123+
// maximumValue : 1 1 2 2 3 3 4 4
124+
pageSlider.maximumValue = Float((numberOfPages - 1) / 2)
125+
// currentPageIndex: 0 1 2 3 4 5 6 7
126+
// value : 0 0 1 1 2 2 3 3
127+
pageSlider.value = Float(currentPageIndex / 2)
128+
}
129+
}
130+
131+
@objc private func startDragging(_ slider: PDFControlSlider) {
132+
if slider === pageSlider {
133+
pageSliderDragging = true
134+
hapticFeedback = HapticFeedback(type: .light)
135+
hapticFeedback!.prepare()
136+
137+
} else if slider === scaleFactorSlider {
138+
scaleFactorSliderDragging = true
139+
}
140+
}
141+
142+
@objc private func valueChanged(_ slider: PDFControlSlider) {
143+
if slider === pageSlider {
144+
let expectedPageIndex = Int(round(pageSlider.value)) * 2
145+
146+
if currentPageIndex != expectedPageIndex {
147+
currentPageIndex = expectedPageIndex
148+
hapticFeedback?.trigger()
149+
150+
goPageHandler?(expectedPageIndex)
151+
}
152+
153+
} else if slider === scaleFactorSlider {
154+
updateScaleFactorHandler?(CGFloat(slider.value))
155+
}
156+
}
157+
158+
@objc private func endDragging(_ slider: PDFControlSlider) {
159+
if slider === pageSlider {
160+
pageSliderDragging = false
161+
hapticFeedback = nil
162+
163+
let expectedPageIndex = Int(round(pageSlider.value)) * 2
164+
pageSlider.value = Float(expectedPageIndex / 2)
165+
166+
} else if slider === scaleFactorSlider {
167+
scaleFactorSliderDragging = false
168+
}
169+
}
170+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//
2+
// PDFFullscreenControlBar.swift
3+
// XUI
4+
//
5+
// Created by xueqooy on 2024/12/26.
6+
//
7+
8+
import UIKit
9+
import PDFKit
10+
11+
class PDFFullscreenControlBar: UIView {
12+
var exitHandler: (() -> Void)?
13+
var updateDisplayModeHandler: ((PDFDisplayMode) -> Void)?
14+
15+
var displayMode: PDFDisplayMode = .twoUpContinuous {
16+
didSet {
17+
updateModeButtonImage()
18+
}
19+
}
20+
21+
var numberOfPages: Int = 0 {
22+
didSet {
23+
guard oldValue != numberOfPages else { return }
24+
25+
pageChanged()
26+
}
27+
}
28+
29+
var currentPageIndex: Int = 0 {
30+
didSet {
31+
guard oldValue != currentPageIndex else { return }
32+
33+
pageChanged()
34+
}
35+
}
36+
37+
private lazy var modeButton: Button = {
38+
let button = Button(configuration: .init(imageTransform: CGAffineTransform(rotationAngle: .pi / 2), foregroundColor: .white)) { [weak self] button in
39+
guard let self else { return }
40+
41+
self.displayMode = displayMode == .singlePageContinuous ? .twoUpContinuous : .singlePageContinuous
42+
button.configuration.image = displayMode == .singlePageContinuous ? singleColumnImage : doubleColumnImage
43+
self.updateDisplayModeHandler?(displayMode)
44+
}
45+
button.imageTransition = [.fade, .scale]
46+
button.hitTestSlop = .init(top: -10, left: -6, bottom: -10, right: -6)
47+
48+
return button
49+
}()
50+
51+
private let pageLabel = UILabel(textColor: .white, font: Fonts.body3Bold, textAlignment: .center)
52+
53+
private lazy var exitButton: Button = {
54+
let button = Button(image: Icons.collapse, imageSize: .square(20), foregroundColor: .white) { [weak self] _ in
55+
self?.exitHandler?()
56+
}
57+
button.hitTestSlop = .init(top: -10, left: -6, bottom: -10, right: -6)
58+
return button
59+
}()
60+
61+
private let doubleColumnImage = UIImage(systemName: "rectangle.grid.2x2", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))!
62+
private let singleColumnImage = UIImage(systemName: "rectangle.grid.1x2", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))!
63+
64+
init() {
65+
super.init(frame: .zero)
66+
67+
let colors = [Colors.darkTeal.withAlphaComponent(0.8), Colors.darkTeal.withAlphaComponent(0)]
68+
let gradient = BackgroundConfiguration.Gradient(colors: colors, startPoint: .init(x: 0.5, y: 0), endPoint: CGPoint(x: 0.5, y: 1))
69+
let backgroundView = BackgroundView(configuration: .init(gradient: gradient))
70+
71+
addSubview(backgroundView) { make in
72+
make.edges.equalToSuperview()
73+
}
74+
75+
let stackView = HStackView(alignment: .center, spacing: .XUI.spacing3) {
76+
modeButton
77+
78+
HSpacerView.flexible()
79+
80+
exitButton
81+
}
82+
83+
addSubview(stackView) { make in
84+
make.left.right.equalTo(self.safeAreaLayoutGuide).inset(CGFloat.XUI.spacing3)
85+
make.top.equalTo(self.safeAreaLayoutGuide)
86+
make.bottom.equalToSuperview()
87+
make.height.equalTo(44)
88+
}
89+
90+
addSubview(pageLabel) { make in
91+
make.center.equalTo(stackView)
92+
}
93+
94+
updateModeButtonImage()
95+
}
96+
97+
required init?(coder: NSCoder) {
98+
fatalError("init(coder:) has not been implemented")
99+
}
100+
101+
private func updateModeButtonImage() {
102+
modeButton.configuration.image = displayMode == .singlePageContinuous ? singleColumnImage : doubleColumnImage
103+
}
104+
105+
private func pageChanged() {
106+
modeButton.isHidden = numberOfPages <= 1
107+
108+
if numberOfPages > 1 {
109+
pageLabel.isHidden = false
110+
pageLabel.text = "\(currentPageIndex + 1) / \(numberOfPages)"
111+
} else {
112+
pageLabel.isHidden = true
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)