Files
language4english/LearnEnglish/LearnEnglish/Views/WordListViewController.swift
T
2026-07-20 22:57:39 +08:00

216 lines
8.7 KiB
Swift

import UIKit
final class WordListViewController: UIViewController {
private let category: WordCategory
private let colorIndex: Int
private var words: [Word]
private var subcategories: [Subcategory]?
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?
private var displaySubcategories: [Subcategory]? {
subcategories.flatMap { $0.isEmpty ? nil : $0 }
}
init(category: WordCategory, colorIndex: Int) {
self.category = category
self.colorIndex = colorIndex
self.words = category.words
self.subcategories = category.subcategories
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
navigationItem.rightBarButtonItem = UIBarButtonItem(
image: UIImage(systemName: "arrow.triangle.2.circlepath"),
style: .plain,
target: self,
action: #selector(shuffleWords)
)
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 quizWords: [Word]
if category.id == "numbers" {
quizWords = NumberWords.randomWords(count: 50)
} else {
quizWords = words
}
let request = QuizSetupRequest(words: quizWords)
navigationController?.pushViewController(QuizSetupViewController(request: request), animated: true)
}
@objc private func shuffleWords() {
Haptics.selection()
if var subs = subcategories {
subs = subs.shuffled().map { sub in
var s = sub
s.words = sub.words.shuffled()
return s
}
subcategories = subs
} else {
words.shuffle()
}
tableView.reloadData()
}
private func updateVisibleSpeakers() {
for cell in tableView.visibleCells.compactMap({ $0 as? WordCardCell }) {
cell.updateSpeakerState()
}
}
}
extension WordListViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
displaySubcategories?.count ?? 1
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
displaySubcategories?[section].name
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let subs = displaySubcategories {
return subs[section].words.count
}
return 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 subs = displaySubcategories {
word = subs[indexPath.section].words[indexPath.row]
} else {
word = 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 subs = displaySubcategories {
word = subs[indexPath.section].words[indexPath.row]
} else {
word = words[indexPath.row]
}
let detail = WordDetailViewController(word: word, categoryName: category.name)
navigationController?.pushViewController(detail, animated: true)
}
}