ReadViewSDK/ReadViewDemo/ReadViewDemo/PDFDemoAnnotationUI.swift
shenlei fed34da246 unify PDF and EPUB reader: convert highlights to underlines, improve annotation UI, change paging from horizontal to vertical
- Convert PDF highlights from filled rectangles to underlines matching EPUB style
- Update PDF annotation editor with EPUB-style design: gold sidebar rail, semi-transparent background, improved spacing and shadows, dark mode support
- Change PDF page-curl paging from left-right horizontal to up-down vertical: update gesture recognition and tap regions accordingly

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-15 10:01:23 +09:00

240 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import UIKit
import RDPDFReaderView
/// PDF
final class PDFDemoAnnotationEditorViewController: UIViewController, UITextViewDelegate {
private let quote: String
private let initialNote: String?
private let onSave: (String?) -> Void
private let textView = UITextView()
private let countLabel = UILabel()
init(quote: String, initialNote: String? = nil, onSave: @escaping (String?) -> Void) {
self.quote = quote
self.initialNote = initialNote
self.onSave = onSave
super.init(nibName: nil, bundle: nil)
title = "添加注释"
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
let isDarkMode = traitCollection.userInterfaceStyle == .dark
view.backgroundColor = isDarkMode ? UIColor(red: 0.11, green: 0.11, blue: 0.12, alpha: 1) : .systemBackground
view.accessibilityIdentifier = "pdf.reader.annotation.editor"
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "取消", style: .plain, target: self, action: #selector(cancelAction))
navigationItem.leftBarButtonItem?.accessibilityIdentifier = "pdf.reader.annotation.cancel"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "保存", style: .done, target: self, action: #selector(saveAction))
navigationItem.rightBarButtonItem?.accessibilityIdentifier = "pdf.reader.annotation.save"
navigationController?.navigationBar.tintColor = .systemBlue
let quoteCard = UIView()
let rail = UIView()
let quoteLabel = UILabel()
let textCard = UIView()
[quoteCard, rail, quoteLabel, textCard, textView, countLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(quoteCard)
quoteCard.addSubview(rail)
quoteCard.addSubview(quoteLabel)
view.addSubview(textCard)
textCard.addSubview(textView)
textCard.addSubview(countLabel)
let contentBgColor = isDarkMode ? UIColor(red: 0.13, green: 0.13, blue: 0.14, alpha: 1) : UIColor(red: 0.97, green: 0.97, blue: 0.99, alpha: 1)
let textColor = isDarkMode ? UIColor(red: 0.92, green: 0.92, blue: 0.96, alpha: 1) : .label
quoteCard.backgroundColor = contentBgColor.withAlphaComponent(0.78)
quoteCard.layer.cornerRadius = 16
rail.backgroundColor = UIColor(red: 0.94, green: 0.63, blue: 0.18, alpha: 1)
rail.layer.cornerRadius = 2
quoteLabel.text = quote
quoteLabel.numberOfLines = 5
quoteLabel.font = UIFont.systemFont(ofSize: 16)
quoteLabel.textColor = textColor.withAlphaComponent(0.85)
textCard.backgroundColor = contentBgColor
textCard.layer.cornerRadius = 18
textCard.layer.shadowColor = UIColor.black.cgColor
textCard.layer.shadowOpacity = 0.06
textCard.layer.shadowRadius = 18
textCard.layer.shadowOffset = CGSize(width: 0, height: 6)
textView.text = initialNote
textView.delegate = self
textView.font = UIFont.systemFont(ofSize: 17)
textView.textColor = textColor
textView.backgroundColor = .clear
textView.accessibilityIdentifier = "pdf.reader.annotation.input"
countLabel.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
countLabel.textColor = .secondaryLabel
countLabel.textAlignment = .right
NSLayoutConstraint.activate([
quoteCard.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
quoteCard.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
quoteCard.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
rail.leadingAnchor.constraint(equalTo: quoteCard.leadingAnchor, constant: 16),
rail.topAnchor.constraint(equalTo: quoteCard.topAnchor, constant: 16),
rail.bottomAnchor.constraint(equalTo: quoteCard.bottomAnchor, constant: -16),
rail.widthAnchor.constraint(equalToConstant: 4),
quoteLabel.leadingAnchor.constraint(equalTo: quoteCard.leadingAnchor, constant: 28),
quoteLabel.trailingAnchor.constraint(equalTo: quoteCard.trailingAnchor, constant: -16),
quoteLabel.topAnchor.constraint(equalTo: quoteCard.topAnchor, constant: 16),
quoteLabel.bottomAnchor.constraint(equalTo: quoteCard.bottomAnchor, constant: -16),
textCard.topAnchor.constraint(equalTo: quoteCard.bottomAnchor, constant: 20),
textCard.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
textCard.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
textCard.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -20),
textCard.heightAnchor.constraint(greaterThanOrEqualToConstant: 240),
textView.topAnchor.constraint(equalTo: textCard.topAnchor, constant: 16),
textView.leadingAnchor.constraint(equalTo: textCard.leadingAnchor, constant: 16),
textView.trailingAnchor.constraint(equalTo: textCard.trailingAnchor, constant: -16),
textView.bottomAnchor.constraint(equalTo: countLabel.topAnchor, constant: -12),
countLabel.leadingAnchor.constraint(equalTo: textCard.leadingAnchor, constant: 16),
countLabel.trailingAnchor.constraint(equalTo: textCard.trailingAnchor, constant: -16),
countLabel.bottomAnchor.constraint(equalTo: textCard.bottomAnchor, constant: -12)
])
updateCount()
DispatchQueue.main.async { [weak self] in self?.textView.becomeFirstResponder() }
}
func textViewDidChange(_ textView: UITextView) {
if textView.text.count > 1000 { textView.text = String(textView.text.prefix(1000)) }
updateCount()
}
private func updateCount() { countLabel.text = "\(textView.text.count)/1000" }
@objc private func cancelAction() { closeEditor() }
@objc private func saveAction() {
let note = textView.text.trimmingCharacters(in: .whitespacesAndNewlines)
onSave(note.isEmpty ? nil : note)
closeEditor()
}
private func closeEditor() {
guard let navigationController else {
dismiss(animated: true)
return
}
if navigationController.viewControllers.first === self {
navigationController.dismiss(animated: true)
} else {
navigationController.popViewController(animated: true)
}
}
}
///
final class PDFDemoAnnotationListViewController: UITableViewController {
var onSelectAnnotation: ((RDPDFReaderAnnotation) -> Void)?
var onDeleteAnnotation: ((RDPDFReaderAnnotation) -> Void)?
var onDone: (() -> Void)?
private let annotationsProvider: () -> [RDPDFReaderAnnotation]
private let filterControl = UISegmentedControl(items: ["全部", "注释", "高亮"])
private var annotations: [RDPDFReaderAnnotation] = []
init(annotationsProvider: @escaping () -> [RDPDFReaderAnnotation]) {
self.annotationsProvider = annotationsProvider
super.init(style: .plain)
title = "笔记"
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
view.accessibilityIdentifier = "pdf.reader.annotations.panel"
tableView.accessibilityIdentifier = "pdf.reader.annotations.table"
tableView.tableFooterView = UIView()
filterControl.selectedSegmentIndex = 0
filterControl.accessibilityIdentifier = "pdf.reader.annotations.filter"
filterControl.addTarget(self, action: #selector(filterChanged), for: .valueChanged)
navigationItem.titleView = filterControl
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "完成", style: .done, target: self, action: #selector(doneAction))
reloadAnnotations()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
reloadAnnotations()
}
private var filteredAnnotations: [RDPDFReaderAnnotation] {
switch filterControl.selectedSegmentIndex {
case 1: return annotations.filter { !($0.note?.isEmpty ?? true) }
case 2: return annotations.filter { $0.note?.isEmpty ?? true }
default: return annotations
}
}
private func reloadAnnotations() {
annotations = annotationsProvider().sorted { $0.updatedAt > $1.updatedAt }
tableView.reloadData()
updateEmptyState()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { filteredAnnotations.count }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "annotation") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "annotation")
let annotation = filteredAnnotations[indexPath.row]
cell.textLabel?.numberOfLines = 2
cell.detailTextLabel?.numberOfLines = 2
cell.textLabel?.text = annotation.selectedText?.isEmpty == false ? annotation.selectedText : "区域标注"
let note = annotation.note?.trimmingCharacters(in: .whitespacesAndNewlines)
cell.detailTextLabel?.text = ["\(annotation.pageIndex + 1)", note].compactMap { $0 }.joined(separator: " · ")
cell.imageView?.image = UIImage(systemName: note?.isEmpty == false ? "note.text" : "highlighter")
cell.imageView?.tintColor = UIColor(rdPDFHexString: annotation.color)
cell.accessibilityIdentifier = "pdf.reader.annotations.row.\(annotation.id)"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let annotation = filteredAnnotations[indexPath.row]
tableView.deselectRow(at: indexPath, animated: true)
onSelectAnnotation?(annotation)
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { true }
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
guard editingStyle == .delete else { return }
let annotation = filteredAnnotations[indexPath.row]
onDeleteAnnotation?(annotation)
reloadAnnotations()
}
private func updateEmptyState() {
guard filteredAnnotations.isEmpty else { tableView.backgroundView = nil; return }
let label = UILabel()
label.text = filterControl.selectedSegmentIndex == 1 ? "暂无注释" : "暂无标注"
label.textAlignment = .center
label.textColor = .secondaryLabel
label.accessibilityIdentifier = "pdf.reader.annotations.empty"
tableView.backgroundView = label
}
@objc private func filterChanged() { reloadAnnotations() }
@objc private func doneAction() { onDone?() }
}
extension UIColor {
convenience init(rdPDFHexString: String) {
let hex = rdPDFHexString.replacingOccurrences(of: "#", with: "")
let value = UInt32(hex, radix: 16) ?? 0xF4C542
self.init(
red: CGFloat((value >> 16) & 0xFF) / 255,
green: CGFloat((value >> 8) & 0xFF) / 255,
blue: CGFloat(value & 0xFF) / 255,
alpha: 1
)
}
}