3d2466edba
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
191 lines
7.9 KiB
Swift
191 lines
7.9 KiB
Swift
import UIKit
|
|
|
|
final class WordListViewController: UIViewController {
|
|
private let category: WordCategory
|
|
private let colorIndex: Int
|
|
private let tableView = UITableView(frame: .zero, style: .plain)
|
|
private let ctaBackground = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
|
private let quizButton = UIButton(type: .system)
|
|
private var speechObserver: NSObjectProtocol?
|
|
|
|
init(category: WordCategory, colorIndex: Int) {
|
|
self.category = category
|
|
self.colorIndex = colorIndex
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) { fatalError() }
|
|
|
|
deinit {
|
|
if let speechObserver {
|
|
NotificationCenter.default.removeObserver(speechObserver)
|
|
}
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
title = category.name
|
|
navigationItem.largeTitleDisplayMode = .never
|
|
view.backgroundColor = .systemBackground
|
|
|
|
tableView.dataSource = self
|
|
tableView.delegate = self
|
|
tableView.register(WordCardCell.self, forCellReuseIdentifier: "cell")
|
|
tableView.separatorStyle = .none
|
|
tableView.backgroundColor = .systemBackground
|
|
tableView.tableHeaderView = makeHeader()
|
|
tableView.contentInset.bottom = 92
|
|
tableView.verticalScrollIndicatorInsets.bottom = 92
|
|
|
|
var buttonConfig = UIButton.Configuration.borderedProminent()
|
|
buttonConfig.title = "开始测验"
|
|
buttonConfig.image = UIImage(systemName: "pencil.and.list.clipboard")
|
|
buttonConfig.imagePadding = 6
|
|
buttonConfig.contentInsets = .init(top: 14, leading: 16, bottom: 14, trailing: 16)
|
|
buttonConfig.background.cornerRadius = Theme.Metrics.controlRadius
|
|
buttonConfig.titleTextAttributesTransformer = .init { incoming in
|
|
var outgoing = incoming
|
|
outgoing.font = .preferredFont(forTextStyle: .headline)
|
|
return outgoing
|
|
}
|
|
quizButton.configuration = buttonConfig
|
|
quizButton.addAction(UIAction { [weak self] _ in self?.startQuiz() }, for: .touchUpInside)
|
|
|
|
view.addSubview(tableView)
|
|
view.addSubview(ctaBackground)
|
|
ctaBackground.contentView.addSubview(quizButton)
|
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
|
ctaBackground.translatesAutoresizingMaskIntoConstraints = false
|
|
quizButton.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
|
|
ctaBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
ctaBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
ctaBackground.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
|
|
quizButton.topAnchor.constraint(equalTo: ctaBackground.contentView.topAnchor, constant: 12),
|
|
quizButton.leadingAnchor.constraint(equalTo: ctaBackground.contentView.leadingAnchor, constant: 16),
|
|
quizButton.trailingAnchor.constraint(equalTo: ctaBackground.contentView.trailingAnchor, constant: -16),
|
|
quizButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12)
|
|
])
|
|
|
|
speechObserver = NotificationCenter.default.addObserver(
|
|
forName: SpeechService.speakingDidChangeNotification, object: nil, queue: .main
|
|
) { [weak self] _ in
|
|
self?.updateVisibleSpeakers()
|
|
}
|
|
}
|
|
|
|
private func makeHeader() -> UIView {
|
|
let color = Theme.Color.category(at: colorIndex)
|
|
let header = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 56))
|
|
|
|
let tile = UIView()
|
|
tile.backgroundColor = color.withAlphaComponent(0.12)
|
|
tile.layer.cornerRadius = 10
|
|
let icon = UIImageView(image: UIImage(systemName: category.icon,
|
|
withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .medium)))
|
|
icon.tintColor = color
|
|
icon.contentMode = .scaleAspectFit
|
|
tile.addSubview(icon)
|
|
header.addSubview(tile)
|
|
tile.translatesAutoresizingMaskIntoConstraints = false
|
|
icon.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
let label = UILabel()
|
|
label.text = "共 \(category.words.count) 个单词"
|
|
label.font = .preferredFont(forTextStyle: .subheadline)
|
|
label.textColor = .secondaryLabel
|
|
header.addSubview(label)
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
NSLayoutConstraint.activate([
|
|
tile.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 16),
|
|
tile.centerYAnchor.constraint(equalTo: header.centerYAnchor),
|
|
tile.widthAnchor.constraint(equalToConstant: 32),
|
|
tile.heightAnchor.constraint(equalToConstant: 32),
|
|
icon.centerXAnchor.constraint(equalTo: tile.centerXAnchor),
|
|
icon.centerYAnchor.constraint(equalTo: tile.centerYAnchor),
|
|
label.leadingAnchor.constraint(equalTo: tile.trailingAnchor, constant: 10),
|
|
label.centerYAnchor.constraint(equalTo: header.centerYAnchor)
|
|
])
|
|
return header
|
|
}
|
|
|
|
private func startQuiz() {
|
|
Haptics.selection()
|
|
let words: [Word]
|
|
if category.id == "numbers" {
|
|
words = NumberWords.randomWords(count: 50)
|
|
} else {
|
|
words = category.words
|
|
}
|
|
let request = QuizSetupRequest(words: words)
|
|
navigationController?.pushViewController(QuizSetupViewController(request: request), animated: true)
|
|
}
|
|
|
|
private func updateVisibleSpeakers() {
|
|
for cell in tableView.visibleCells.compactMap({ $0 as? WordCardCell }) {
|
|
cell.updateSpeakerState()
|
|
}
|
|
}
|
|
}
|
|
|
|
extension WordListViewController: UITableViewDataSource, UITableViewDelegate {
|
|
private var sections: [Subcategory]? {
|
|
category.subcategories.flatMap { $0.isEmpty ? nil : $0 }
|
|
}
|
|
|
|
func numberOfSections(in tableView: UITableView) -> Int {
|
|
sections?.count ?? 1
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
|
sections?[section].name
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
if let sections {
|
|
return sections[section].words.count
|
|
}
|
|
return category.words.count
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! WordCardCell
|
|
let word: Word
|
|
if let sections {
|
|
word = sections[indexPath.section].words[indexPath.row]
|
|
} else {
|
|
word = category.words[indexPath.row]
|
|
}
|
|
cell.configure(with: word)
|
|
cell.onSpeak = { word in
|
|
SpeechService.shared.speak(word.word)
|
|
}
|
|
cell.onLearn = { [weak self] word in
|
|
guard let self else { return }
|
|
let config = QuizConfig(mode: .spelling, words: [word], isStudyMode: true)
|
|
navigationController?.pushViewController(QuizViewController(config: config), animated: true)
|
|
}
|
|
return cell
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
Haptics.selection()
|
|
let word: Word
|
|
if let sections {
|
|
word = sections[indexPath.section].words[indexPath.row]
|
|
} else {
|
|
word = category.words[indexPath.row]
|
|
}
|
|
let detail = WordDetailViewController(word: word, categoryName: category.name)
|
|
navigationController?.pushViewController(detail, animated: true)
|
|
}
|
|
}
|