입력 검증 UI(에러 스트로크) & 15자 입력 제한

hyun·2025년 9월 2일
0

iOS

목록 보기
48/54

배경

타이머 생성/수정 화면에서 이름이 비었을 때 저장을 막고 시각적으로 에러를 보여줘야 함.
타이머 이름은 15자로 제한하고 한글 조합 시 오동작이 없어야 함

문제

단순히 text.isEmpty만 체크하면 한글 조합 중에도 에러가 깜빡거릴 수 있음
shouldChangeCharactersIn에서 길이 제한을 걸면 붙여넣기나 중간 삽입에서 잘리는 로직이 어색해질 수 있음
시각적 에러 상태는 애니메이션/해제 타이밍이 부자연스러울 수 있음

해결

1) 빈 값 에러 스트로크

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()로 햅틱 제공.

2) 15자 제한

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
}

0개의 댓글