Files
language4english/LearnEnglish/LearnEnglish/Views/WordDetailViewController.swift
T
2026-07-19 21:37:40 +08:00

190 lines
7.8 KiB
Swift

import UIKit
final class WordDetailViewController: UIViewController {
private let word: Word
private let categoryName: String
private let speech = SpeechService.shared
private let speakButton = UIButton(type: .system)
private let exampleSpeakButton = UIButton(type: .system)
private let contentStack = UIStackView()
private var speechObserver: NSObjectProtocol?
init(word: Word, categoryName: String = "") {
self.word = word
self.categoryName = categoryName
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()
view.backgroundColor = .systemBackground
title = categoryName
navigationItem.largeTitleDisplayMode = .never
buildLayout()
updateSpeakState()
speechObserver = NotificationCenter.default.addObserver(
forName: SpeechService.speakingDidChangeNotification, object: nil, queue: .main
) { [weak self] _ in
self?.updateSpeakState()
}
}
private func updateSpeakState() {
let wordSpeaking = speech.isSpeaking(word.word)
speakButton.configuration?.image = UIImage(
systemName: wordSpeaking ? "speaker.wave.3.fill" : "speaker.wave.2.fill",
withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .semibold))
speakButton.isEnabled = !wordSpeaking
let exampleSpeaking = speech.isSpeaking(word.example)
exampleSpeakButton.configuration?.image = UIImage(
systemName: exampleSpeaking ? "speaker.wave.3.fill" : "speaker.wave.2",
withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))
exampleSpeakButton.isEnabled = !exampleSpeaking
}
private func buildLayout() {
let scrollView = UIScrollView()
scrollView.alwaysBounceVertical = true
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
contentStack.axis = .vertical
contentStack.spacing = 32
scrollView.addSubview(contentStack)
contentStack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentStack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 24),
contentStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 16),
contentStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -16),
contentStack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -16),
contentStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32)
])
contentStack.addArrangedSubview(makeHeader())
contentStack.addArrangedSubview(makeCard())
}
private func makeHeader() -> UIView {
let wordLabel = UILabel()
wordLabel.text = word.word
wordLabel.font = Theme.Font.wordHero()
wordLabel.textAlignment = .center
let phoneticIcon = UIImageView(image: UIImage(
systemName: "speaker.wave.2",
withConfiguration: UIImage.SymbolConfiguration(pointSize: 13, weight: .medium)))
phoneticIcon.tintColor = .secondaryLabel
let phoneticLabel = UILabel()
phoneticLabel.text = word.phonetic
phoneticLabel.font = .preferredFont(forTextStyle: .title3)
phoneticLabel.textColor = .secondaryLabel
let phoneticRow = UIStackView(arrangedSubviews: [phoneticIcon, phoneticLabel])
phoneticRow.axis = .horizontal
phoneticRow.alignment = .center
phoneticRow.spacing = 6
var buttonConfig = UIButton.Configuration.filled()
buttonConfig.baseBackgroundColor = Theme.Color.accent
buttonConfig.baseForegroundColor = .white
buttonConfig.cornerStyle = .capsule
speakButton.configuration = buttonConfig
speakButton.addAction(UIAction { [weak self] _ in
guard let self else { return }
speech.speak(word.word)
}, for: .touchUpInside)
speakButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
speakButton.widthAnchor.constraint(equalToConstant: 56),
speakButton.heightAnchor.constraint(equalToConstant: 56)
])
let stack = UIStackView(arrangedSubviews: [wordLabel, phoneticRow, speakButton])
stack.axis = .vertical
stack.spacing = 16
stack.alignment = .center
return stack
}
private func makeCard() -> UIView {
let card = UIView()
card.backgroundColor = Theme.Color.cardBackground
card.layer.cornerRadius = Theme.Metrics.cardRadius
let meaningTitle = makeCaption("释义")
let meaningLabel = UILabel()
meaningLabel.text = word.meaning
meaningLabel.font = .preferredFont(forTextStyle: .title2)
meaningLabel.numberOfLines = 0
let divider = HairlineView()
let exampleTitle = makeCaption("例句")
let exampleLabel = UILabel()
exampleLabel.text = word.example
exampleLabel.numberOfLines = 0
let exampleMeaningLabel = UILabel()
exampleMeaningLabel.text = word.exampleMeaning
exampleMeaningLabel.font = .preferredFont(forTextStyle: .subheadline)
exampleMeaningLabel.textColor = .secondaryLabel
exampleMeaningLabel.numberOfLines = 0
var ghostConfig = UIButton.Configuration.plain()
ghostConfig.baseForegroundColor = Theme.Color.accent
exampleSpeakButton.configuration = ghostConfig
exampleSpeakButton.addAction(UIAction { [weak self] _ in
guard let self else { return }
speech.speak(word.example)
}, for: .touchUpInside)
exampleSpeakButton.setContentHuggingPriority(.required, for: .horizontal)
let exampleTextStack = UIStackView(arrangedSubviews: [exampleLabel, exampleMeaningLabel])
exampleTextStack.axis = .vertical
exampleTextStack.spacing = 4
let exampleRow = UIStackView(arrangedSubviews: [exampleTextStack, exampleSpeakButton])
exampleRow.axis = .horizontal
exampleRow.alignment = .top
exampleRow.spacing = 8
let stack = UIStackView(arrangedSubviews: [meaningTitle, meaningLabel, divider, exampleTitle, exampleRow])
stack.axis = .vertical
stack.spacing = 16
stack.setCustomSpacing(6, after: meaningTitle)
stack.setCustomSpacing(6, after: exampleTitle)
card.addSubview(stack)
stack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 16),
stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16)
])
return card
}
private func makeCaption(_ text: String) -> UILabel {
let label = UILabel()
label.text = text
label.font = .preferredFont(forTextStyle: .caption1)
label.textColor = .secondaryLabel
return label
}
}