iOS프로그래밍기초(Smile Han)/복습

[iOS] 복습 12 - BMI 앱 구현(2/4)

wse46 2024. 11. 20. 16:50

iOS에서 **오토 레이아웃(Auto Layout)**은 앱의 사용자 인터페이스(UI)를 화면 크기, 방향, 기기 종류에 따라 자동으로 적응하도록 설계하는 레이아웃 시스템이야.
 
오토 레이아웃의 주요 역할
 
1. 화면 크기와 비율에 따른 적응
• 아이폰 SE부터 아이패드까지 다양한 화면 크기를 가진 기기에서 UI가 깔끔하게 표시되도록 도와줘.
2. 가로/세로 방향 지원
• 디바이스를 회전해도 UI가 자연스럽게 재배치되도록 설정 가능.
3. 다국어 지원
• 언어에 따라 텍스트 길이가 달라져도 적절히 배치되도록 자동 조정.
 
오토 레이아웃의 핵심 요소
 
1. 제약 조건(Constraints)
• 뷰들의 위치와 크기를 정의하는 규칙이야. 예를 들어:
• “이 버튼은 화면의 가운데에 위치한다.”
• “이 텍스트 필드의 높이는 50으로 고정한다.”
• “이미지 뷰는 버튼 아래 10pt 간격으로 배치한다.”
2. 뷰 간의 관계
• 뷰들끼리 상호 의존 관계를 정의할 수도 있어.
예: “레이블의 오른쪽은 버튼의 왼쪽에 20pt 떨어져 있다.”
3. 우선순위(Priority)
• 제약 조건 중 충돌이 발생하면 우선순위가 높은 조건이 적용돼.
 
오토 레이아웃을 설정하는 방법
 
1. 스토리보드에서 시각적으로 설정하기
• Interface Builder에서 뷰를 배치한 뒤, ‘Add Constraints’ 버튼을 사용해 제약 조건을 추가할 수 있어.
2. 코드로 설정하기
• NSLayoutConstraint 클래스나 Anchors를 사용해서 프로그래밍으로 설정할 수 있어.
 
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
 
 
3. 스택 뷰 활용하기
• 스택 뷰는 뷰들을 자동으로 정렬해 주는 컨테이너로, 오토 레이아웃을 더 쉽게 관리할 수 있어.
 
장점
 
• 다양한 기기와 화면 크기에 대응 가능.
• UI 유지보수가 쉬워짐.
• 다국어 지원 및 텍스트 확장성 강화.
 
단점
 
• 처음 익힐 때 약간 복잡해 보일 수 있어.
• 제약 조건 간 충돌이 발생하면 디버깅이 필요함.
 
오토 레이아웃은 iOS 앱 개발의 필수 기술이니까, 처음엔 간단한 프로젝트에서 연습해 보고 점점 복잡한 UI로 확장하면 좋아! 😊
 

입력 아울렛 2개
출력 아울렛 1개
 

아울렛 3개, 액션 1개
 

BMI 계산 버튼을 누르면 콘솔창에 bmi와 판정 결과가 출력된다.
 

text는 optional String형

 
강제 언래핑하여 String형으로 변환 후 Double형으로 변환


 

# 위 코드에서 느낌표(!)가 두 번씩 사용된 이유 - 강제 언래핑(Force Unwrapping)을 위해

Swift에서 느낌표는 옵셔널 타입을 강제로 언래핑할 때 사용됩니다. 구체적으로 코드의 의미를 풀어보면 다음과 같습니다:
 
코드 설명
 
1. txtHeight.text:
• txtHeight는 UITextField 객체이고, text 속성은 **옵셔널(String?)**입니다. 이는 사용자가 텍스트 필드에 값을 입력하지 않았을 경우 nil이 될 수 있기 때문입니다.
2. Double(txtHeight.text!)!:
• txtHeight.text!: 강제 언래핑. text 속성에서 값을 꺼내옵니다. 만약 nil이면 프로그램이 크래시됩니다.
• Double(...): 문자열 값을 Double 타입으로 변환합니다. 변환이 실패할 경우 **nil**을 반환합니다.
• !: 변환된 결과를 다시 강제 언래핑합니다. 여기서도 nil이면 프로그램이 크래시됩니다.
 
1. 첫 번째 느낌표: txtHeight.text를 옵셔널(String?)에서 일반 문자열(String)로 변환.
2. 두 번째 느낌표: Double() 함수의 반환값이 옵셔널(Double?)이기 때문에 이를 일반 Double로 변환.
 
위험성
 
강제 언래핑(!)은 값이 nil일 가능성이 있을 때 크래시를 유발할 수 있습니다. 위 코드는 다음 두 가지 경우 크래시될 수 있습니다:
 
1. txtHeight.text가 nil일 경우.
2. txtHeight.text의 값이 Double로 변환할 수 없는 경우 (예: “abc”).
 
개선 방안
 
강제 언래핑을 피하고, 안전하게 처리하려면 옵셔널 바인딩(Optional Binding) 사용

if let heightText = txtHeight.text,
   let height = Double(heightText),
   let weightText = txtWeight.text,
   let weight = Double(weightText) {
    // 안전하게 height와 weight를 사용할 수 있습니다.
    print("Height: \(height), Weight: \(weight)")
} else {
    print("입력 값이 잘못되었습니다.")
}

 
이 방식은 크래시를 방지하고, 사용자가 잘못된 값을 입력했을 때 적절히 처리할 수 있는 코드를 작성할 수 있습니다.
 



# guard let

조건이 만족되지 않을 경우 바로 빠져나올 수 있어, 코드를 더 간결하게 정리할 수 있습니다. 


수정된 코드: guard let 사용

func processInput() {
    guard let heightText = txtHeight.text, 
          let weightText = txtWeight.text,
          let height = Double(heightText), 
          let weight = Double(weightText) else {
        print("입력 값이 잘못되었습니다.")
        return
    }
    
    print("Height: \(height), Weight: \(weight)")
    // 여기서 height와 weight를 안전하게 사용할 수 있습니다.
}

 
1. guard let은 조건이 충족되지 않을 경우 else 블록이 실행되고, 이후 코드를 실행하지 않고 함수나 블록에서 빠져나옵니다.
2. 입력 값이 올바른 경우에만 이후 코드를 실행합니다.
3. 입력 값이 잘못되었을 때, 사용자에게 오류를 알리거나 기본값을 설정하는 코드를 추가할 수 있습니다.
 
guard let의 장점
 
가독성 향상: 오류 처리와 정상 흐름을 명확히 구분.
안전성: 강제 언래핑 없이 옵셔널을 안전하게 처리.
 
이 코드는 특히 입력 값을 검증하는 초기 단계에서 많이 사용됩니다. guard let을 통해 앱의 안정성을 높이고 크래시를 방지할 수 있습니다.

ent


 

결과값을 콘솔창이 아닌 시뮬레이터 결과칸에 출력하기 위한 소스

lblResult.text = "BMI:\(shortenedBmi), 판정:\(body)"

 

 

더보기

전체 선택 후 코드 정렬(Re-Indent) : command + A, control + I

 

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var txtHeight: UITextField!
    @IBOutlet weak var txtWeight: UITextField!
    @IBOutlet weak var lblResult: UILabel!
    @IBAction func calcBmi(_ sender: UIButton) {
        if txtHeight.text == "" || txtWeight.text == "" {
            lblResult.textColor = .red
            lblResult.text = "키와 체중을 입력하세요!"
            return
        } else {
            let weight = Double(txtWeight.text!)!
            let height = Double(txtHeight.text!)!
            let bmi = weight / (height*height*0.0001) // kg/m*m
            let shortenedBmi = String(format: "%.1f", bmi)
            var body = ""
            if bmi >= 40 {
                body = "3단계 비만"
            } else if bmi >= 30 && bmi < 40 {
                body = "2단계 비만"
            } else if bmi >= 25 && bmi < 30 {
                body = "1단계 비만"
            } else if bmi >= 18.5 && bmi < 25 {
                body = "정상"
            } else {
                body = "저체중"
            }
            print("BMI:\(shortenedBmi), 판정:\(body)")
            lblResult.text = "BMI:\(shortenedBmi), 판정:\(body)"
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
        
}

 

 

입력값이 없는 상태로 계산을 시도하면 안내문이 출력되도록 설정

 

판정 결과에 따라 출력 레이블에 다른 배경색 지정

 

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var txtHeight: UITextField!
    @IBOutlet weak var txtWeight: UITextField!
    @IBOutlet weak var lblResult: UILabel!
    @IBAction func calcBmi(_ sender: UIButton) {
        if txtHeight.text == "" || txtWeight.text == "" {
            lblResult.textColor = .red
            lblResult.text = "키와 체중을 입력하세요!"
            return
        } else {
            let weight = Double(txtWeight.text!)!
            let height = Double(txtHeight.text!)!
            let bmi = weight / (height*height*0.0001) // kg/m*m
            let shortenedBmi = String(format: "%.1f", bmi)
            var body = ""
            var color = UIColor.white
            if bmi >= 40 {
                color = UIColor(displayP3Red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)
                body = "3단계 비만"
            } else if bmi >= 30 && bmi < 40 {
                color = UIColor(displayP3Red: 0.7, green: 0.0, blue: 0.0, alpha: 1.0)
                body = "2단계 비만"
            } else if bmi >= 25 && bmi < 30 {
                color = UIColor(displayP3Red: 0.4, green: 0.0, blue: 0.0, alpha: 1.0)
                body = "1단계 비만"
            } else if bmi >= 18.5 && bmi < 25 {
                color = UIColor(displayP3Red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
                body = "정상"
            } else {
                color = UIColor(displayP3Red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0)
                body = "저체중"
            }
            //print("BMI:\(shortenedBmi), 판정:\(body)")
            lblResult.backgroundColor = color
            lblResult.textColor = .white
            lblResult.text = "BMI:\(shortenedBmi), 판정:\(body)"
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
    
}
import UIKit
// UIKit 프레임워크를 임포트합니다. 이 프레임워크는 iOS 앱의 UI와 인터랙션을 관리합니다.

class ViewController: UIViewController {
    // ViewController 클래스는 UIViewController를 상속받습니다. 이 클래스는 화면에 UI 요소를 관리합니다.

    @IBOutlet weak var txtHeight: UITextField!
    // 키(TextField)를 입력받는 IBOutlet 변수입니다. 인터페이스 빌더에서 연결합니다.

    @IBOutlet weak var txtWeight: UITextField!
    // 체중(TextField)을 입력받는 IBOutlet 변수입니다. 인터페이스 빌더에서 연결합니다.

    @IBOutlet weak var lblResult: UILabel!
    // BMI 계산 결과를 표시하는 UILabel IBOutlet 변수입니다. 인터페이스 빌더에서 연결합니다.

    @IBAction func calcBmi(_ sender: UIButton) {
        // BMI 계산을 실행하는 함수입니다. UIButton과 연결된 IBAction으로 버튼을 클릭했을 때 호출됩니다.

        if txtHeight.text == "" || txtWeight.text == "" {
            // 키 또는 체중이 입력되지 않았는지 확인합니다.
            
            lblResult.textColor = .red
            // 입력이 없을 경우 결과 레이블의 텍스트 색상을 빨간색으로 설정합니다.
            
            lblResult.text = "키와 체중을 입력하세요!"
            // 경고 메시지를 결과 레이블에 표시합니다.
            
            return
            // 함수 실행을 종료합니다.
        } else {
            // 키와 체중이 모두 입력된 경우 BMI를 계산합니다.

            let weight = Double(txtWeight.text!)!
            // 텍스트 필드에서 체중 값을 가져와 Double로 변환합니다.
            
            let height = Double(txtHeight.text!)!
            // 텍스트 필드에서 키 값을 가져와 Double로 변환합니다.

            let bmi = weight / (height*height*0.0001)
            // BMI를 계산합니다. BMI = 체중(kg) / (키(m) * 키(m))이고, cm 단위를 m로 변환하기 위해 0.0001을 곱합니다.
            
            let shortenedBmi = String(format: "%.1f", bmi)
            // BMI 값을 소수점 첫째 자리까지 문자열로 변환합니다.

            var body = ""
            // BMI에 따른 판정을 저장할 변수입니다.

            var color = UIColor.white
            // BMI 판정에 따라 배경색을 설정할 변수로, 기본값은 흰색입니다.

            if bmi >= 40 {
                // BMI가 40 이상이면
                
                color = UIColor(displayP3Red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)
                // 배경색을 빨간색으로 설정합니다.
                
                body = "3단계 비만"
                // 판정을 "3단계 비만"으로 설정합니다.
            } else if bmi >= 30 && bmi < 40 {
                // BMI가 30 이상 40 미만이면
                
                color = UIColor(displayP3Red: 0.7, green: 0.0, blue: 0.0, alpha: 1.0)
                // 배경색을 짙은 빨간색으로 설정합니다.
                
                body = "2단계 비만"
                // 판정을 "2단계 비만"으로 설정합니다.
            } else if bmi >= 25 && bmi < 30 {
                // BMI가 25 이상 30 미만이면
                
                color = UIColor(displayP3Red: 0.4, green: 0.0, blue: 0.0, alpha: 1.0)
                // 배경색을 연한 빨간색으로 설정합니다.
                
                body = "1단계 비만"
                // 판정을 "1단계 비만"으로 설정합니다.
            } else if bmi >= 18.5 && bmi < 25 {
                // BMI가 18.5 이상 25 미만이면
                
                color = UIColor(displayP3Red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
                // 배경색을 검정색으로 설정합니다.
                
                body = "정상"
                // 판정을 "정상"으로 설정합니다.
            } else {
                // BMI가 18.5 미만이면
                
                color = UIColor(displayP3Red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0)
                // 배경색을 초록색으로 설정합니다.
                
                body = "저체중"
                // 판정을 "저체중"으로 설정합니다.
            }

            lblResult.backgroundColor = color
            // BMI 판정에 따라 설정된 배경색을 결과 레이블에 적용합니다.

            lblResult.clipsToBounds = true
            // 레이블의 경계가 잘리도록 설정합니다.
            
            lblResult.layer.cornerRadius = 10
            // 레이블의 모서리를 둥글게 설정합니다. 반지름은 10입니다.

            lblResult.text = "BMI:\(shortenedBmi), 판정:\(body)"
            // 결과 레이블에 BMI 값과 판정을 표시합니다.
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 화면이 로드된 후 실행되는 초기화 코드입니다.
    }
}

 

# Manual Segue와 Relationship Segue의 차이점

구분 Manual Segue Relationship Segue
용도 특정 동작(버튼 클릭, 코드 실행 등)에 의해 화면 전환을 수행할 때 사용 뷰 컨트롤러 간 부모-자식 관계를 정의할 때 사용
트리거 방식 - 버튼 클릭, 제스처
- 코드에서 performSegue(withIdentifier:) 호출
뷰 컨트롤러 계층 관계에 의해 자동으로 작동
설정 위치 - 스토리보드에서 컨트롤(버튼, 제스처 등)과 연결
- 코드에서 수동 호출
컨테이너 뷰(예: Navigation Controller, Tab Bar Controller)와 연결
식별자(identifier) - 수동 호출 시 식별자를 설정하여 식별 가능 일반적으로 식별자를 설정하지 않음
역할 뷰 컨트롤러 간 화면 전환(탐색 모달 등)을 처리 컨테이너 뷰 컨트롤러에서 자식 뷰 컨트롤러를 정의
예시 - 버튼을 클릭해 특정 화면으로 이동
- 코드에서 조건에 따라 화면 전환
- Navigation Controlle와 뷰 컨트롤러 연결
- Tab Bar Controller와 탭 화면 연결
코드 의존성 코드에서 명시적으로 호출 가능 일반적으로 코드 없이 스토리보드에서 설정