타이머 생성/수정 화면에서 이름이 비었을 때 저장을 막고 시각적으로 에러를 보여줘야 함.
타이머 이름은 15자로 제한하고 한글 조합 시 오동작이 없어야 함
단순히 text.isEmpty만 체크하면 한글 조합 중에도 에러가 깜빡거릴 수 있음
shouldChangeCharactersIn에서 길이 제한을 걸면 붙여넣기나 중간 삽입에서 잘리는 로직이 어색해질 수 있음
시각적 에러 상태는 애니메이션/해제 타이밍이 부자연스러울 수 있음
validateNameField()에서 공백 제거 후 빈 문자열이면 빨간 테두리 표시.
nameEditingChanged에서 IME 조합 중(markedTextRange)이면 패스하여 깜빡임 방지
에러 표시/해제는 UIView.animate(withDuration:)로 전환
private func validateNameField() {
let text = (nameTextField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
setNameFieldError(text.isEmpty)
}
@objc private func nameEditingChanged(_ sender: UITextField) {
// IME 조합 중이면 건너뜀
if let marked = sender.markedTextRange,
sender.position(from: marked.start, offset: 0) != nil { return }
validateNameField()
}
private func setNameFieldError(_ show: Bool, animated: Bool = true) {
let updates = {
self.nameTextField.layer.borderWidth = show ? 1 : 0
self.nameTextField.layer.borderColor = show ? self.errorStrokeColor() : UIColor.clear.cgColor
self.nameTextField.layer.cornerRadius = Metrics.cornerRadius
}
animated ? UIView.animate(withDuration: 0.15, animations: updates) : updates()
}
저장 시 비어 있으면 UIImpactFeedbackGenerator(style: .light).impactOccurred()로 햅틱 제공.
⸻
shouldChangeCharactersIn에서 바뀐 결과 문자열을 미리 만든 뒤 길이 판단
초과면 남은 자리수만큼 자른 문자열로 직접 세팅하고 false 반환.
한글 조합 중에는 무조건 허용하여 조합이 끊기지 않게 함.
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool
{
guard textField === nameTextField else { return true }
// IME 조합 중은 제한 미적용
if let marked = textField.markedTextRange,
textField.position(from: marked.start, offset: 0) != nil { return true }
let limit = 15
let current = textField.text ?? ""
guard let swiftRange = Range(range, in: current) else { return true }
let proposed = current.replacingCharacters(in: swiftRange, with: string)
if proposed.count <= limit { return true } // 정상 범위
// 초과 → 남은 길이만 허용
let replacingCount = current[swiftRange].count
let remaining = limit - (current.count - replacingCount)
guard remaining > 0 else {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
return false
}
let allowedPrefix = String(string.prefix(remaining))
let truncated = current.replacingCharacters(in: swiftRange, with: allowedPrefix)
textField.text = truncated
UIImpactFeedbackGenerator(style: .light).impactOccurred()
return false
}