ReadViewSDK/Sources/RDReaderView/EPUBUI/RDEPUBReaderSearchBarView.swift
shenlei 0e7c0577e3 feat: add in-reader search and restructure documentation
- Add RDEPUBReaderSearchBarView with animated show/hide, keyword
  navigation, and match counting integrated into the reader controller
- Restructure docs: replace scattered design docs with consolidated
  BUSINESS_LOGIC.md and UML_CLASS_DIAGRAMS.md; update ARCHITECTURE.md
- Add SearchTests and FanrenParseTimeTest; enhance LargeBookOnDemandTests
- Add scripts/run_ui_regression.sh and summarize_ui_results.py for
  automated UI test execution and reporting

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 17:34:50 +08:00

252 lines
9.6 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
// MARK: -
///
/// /
final class RDEPUBReaderSearchBarView: RDEPUBReaderToolView {
// MARK:
///
var onSearchSubmit: ((String) -> Void)?
///
var onSearchPrevious: (() -> Void)?
///
var onSearchNext: (() -> Void)?
///
var onClose: (() -> Void)?
// MARK: UI
private let containerView: UIView = {
let view = UIView()
view.layer.cornerRadius = 8
view.layer.masksToBounds = true
view.isAccessibilityElement = false
view.accessibilityElementsHidden = false
return view
}()
private let searchIcon: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 14, weight: .medium)
if #available(iOS 13.0, *) {
imageView.image = UIImage(systemName: "magnifyingglass")
}
imageView.tintColor = .gray
return imageView
}()
let textField: UITextField = {
let field = UITextField()
field.placeholder = "搜索..."
field.font = UIFont.systemFont(ofSize: 15)
field.returnKeyType = .search
field.autocorrectionType = .no
field.autocapitalizationType = .none
field.clearButtonMode = .whileEditing
field.isAccessibilityElement = true
if #available(iOS 13.0, *) {
field.accessibilityTraits = .searchField
}
return field
}()
private let previousButton = RDEPUBReaderTintButton(type: .system)
private let nextButton = RDEPUBReaderTintButton(type: .system)
private let countLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 13, weight: .medium)
label.textAlignment = .center
label.setContentHuggingPriority(.required, for: .horizontal)
label.setContentCompressionResistancePriority(.required, for: .horizontal)
return label
}()
private let closeButton = RDEPUBReaderTintButton(type: .system)
// MARK:
private let horizontalInset: CGFloat = 12
private let spacing: CGFloat = 6
private let containerHeight: CGFloat = 36
override init(frame: CGRect) {
super.init(frame: frame)
accessibilityIdentifier = "epub.reader.search.bar"
shouldGroupAccessibilityChildren = false
isAccessibilityElement = false
setupSubviews()
setupConstraints()
setupActions()
updateNavigationEnabled(false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK:
override func lineFrame(in bounds: CGRect) -> CGRect {
CGRect(x: 0, y: bounds.height - 0.5, width: bounds.width, height: 0.5)
}
override func apply(theme: RDEPUBReaderTheme) {
super.apply(theme: theme)
backgroundColor = theme.toolBackgroundColor
containerView.backgroundColor = theme.toolControlBorderUnselectColor
searchIcon.tintColor = theme.toolControlTextColor
textField.textColor = theme.toolControlTextColor
textField.attributedPlaceholder = NSAttributedString(
string: "搜索...",
attributes: [.foregroundColor: theme.toolControlTextColor.withAlphaComponent(0.5)]
)
countLabel.textColor = theme.toolControlTextColor
previousButton.tintColor = theme.toolControlTextColor
nextButton.tintColor = theme.toolControlTextColor
closeButton.tintColor = theme.toolControlTextColor
}
// MARK:
///
func updateMatchCount(current: Int, total: Int) {
countLabel.text = "\(current)/\(total)"
updateNavigationEnabled(total > 0)
}
///
func showNoResults() {
countLabel.text = "0/0"
updateNavigationEnabled(false)
}
///
func showSearching() {
countLabel.text = "搜索中..."
updateNavigationEnabled(false)
}
///
func restoreKeyword(_ keyword: String) {
textField.text = keyword
}
// MARK:
private func setupSubviews() {
addSubview(containerView)
containerView.addSubview(searchIcon)
containerView.addSubview(textField)
addSubview(previousButton)
addSubview(nextButton)
addSubview(countLabel)
addSubview(closeButton)
if #available(iOS 13.0, *) {
previousButton.setImage(UIImage(systemName: "chevron.up")?.withRenderingMode(.alwaysTemplate), for: .normal)
nextButton.setImage(UIImage(systemName: "chevron.down")?.withRenderingMode(.alwaysTemplate), for: .normal)
closeButton.setImage(UIImage(systemName: "xmark")?.withRenderingMode(.alwaysTemplate), for: .normal)
} else {
previousButton.setTitle("", for: .normal)
nextButton.setTitle("", for: .normal)
closeButton.setTitle("", for: .normal)
}
previousButton.accessibilityIdentifier = "epub.reader.search.previous"
nextButton.accessibilityIdentifier = "epub.reader.search.next"
closeButton.accessibilityIdentifier = "epub.reader.search.close"
countLabel.accessibilityIdentifier = "epub.reader.search.count"
textField.accessibilityIdentifier = "epub.reader.search.field"
[previousButton, nextButton, closeButton].forEach { button in
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .medium)
button.tintColor = .black
button.setTitleColor(.black, for: .normal)
}
}
private func setupConstraints() {
[containerView, searchIcon, textField, previousButton, nextButton, countLabel, closeButton].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
//
containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
containerView.centerYAnchor.constraint(equalTo: centerYAnchor),
containerView.heightAnchor.constraint(equalToConstant: containerHeight),
//
searchIcon.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10),
searchIcon.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
searchIcon.widthAnchor.constraint(equalToConstant: 16),
//
textField.leadingAnchor.constraint(equalTo: searchIcon.trailingAnchor, constant: 6),
textField.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8),
textField.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
textField.heightAnchor.constraint(equalToConstant: containerHeight - 4),
//
previousButton.leadingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: spacing),
previousButton.centerYAnchor.constraint(equalTo: centerYAnchor),
previousButton.widthAnchor.constraint(equalToConstant: 32),
previousButton.heightAnchor.constraint(equalToConstant: 32),
//
nextButton.leadingAnchor.constraint(equalTo: previousButton.trailingAnchor, constant: spacing),
nextButton.centerYAnchor.constraint(equalTo: centerYAnchor),
nextButton.widthAnchor.constraint(equalToConstant: 32),
nextButton.heightAnchor.constraint(equalToConstant: 32),
//
countLabel.leadingAnchor.constraint(equalTo: nextButton.trailingAnchor, constant: spacing),
countLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
countLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44),
//
closeButton.leadingAnchor.constraint(equalTo: countLabel.trailingAnchor, constant: spacing),
closeButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
closeButton.centerYAnchor.constraint(equalTo: centerYAnchor),
closeButton.widthAnchor.constraint(equalToConstant: 32),
closeButton.heightAnchor.constraint(equalToConstant: 32)
])
}
private func setupActions() {
textField.addTarget(self, action: #selector(textFieldDidReturn), for: .editingDidEndOnExit)
previousButton.addTarget(self, action: #selector(previousAction), for: .touchUpInside)
nextButton.addTarget(self, action: #selector(nextAction), for: .touchUpInside)
closeButton.addTarget(self, action: #selector(closeAction), for: .touchUpInside)
}
private func updateNavigationEnabled(_ enabled: Bool) {
previousButton.isEnabled = enabled
previousButton.alpha = enabled ? 1 : 0.45
nextButton.isEnabled = enabled
nextButton.alpha = enabled ? 1 : 0.45
}
@objc private func textFieldDidReturn() {
guard let keyword = textField.text, !keyword.isEmpty else { return }
onSearchSubmit?(keyword)
textField.resignFirstResponder()
}
@objc private func previousAction() {
onSearchPrevious?()
}
@objc private func nextAction() {
onSearchNext?()
}
@objc private func closeAction() {
onClose?()
}
}