목차
- 개요
- 화면 구성 & 아키텍처 개요
- 핵심 코드 라인-by-라인 해설
- 입력 처리(로케일/포맷) 설계 포인트
- BMI 계산 흐름과 결과 표시
- UX 디테일(키보드, 포커스, 터치 처리)
- 확장: BMI 분류 버튼 액션 구현 예시
- 실습 체크리스트 & 흔한 오류(KVC, 아울렛 연결)
- 마무리 팁
1) 개요
이 글은 아래 ViewController 코드가 어떤 책임을 갖고, 어떤 흐름으로 동작하며, 어디를 확장하면 되는지를 초보 개발자 관점에서 차근차근 설명합니다.
작성 언어는 Swift + UIKit, 입력은 UITextField, 출력은 UILabel이며 BMI 계산은 별도 모델(BMI)을 호출하는 구조를 가정합니다.
2) 화면 구성 & 아키텍처 개요
- 입력 필드 2개: txtHeight(키 cm), txtWeight(몸무게 kg)
- 결과 라벨: lblResult
- 버튼 2개(가정): “계산”(calcBmi)과 “분류”(classificationForBMI)
- 아키텍처 감각:
- Controller는 사용자 입력을 검증하고, **도메인 로직(BMI 계산)**은 BMI 타입에 위임합니다.
- 이렇게 분리하면 테스트가 쉬워지고, 유지보수성이 좋아집니다.
3) 핵심 코드 라인-by-라인 해설
class ViewController: UIViewController, UITextFieldDelegate {
- UIViewController 상속 + UITextFieldDelegate 채택 → 텍스트필드 리턴키 동작 등 입력 제어 가능.
@IBOutlet weak var txtHeight: UITextField!
@IBOutlet weak var txtWeight: UITextField!
@IBOutlet weak var lblResult: UILabel!
- 스토리보드와 연결되는 아울렛. Xcode Interface Builder에서 ViewController에 드래그로 연결해야 합니다.
- 연결 불일치 시 런타임에 KVC 에러(예: “not key value coding-compliant”)가 발생합니다. (8장 참고)
override func viewDidLoad() {
super.viewDidLoad()
txtHeight.placeholder = "키(cm)를 입력하세요"
txtWeight.placeholder = "몸무게(kg)를 입력하세요"
txtHeight.keyboardType = .decimalPad
txtWeight.keyboardType = .decimalPad
txtHeight.delegate = self
txtWeight.delegate = self
lblResult.text = "키와 몸무게를 입력한 뒤 계산 버튼을 눌러주세요"
}
- 초기 UI 세팅:
- placeholder로 입력 가이드 제공.
- decimalPad 키패드 → 숫자/소수점만 입력되도록 UX 개선.
- delegate 지정 → 리턴키 이동/계산 자동 트리거 가능.
- 결과 라벨에 초기 안내 문구 세팅.
@IBAction func calcBmi(_ sender: UIButton) {
view.endEditing(true)
- 키보드 내리기: 버튼 탭 시 편집 종료로 키보드 숨김.
guard
let h = parseNumber(from: txtHeight.text),
let w = parseNumber(from: txtWeight.text),
h > 0, w > 0
else {
lblResult.text = "유효한 키(cm)와 몸무게(kg)를 입력하세요"
return
}
- 입력 검증: 숫자 파싱 실패 또는 0 이하 값이면 즉시 사용자에게 피드백.
- parseNumber로 공백 제거, 쉼표→점 치환 등 유연한 입력 허용.
let bmi = BMI(weight: w, height: h)
lblResult.text = bmi.calcBMI()
}
- **도메인 객체(BMI)**에 계산 위임 → 컨트롤러가 비즈니스 로직을 직접 갖지 않음(좋은 분리).
private func parseNumber(from text: String?) -> Double? {
let cleaned = (text ?? "")
.replacingOccurrences(of: " ", with: "")
.replacingOccurrences(of: ",", with: ".")
return Double(cleaned)
}
- 문자열 전처리로 파싱 내구성 강화:
- 공백 제거
- 소수 구분자 **, → .**로 통일(일부 로케일 사용자가 쉼표를 소수점으로 입력하는 상황 대응)
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField == txtHeight {
txtWeight.becomeFirstResponder()
} else {
textField.resignFirstResponder()
calcBmi(UIButton())
}
return true
}
- 리턴키 UX:
- 키 입력 흐름: 키 → 몸무게로 포커스 이동
- 몸무게에서 리턴키: 포커스 내리고 바로 계산 호출(빠른 입력 흐름)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
view.endEditing(true)
}
- 화면 빈 공간 터치 시 키보드 내려가기: 모바일에서 필수 UX 디테일.
@IBAction func classificationForBMI(_ sender: UIButton) {
}
- 확장 포인트: 계산된 BMI 값을 **범주(저체중/정상/과체중/비만)**로 보여줄 버튼 액션. 아래 7장에서 예시 제공.
4) 입력 처리(로케일/포맷) 설계 포인트
- 사용자가 170,5처럼 쉼표 소수점을 입력할 수 있습니다.
- parseNumber가 쉼표를 점으로 바꿔 Double 파싱을 시도하므로 국내/유럽권 혼용 입력에 꽤 관대합니다.
- 더 정교하게 하려면 NumberFormatter를 쓰고 현재 로케일(Locale.current)을 반영하는 것도 좋습니다.
5) BMI 계산 흐름과 결과 표시
- 컨트롤러는 입력 검증과 흐름 제어만 담당하고, 실제 계산은 BMI 타입에 맡깁니다.
- 예시 BMI 타입(참고용):
struct BMI {
let weight: Double // kg
let height: Double // cm
func calcBMI() -> String {
let m = height / 100.0
guard m > 0 else { return "키가 올바르지 않습니다." }
let value = weight / (m * m)
let rounded = (value * 10).rounded() / 10 // 소수점 1자리
return "BMI: \(rounded)"
}
func category() -> String {
let m = height / 100.0
guard m > 0 else { return "키가 올바르지 않습니다." }
let v = weight / (m * m)
switch v {
case ..<18.5: return "저체중"
case 18.5..<23: return "정상(대한비만학회 기준)"
case 23..<25: return "과체중"
case 25..<30: return "비만(1단계)"
default: return "고도비만(2단계+)"
}
}
}
포인트: 아시아(대한비만학회) 기준은 서구 기준(정상 18.5~25)과 다릅니다. 사용 목적에 맞는 기준을 명시하세요.
6) UX 디테일(키보드, 포커스, 터치 처리)
- decimalPad로 잘못된 문자 입력을 사전 차단 → 에러 메시지 빈도 감소.
- textFieldShouldReturn으로 빠른 폼 이동/자동 계산 → 입력 흐름 매끄러움.
- touchesBegan으로 언제든 화면 탭 → 키보드 숨김 → 답답함 해소.
7) 확장: BMI 분류 버튼 액션 구현 예시
현재 classificationForBMI는 비어 있으므로 다음처럼 구현할 수 있습니다.
@IBAction func classificationForBMI(_ sender: UIButton) {
view.endEditing(true)
guard
let h = parseNumber(from: txtHeight.text),
let w = parseNumber(from: txtWeight.text),
h > 0, w > 0
else {
lblResult.text = "유효한 키(cm)와 몸무게(kg)를 입력하세요"
return
}
let bmi = BMI(weight: w, height: h)
let resultText = "\(bmi.calcBMI())\n분류: \(bmi.category())"
lblResult.text = resultText
}
적용 방식: “계산” 버튼은 숫자만 표시, “분류” 버튼은 숫자 + 카테고리 표시처럼 역할을 분리하면 사용자에게 더 명확합니다.
8) 실습 체크리스트 & 흔한 오류(KVC, 아울렛 연결)
✅ 체크리스트
- 스토리보드에서 ViewController의 Class가 실제 파일의 클래스명과 일치하는가?
- txtHeight, txtWeight, lblResult 아울렛 연결이 모두 되어 있는가?
- “계산”, “분류” 버튼의 IBAction 연결이 맞는가?
- 각 텍스트필드의 Keyboard Type은 .decimalPad인가?
- 탭/리턴키 동작이 의도대로 포커스를 이동/계산하는가?
- 비정상 입력(공백, 음수, 0, 문자) 시 에러 메시지가 잘 표시되는가?
❗흔한 오류: KVC(Key-Value Coding) 불일치
- 증상: 앱 실행 직후 크래시 +
this class is not key value coding-compliant for the key ... - 원인: 스토리보드에서 연결된 아울렛/액션의 이름 또는 타입이 변경/삭제되었는데, 연결을 갱신하지 않음.
- 해결:
- 스토리보드에서 해당 ViewController 선택
- Connections Inspector 탭에서 ⚠️표시된 연결 삭제
- 실제 코드의 @IBOutlet / @IBAction 이름과 타입을 확인하고 다시 연결
- 클래스명, 모듈 설정도 올바른지 확인
9) 마무리 팁
- 컨트롤러는 “흐름/검증”, 모델은 “계산/규칙”: 역할을 나누면 코드가 깨끗해집니다.
- 입력 전처리는 지금처럼 단순 문자열 치환으로 시작하고, 필요 시 NumberFormatter + 로케일 대응으로 확장하세요.
- BMI 기준은 사용자에게 공지(대한비만학회 vs WHO 등). 혼동을 줄이고 신뢰성을 높입니다.
- 테스트는 경계값(예: 18.5, 23, 25, 30)에 특히 집중하세요.
'iOS' 카테고리의 다른 글
| [iOS]BMI 앱 만들기 3주차 (0) | 2025.11.25 |
|---|---|
| [iOS]BMI 앱 만들기 2주 (0) | 2025.11.18 |
| [iOS]10주차 이후 — BMI 판정 / 동영상 재생 / 웹뷰 앱 만들기 (0) | 2025.11.04 |
| [iOS]9 주차 — Xcode로 간단한 앱 개발 정리 (0) | 2025.10.28 |
| [iOS] 7주차 Swift 문법 클래스(Class), Failable Initializer, 상속 (1) | 2025.10.14 |