拼写改为确认按钮手动提交,点击已填字母可删除重输
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -89,7 +89,7 @@ final class QuizViewController: UIViewController {
|
|||||||
var nextConfig = UIButton.Configuration.borderedProminent()
|
var nextConfig = UIButton.Configuration.borderedProminent()
|
||||||
nextConfig.contentInsets = .init(top: 12, leading: 16, bottom: 12, trailing: 16)
|
nextConfig.contentInsets = .init(top: 12, leading: 16, bottom: 12, trailing: 16)
|
||||||
nextButton.configuration = nextConfig
|
nextButton.configuration = nextConfig
|
||||||
nextButton.addAction(UIAction { [weak self] _ in self?.next() }, for: .touchUpInside)
|
nextButton.addAction(UIAction { [weak self] _ in self?.primaryButtonTapped() }, for: .touchUpInside)
|
||||||
|
|
||||||
let headerStack = UIStackView(arrangedSubviews: [progressView, countLabel])
|
let headerStack = UIStackView(arrangedSubviews: [progressView, countLabel])
|
||||||
headerStack.axis = .vertical
|
headerStack.axis = .vertical
|
||||||
@@ -136,7 +136,6 @@ final class QuizViewController: UIViewController {
|
|||||||
|
|
||||||
resultLabel.isHidden = true
|
resultLabel.isHidden = true
|
||||||
resultPhoneticLabel.isHidden = true
|
resultPhoneticLabel.isHidden = true
|
||||||
nextButton.isHidden = true
|
|
||||||
|
|
||||||
let isSpelling = config.mode == .spelling
|
let isSpelling = config.mode == .spelling
|
||||||
optionsStack.isHidden = isSpelling
|
optionsStack.isHidden = isSpelling
|
||||||
@@ -146,11 +145,16 @@ final class QuizViewController: UIViewController {
|
|||||||
if isSpelling {
|
if isSpelling {
|
||||||
spellingHintLabel.text = "\(question.word.word.count) 个字母,补全空缺部分"
|
spellingHintLabel.text = "\(question.word.word.count) 个字母,补全空缺部分"
|
||||||
spellingView.configure(blankedWord: question.blankedWord, blankPositions: question.blankPositions)
|
spellingView.configure(blankedWord: question.blankedWord, blankPositions: question.blankPositions)
|
||||||
spellingView.onComplete = { [weak self] typed in
|
spellingView.onChange = { [weak self] in
|
||||||
self?.submitSpelling(typed, for: question)
|
guard let self else { return }
|
||||||
|
nextButton.isEnabled = spellingView.isComplete
|
||||||
}
|
}
|
||||||
|
nextButton.configuration?.title = "确认"
|
||||||
|
nextButton.isEnabled = false
|
||||||
|
nextButton.isHidden = false
|
||||||
spellingView.activate()
|
spellingView.activate()
|
||||||
} else {
|
} else {
|
||||||
|
nextButton.isHidden = true
|
||||||
buildOptionButtons(for: question)
|
buildOptionButtons(for: question)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,9 +225,19 @@ final class QuizViewController: UIViewController {
|
|||||||
resultLabel.isHidden = false
|
resultLabel.isHidden = false
|
||||||
|
|
||||||
nextButton.configuration?.title = currentIndex + 1 < questions.count ? "下一题" : "查看成绩"
|
nextButton.configuration?.title = currentIndex + 1 < questions.count ? "下一题" : "查看成绩"
|
||||||
|
nextButton.isEnabled = true
|
||||||
nextButton.isHidden = false
|
nextButton.isHidden = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func primaryButtonTapped() {
|
||||||
|
if config.mode == .spelling, selectedOption == nil {
|
||||||
|
guard let question = current, spellingView.isComplete else { return }
|
||||||
|
submitSpelling(spellingView.typedWord(), for: question)
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func next() {
|
private func next() {
|
||||||
if currentIndex + 1 < questions.count {
|
if currentIndex + 1 < questions.count {
|
||||||
currentIndex += 1
|
currentIndex += 1
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class SpellingInputView: UIView, UITextFieldDelegate {
|
final class SpellingInputView: UIView, UITextFieldDelegate {
|
||||||
var onComplete: ((String) -> Void)?
|
var onChange: (() -> Void)?
|
||||||
|
|
||||||
private let hiddenField = UITextField()
|
private let hiddenField = UITextField()
|
||||||
private let stack = UIStackView()
|
private let stack = UIStackView()
|
||||||
@@ -9,16 +9,21 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
|
|||||||
private var chars: [String] = []
|
private var chars: [String] = []
|
||||||
private var blankPositions: [Int] = []
|
private var blankPositions: [Int] = []
|
||||||
private var filledBlanks: [String] = []
|
private var filledBlanks: [String] = []
|
||||||
private var currentBlankIndex = 0
|
private var cursorIndex = 0
|
||||||
private var boxViews: [UIView] = []
|
private var boxViews: [UIView] = []
|
||||||
private var cursorView: UIView?
|
private var cursorView: UIView?
|
||||||
private var inputEnabled = true
|
private var inputEnabled = true
|
||||||
|
|
||||||
|
var isComplete: Bool {
|
||||||
|
!filledBlanks.isEmpty && filledBlanks.allSatisfy { !$0.isEmpty }
|
||||||
|
}
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
stack.axis = .horizontal
|
stack.axis = .horizontal
|
||||||
stack.spacing = 6
|
stack.spacing = 6
|
||||||
stack.alignment = .center
|
stack.alignment = .center
|
||||||
|
stack.isUserInteractionEnabled = true
|
||||||
addSubview(stack)
|
addSubview(stack)
|
||||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
@@ -47,11 +52,19 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
|
|||||||
chars = blankedWord.map { String($0) }
|
chars = blankedWord.map { String($0) }
|
||||||
self.blankPositions = blankPositions
|
self.blankPositions = blankPositions
|
||||||
filledBlanks = Array(repeating: "", count: blankPositions.count)
|
filledBlanks = Array(repeating: "", count: blankPositions.count)
|
||||||
currentBlankIndex = 0
|
cursorIndex = 0
|
||||||
inputEnabled = true
|
inputEnabled = true
|
||||||
rebuildBoxes()
|
rebuildBoxes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func typedWord() -> String {
|
||||||
|
var rebuilt = chars
|
||||||
|
for (i, pos) in blankPositions.enumerated() {
|
||||||
|
rebuilt[pos] = filledBlanks[i]
|
||||||
|
}
|
||||||
|
return rebuilt.joined().lowercased()
|
||||||
|
}
|
||||||
|
|
||||||
func activate() {
|
func activate() {
|
||||||
guard inputEnabled, !hiddenField.isFirstResponder else { return }
|
guard inputEnabled, !hiddenField.isFirstResponder else { return }
|
||||||
hiddenField.becomeFirstResponder()
|
hiddenField.becomeFirstResponder()
|
||||||
@@ -72,22 +85,17 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
|
|||||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||||
guard inputEnabled else { return false }
|
guard inputEnabled else { return false }
|
||||||
if string.isEmpty {
|
if string.isEmpty {
|
||||||
if currentBlankIndex > 0 {
|
if cursorIndex > 0 {
|
||||||
currentBlankIndex -= 1
|
cursorIndex -= 1
|
||||||
filledBlanks[currentBlankIndex] = ""
|
filledBlanks[cursorIndex] = ""
|
||||||
refreshBoxes()
|
refreshBoxes()
|
||||||
|
onChange?()
|
||||||
}
|
}
|
||||||
} else if let char = string.last, char.isLetter, currentBlankIndex < blankPositions.count {
|
} else if let char = string.last, char.isLetter, cursorIndex < blankPositions.count {
|
||||||
filledBlanks[currentBlankIndex] = String(char).lowercased()
|
filledBlanks[cursorIndex] = String(char).lowercased()
|
||||||
currentBlankIndex += 1
|
cursorIndex += 1
|
||||||
refreshBoxes()
|
refreshBoxes()
|
||||||
if currentBlankIndex == blankPositions.count {
|
onChange?()
|
||||||
var rebuilt = chars
|
|
||||||
for (i, pos) in blankPositions.enumerated() {
|
|
||||||
rebuilt[pos] = filledBlanks[i]
|
|
||||||
}
|
|
||||||
onComplete?(rebuilt.joined().lowercased())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
textField.text = nil
|
textField.text = nil
|
||||||
return false
|
return false
|
||||||
@@ -99,10 +107,12 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
|
|||||||
stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
boxViews = chars.enumerated().map { index, ch in
|
boxViews = chars.enumerated().map { index, ch in
|
||||||
let box = makeBox()
|
let box = makeBox()
|
||||||
if ch != "_", let label = box.subviews.compactMap({ $0 as? UILabel }).first {
|
box.tag = index
|
||||||
|
if ch != "_", let label = box.viewWithTag(100) as? UILabel {
|
||||||
label.text = ch.uppercased()
|
label.text = ch.uppercased()
|
||||||
label.isHidden = false
|
label.isHidden = false
|
||||||
}
|
}
|
||||||
|
box.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(boxTapped(_:))))
|
||||||
stack.addArrangedSubview(box)
|
stack.addArrangedSubview(box)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
box.widthAnchor.constraint(equalToConstant: 36),
|
box.widthAnchor.constraint(equalToConstant: 36),
|
||||||
@@ -113,6 +123,20 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
|
|||||||
refreshBoxes()
|
refreshBoxes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func boxTapped(_ gesture: UITapGestureRecognizer) {
|
||||||
|
guard inputEnabled, let box = gesture.view else { return }
|
||||||
|
let index = box.tag
|
||||||
|
guard chars[index] == "_", let blankIndex = blankPositions.firstIndex(of: index) else {
|
||||||
|
activate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filledBlanks[blankIndex] = ""
|
||||||
|
cursorIndex = blankIndex
|
||||||
|
refreshBoxes()
|
||||||
|
onChange?()
|
||||||
|
activate()
|
||||||
|
}
|
||||||
|
|
||||||
private func makeBox() -> UIView {
|
private func makeBox() -> UIView {
|
||||||
let box = UIView()
|
let box = UIView()
|
||||||
box.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.15)
|
box.backgroundColor = .quaternarySystemFill.withAlphaComponent(0.15)
|
||||||
@@ -147,13 +171,14 @@ final class SpellingInputView: UIView, UITextFieldDelegate {
|
|||||||
label?.text = filled.uppercased()
|
label?.text = filled.uppercased()
|
||||||
label?.isHidden = filled.isEmpty
|
label?.isHidden = filled.isEmpty
|
||||||
|
|
||||||
let isCurrent = inputEnabled && blankIndex == currentBlankIndex
|
let isCurrent = inputEnabled && blankIndex == cursorIndex
|
||||||
box.layer.borderColor = isCurrent ? UIColor.tintColor.cgColor : UIColor.separator.cgColor
|
box.layer.borderColor = isCurrent ? UIColor.tintColor.cgColor : UIColor.separator.cgColor
|
||||||
box.layer.borderWidth = isCurrent ? 2 : 1
|
box.layer.borderWidth = isCurrent ? 2 : 1
|
||||||
|
|
||||||
if isCurrent, filled.isEmpty {
|
if isCurrent, filled.isEmpty {
|
||||||
let cursor = UIView()
|
let cursor = UIView()
|
||||||
cursor.backgroundColor = .tintColor
|
cursor.backgroundColor = .tintColor
|
||||||
|
cursor.isUserInteractionEnabled = false
|
||||||
box.addSubview(cursor)
|
box.addSubview(cursor)
|
||||||
cursor.translatesAutoresizingMaskIntoConstraints = false
|
cursor.translatesAutoresizingMaskIntoConstraints = false
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
|||||||
Reference in New Issue
Block a user