引入设计系统,统一 UI 风格,增加拼写/默写结果状态反馈

This commit is contained in:
2026-07-18 23:55:00 +08:00
parent 1c3d62d46c
commit c3a4990984
15 changed files with 981 additions and 205 deletions
@@ -5,7 +5,10 @@ final class WordDetailViewController: UIViewController {
private let speech = SpeechService.shared
private let speakButton = UIButton(type: .system)
private var isSpeaking: Bool { speech.speakingText == word.word }
private let exampleSpeakButton = UIButton(type: .system)
private let contentStack = UIStackView()
private var speechObserver: NSObjectProtocol?
private var didAnimateEntry = false
init(word: Word) {
self.word = word
@@ -15,28 +18,46 @@ final class WordDetailViewController: UIViewController {
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
deinit {
if let speechObserver {
NotificationCenter.default.removeObserver(speechObserver)
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
title = word.word
navigationItem.largeTitleDisplayMode = .never
buildLayout()
updateSpeakButton()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
speech.onSpeakingChanged = { [weak self] in
self?.updateSpeakButton()
updateSpeakState()
speechObserver = NotificationCenter.default.addObserver(
forName: SpeechService.speakingDidChangeNotification, object: nil, queue: .main
) { [weak self] _ in
self?.updateSpeakState()
}
}
private func updateSpeakButton() {
var config = speakButton.configuration
config?.title = isSpeaking ? "朗读中" : "发音"
config?.image = UIImage(systemName: isSpeaking ? "speaker.wave.3.fill" : "speaker.wave.2.fill")
speakButton.configuration = config
speakButton.isEnabled = !isSpeaking
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !didAnimateEntry {
didAnimateEntry = true
contentStack.animateEntry(distance: 16, duration: 0.35)
}
}
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() {
@@ -50,55 +71,67 @@ final class WordDetailViewController: UIViewController {
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 32
scrollView.addSubview(stack)
stack.translatesAutoresizingMaskIntoConstraints = false
contentStack.axis = .vertical
contentStack.spacing = 32
scrollView.addSubview(contentStack)
contentStack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 16),
stack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 16),
stack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -16),
stack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -16),
stack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32)
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)
])
stack.addArrangedSubview(makeHeader())
stack.addArrangedSubview(makeCard())
contentStack.addArrangedSubview(makeHeader())
contentStack.addArrangedSubview(makeCard())
}
private func makeHeader() -> UIView {
let wordLabel = UILabel()
wordLabel.text = word.word
wordLabel.font = .systemFont(ofSize: 48, weight: .bold)
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
phoneticLabel.textAlignment = .center
let phoneticRow = UIStackView(arrangedSubviews: [phoneticIcon, phoneticLabel])
phoneticRow.axis = .horizontal
phoneticRow.alignment = .center
phoneticRow.spacing = 6
var config = UIButton.Configuration.borderedProminent()
config.imagePadding = 6
config.contentInsets = .init(top: 10, leading: 24, bottom: 10, trailing: 24)
speakButton.configuration = config
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, phoneticLabel, speakButton])
let stack = UIStackView(arrangedSubviews: [wordLabel, phoneticRow, speakButton])
stack.axis = .vertical
stack.spacing = 12
stack.spacing = 16
stack.alignment = .center
return stack
}
private func makeCard() -> UIView {
let card = UIView()
card.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.5)
card.layer.cornerRadius = 16
card.backgroundColor = Theme.Color.cardBackground
card.layer.cornerRadius = Theme.Metrics.cardRadius
let meaningTitle = makeCaption("释义")
let meaningLabel = UILabel()
@@ -118,8 +151,9 @@ final class WordDetailViewController: UIViewController {
exampleMeaningLabel.textColor = .secondaryLabel
exampleMeaningLabel.numberOfLines = 0
let exampleSpeakButton = UIButton(type: .system)
exampleSpeakButton.setImage(UIImage(systemName: "speaker.wave.2"), for: .normal)
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)