引入设计系统,统一 UI 风格,增加拼写/默写结果状态反馈
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user