应用改用 UIKit 重写,删除 SwiftUI 版本

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 22:24:12 +08:00
parent 4e3146f9fe
commit 2220f4be52
25 changed files with 1148 additions and 871 deletions
@@ -0,0 +1,164 @@
import UIKit
final class WordDetailViewController: UIViewController {
private let word: Word
private let speech = SpeechService.shared
private let speakButton = UIButton(type: .system)
private var isSpeaking: Bool { speech.speakingText == word.word }
init(word: Word) {
self.word = word
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
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()
}
}
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
}
private func buildLayout() {
let scrollView = UIScrollView()
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)
])
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 32
scrollView.addSubview(stack)
stack.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)
])
stack.addArrangedSubview(makeHeader())
stack.addArrangedSubview(makeCard())
}
private func makeHeader() -> UIView {
let wordLabel = UILabel()
wordLabel.text = word.word
wordLabel.font = .systemFont(ofSize: 48, weight: .bold)
wordLabel.textAlignment = .center
let phoneticLabel = UILabel()
phoneticLabel.text = word.phonetic
phoneticLabel.font = .preferredFont(forTextStyle: .title3)
phoneticLabel.textColor = .secondaryLabel
phoneticLabel.textAlignment = .center
var config = UIButton.Configuration.borderedProminent()
config.imagePadding = 6
config.contentInsets = .init(top: 10, leading: 24, bottom: 10, trailing: 24)
speakButton.configuration = config
speakButton.addAction(UIAction { [weak self] _ in
guard let self else { return }
speech.speak(word.word)
}, for: .touchUpInside)
let stack = UIStackView(arrangedSubviews: [wordLabel, phoneticLabel, speakButton])
stack.axis = .vertical
stack.spacing = 12
stack.alignment = .center
return stack
}
private func makeCard() -> UIView {
let card = UIView()
card.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.5)
card.layer.cornerRadius = 16
let meaningTitle = makeCaption("释义")
let meaningLabel = UILabel()
meaningLabel.text = word.meaning
meaningLabel.font = .preferredFont(forTextStyle: .title2)
meaningLabel.numberOfLines = 0
let divider = UIView()
divider.backgroundColor = .separator
divider.translatesAutoresizingMaskIntoConstraints = false
divider.heightAnchor.constraint(equalToConstant: 1.0 / UIScreen.main.scale).isActive = true
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
let exampleSpeakButton = UIButton(type: .system)
exampleSpeakButton.setImage(UIImage(systemName: "speaker.wave.2"), for: .normal)
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
}
}