코딩공부

iOS 10주차

늠재 2025. 11. 3. 15:08

connections Inspecter

Connections Inspector : outlet과 action이 잘 연결되어있는지 확인

중복 연결이 없는지 제대로 확인하기

 

ViewController.swift의 소스 ai에게 정리시켜보기

 

//
//  ViewController.swift // 현재 파일의 이름이 ViewController.swift임을 나타냅니다.
//  ImageView            // 이 파일이 속한 프로젝트의 이름이 ImageView임을 나타냅니다.
//                       // (이는 Xcode 프로젝트 생성 시 기본으로 제공되는 주석 양식입니다.)
//  Created by Ho-Jeong Song on 2021/11/23. // 파일이 생성된 날짜와 생성자 정보를 나타냅니다.

import UIKit // UIKit 프레임워크를 현재 파일로 가져옵니다. iOS UI 개발에 필수적인 요소들을 포함합니다.

class ViewController: UIViewController { // ViewController라는 이름의 클래스를 정의하고, UIViewController를 상속받습니다.
                                        // UIViewController는 iOS 앱의 화면 하나를 관리하는 데 사용되는 기본 클래스입니다.

    var isZoom = false // 이미지 확대/축소 상태를 나타내는 불리언(Bool) 변수입니다. 초기값은 false (축소 상태)입니다.
    var imgOn: UIImage? // "램프 켜짐" 이미지를 저장할 UIImage 옵셔널 변수입니다. 초기값은 nil입니다.
    var imgOff: UIImage? // "램프 꺼짐" 이미지를 저장할 UIImage 옵셔널 변수입니다. 초기값은 nil입니다.

    @IBOutlet var imgView: UIImageView! // UIImageView 타입의 변수 imgView를 선언하고, 이 변수를 Interface Builder (스토리보드)의 UIImageView 객체와 연결할 것임을 나타냅니다.
                                      // '!'는 암시적 언래핑 옵셔널로, imgView가 항상 유효한 값을 가질 것이라고 가정합니다.
    @IBOutlet var btnResize: UIButton! // UIButton 타입의 변수 btnResize를 선언하고, 이 변수를 Interface Builder의 UIButton 객체와 연결할 것임을 나타냅니다.
    
    override func viewDidLoad() { // 부모 클래스인 UIViewController의 viewDidLoad() 메서드를 재정의(override)합니다.
                                  // 뷰 컨트롤러의 뷰 계층이 메모리에 로드된 후 한 번 호출되는 생명 주기 메서드입니다.
        super.viewDidLoad() // 부모 클래스인 UIViewController의 viewDidLoad() 메서드를 호출하여 기본 초기화 작업을 수행합니다.
        // Do any additional setup after loading the view. // 추가적인 설정 작업을 이곳에 작성하라는 Xcode 기본 주석입니다.

        imgOn = UIImage(named: "lamp_on.png") // Assets.xcassets에 있는 "lamp_on.png" 이미지를 찾아 imgOn 변수에 할당합니다.
        imgOff = UIImage(named: "lamp_off.png") // Assets.xcassets에 있는 "lamp_off.png" 이미지를 찾아 imgOff 변수에 할당합니다.
        
        imgView.image = imgOn // imgView (UIImageView)에 imgOn (램프 켜짐 이미지)을 설정하여 초기 화면에 표시합니다.
    } // viewDidLoad() 메서드 끝

    @IBAction func btnResizeImage(_ sender: UIButton) { // Interface Builder의 버튼(Sender)과 연결될 액션(Action) 메서드를 정의합니다.
                                                      // 사용자가 "확대/축소" 버튼을 눌렀을 때 호출됩니다.
        let scale: CGFloat = 2.0 // 이미지 확대/축소 배율을 저장하는 상수입니다. 여기서는 2배로 설정되었습니다.
        var newWidth: CGFloat, newHeight: CGFloat // 새로 계산될 이미지의 너비와 높이를 저장할 변수입니다. (CGFloat는 실수형 타입)

        if (isZoom) {   // isZoom이 true인 경우 (현재 확대 상태인 경우)
            newWidth = imgView.frame.width / scale // 현재 이미지 뷰의 너비를 scale (2.0)로 나누어 새로운 너비를 계산합니다 (축소).
            newHeight = imgView.frame.height / scale // 현재 이미지 뷰의 높이를 scale (2.0)로 나누어 새로운 높이를 계산합니다 (축소).
            
            btnResize.setTitle("확대", for: .normal) // 버튼의 텍스트를 "확대"로 변경하여 다음 동작이 확대임을 사용자에게 알립니다.
        }
        else {  // isZoom이 false인 경우 (현재 축소 상태인 경우)
            newWidth = imgView.frame.width * scale // 현재 이미지 뷰의 너비를 scale (2.0)로 곱하여 새로운 너비를 계산합니다 (확대).
            newHeight = imgView.frame.height * scale // 현재 이미지 뷰의 높이를 scale (2.0)로 곱하여 새로운 높이를 계산합니다 (확대).
            
            btnResize.setTitle("축소", for: .normal) // 버튼의 텍스트를 "축소"로 변경하여 다음 동작이 축소임을 사용자에게 알립니다.
        }
        
        imgView.frame.size = CGSize(width: newWidth, height: newHeight) // 계산된 newWidth와 newHeight로 imgView의 크기를 업데이트합니다.
        
        isZoom = !isZoom // isZoom 변수의 값을 반전시켜 (true이면 false로, false이면 true로) 다음 클릭 시 동작을 변경합니다.
    } // btnResizeImage() 메서드 끝
    
    @IBAction func switchImageOnOff(_ sender: UISwitch) { // Interface Builder의 스위치(Sender)와 연결될 액션(Action) 메서드를 정의합니다.
                                                      // 사용자가 UISwitch의 상태를 변경할 때 호출됩니다.
        if sender.isOn { // sender (UISwitch)의 현재 상태가 켜져(on) 있으면
            imgView.image = imgOn // imgView에 램프 켜짐 이미지(imgOn)를 설정합니다.
        } else { // sender (UISwitch)의 현재 상태가 꺼져(off) 있으면
            imgView.image = imgOff // imgView에 램프 꺼짐 이미지(imgOff)를 설정합니다.
        }
    } // switchImageOnOff() 메서드 끝
} // ViewController 클래스 끝

 

리팩토링 <<

 

import UIKit

// MARK: - Enums (상태를 명확하게 표현하기 위한 열거형)

// 이미지의 확대/축소 상태를 정의하는 열거형입니다.
// 각 상태는 다음 클릭 시 버튼에 표시될 텍스트를 함께 제공합니다.
enum ResizeState {
    case zoomedOut // 현재 이미지가 축소되어 있는 상태 (다음 동작: 확대)
    case zoomedIn  // 현재 이미지가 확대되어 있는 상태 (다음 동작: 축소)

    // 현재 상태에서 다음 동작 버튼에 표시될 텍스트를 반환합니다.
    var buttonTitle: String {
        switch self {
        case .zoomedOut: return "확대" // 현재 축소 상태이므로, 다음 동작은 "확대"
        case .zoomedIn: return "축소"   // 현재 확대 상태이므로, 다음 동작은 "축소"
        }
    }
    
    // 다음 상태로 전환합니다. (zoomedOut <-> zoomedIn)
    var toggle: ResizeState {
        switch self {
        case .zoomedOut: return .zoomedIn
        case .zoomedIn: return .zoomedOut
        }
    }
}

class ViewController: UIViewController {

    // 램프 켜짐/꺼짐 이미지를 저장하는 프로퍼티입니다.
    // viewDidLoad()에서 안전하게 로드한 후에는 nil이 아니라고 가정합니다.
    private var lampOnImage: UIImage!
    private var lampOffImage: UIImage!

    // 이미지의 현재 확대/축소 상태를 저장하는 프로퍼티입니다.
    // 이 값이 변경될 때마다 didSet 블록이 자동으로 실행되어 UI를 업데이트합니다.
    private var currentResizeState: ResizeState = .zoomedOut { // 초기 상태는 축소
        didSet {
            // 상태가 변경되면 버튼의 타이틀을 업데이트합니다.
            btnResize.setTitle(currentResizeState.buttonTitle, for: .normal)
            // 상태에 따라 이미지 뷰의 크기를 조정하는 메서드를 호출합니다.
            resizeImageView(to: currentResizeState)
        }
    }

    // 이미지 확대/축소에 사용될 배율 상수입니다. 매직넘버 제거.
    private let scaleFactor: CGFloat = 2.0

    // MARK: - IBOutlets (Interface Builder에서 연결된 UI 요소들)

    @IBOutlet var imgView: UIImageView!     // 이미지를 표시할 UIImageView
    @IBOutlet var btnResize: UIButton!      // 이미지 크기 조절 버튼

    // MARK: - View Lifecycle (뷰 컨트롤러 생명 주기 메서드)

    override func viewDidLoad() {
        super.viewDidLoad()

        // 앱 시작 시 필요한 이미지들을 안전하게 로드합니다.
        // guard let을 사용하여 이미지 로드 실패 시 에러를 처리합니다.
        guard let onImage = UIImage(named: "lamp_on.png"),
              let offImage = UIImage(named: "lamp_off.png") else {
            // 이미지 로드 실패 시 콘솔에 에러 메시지를 출력합니다.
            print("🚨 에러: 'lamp_on.png' 또는 'lamp_off.png' 이미지를 찾을 수 없습니다.")
            // 필수 이미지가 없으므로, 앱의 나머지 부분이 올바르게 작동하지 않을 수 있습니다.
            // 필요하다면 이곳에서 사용자에게 알림을 표시하거나 기본 이미지를 설정할 수 있습니다.
            lampOnImage = UIImage() // nil 방지를 위한 빈 이미지 할당 (실제 앱에서는 적절히 처리)
            lampOffImage = UIImage()
            return
        }
        
        // 성공적으로 로드된 이미지를 프로퍼티에 할당합니다.
        self.lampOnImage = onImage
        self.lampOffImage = offImage
        
        // 초기 뷰 설정: 램프 켜짐 이미지를 표시합니다.
        imgView.image = lampOnImage
        
        // currentResizeState의 초기값 (.zoomedOut) 설정 시 didSet이 호출되어
        // btnResize의 타이틀이 "확대"로 설정되고, 초기 이미지 크기가 적용됩니다.
        currentResizeState = .zoomedOut
    }

    // MARK: - Private Methods (내부적으로 사용되는 도우미 메서드)

    /// 주어진 상태에 따라 `imgView`의 크기를 조정합니다.
    /// - Parameter state: 이미지 뷰가 변경될 `ResizeState`.
    private func resizeImageView(to state: ResizeState) {
        var newWidth: CGFloat, newHeight: CGFloat

        switch state {
        case .zoomedOut: // 축소 상태 (다음 동작이 확대) -> 원본 크기에서 축소된 크기로 설정
            // 현재 너비/높이를 배율로 나누면 이전 확대 상태에서 원래대로 돌아가는 효과
            // 또는 초기 크기를 기준으로 계산하는 것도 가능
            // 여기서는 현재 imgView 크기를 기준으로 계산하도록 유지
            newWidth = imgView.frame.width / scaleFactor
            newHeight = imgView.frame.height / scaleFactor
        case .zoomedIn: // 확대 상태 (다음 동작이 축소) -> 원본 크기에서 확대된 크기로 설정
            newWidth = imgView.frame.width * scaleFactor
            newHeight = imgView.frame.height * scaleFactor
        }
        
        // 계산된 새 크기로 이미지 뷰의 프레임 크기를 업데이트합니다.
        imgView.frame.size = CGSize(width: newWidth, height: newHeight)
    }

    // MARK: - IBActions (Interface Builder와 연결된 사용자 액션)

    /// "확대/축소" 버튼을 눌렀을 때 호출되는 액션 메서드입니다.
    @IBAction func btnResizeImage(_ sender: UIButton) {
        // currentResizeState를 토글하여 상태를 변경합니다.
        // currentResizeState의 didSet이 자동으로 호출되어 UI가 업데이트됩니다.
        currentResizeState = currentResizeState.toggle
    }
    
    /// 램프 ON/OFF 스위치를 조작했을 때 호출되는 액션 메서드입니다.
    /// - Parameter sender: 이 액션을 발생시킨 UISwitch 객체입니다.
    @IBAction func switchImageOnOff(_ sender: UISwitch) {
        // 스위치의 `isOn` 프로퍼티 값에 따라 `imgView`에 표시될 이미지를 변경합니다.
        imgView.image = sender.isOn ? lampOnImage : lampOffImage
    }
}

 

Datepicker의 소스 주석 형식으로 질문

//
//  ViewController.swift // 현재 파일의 이름이 ViewController.swift임을 나타냅니다.
//  DatePicker             // 이 파일이 속한 프로젝트의 이름이 DatePicker임을 나타냅니다.
//                       // (이는 Xcode 프로젝트 생성 시 기본으로 제공되는 주석 양식입니다.)
//  Created by Ho-Jeong Song on 2021/11/24. // 파일이 생성된 날짜와 생성자 정보를 나타냅니다.

import UIKit // UIKit 프레임워크를 현재 파일로 가져옵니다. iOS UI 개발에 필수적인 요소들을 포함합니다.

class ViewController: UIViewController { // ViewController라는 이름의 클래스를 정의하고, UIViewController를 상속받습니다.
                                        // UIViewController는 iOS 앱의 화면 하나를 관리하는 데 사용되는 기본 클래스입니다.

    let timeSelector: Selector = #selector(ViewController.updateTime) // `updateTime` 메서드를 가리키는 `Selector` 상수를 선언합니다.
                                                                     // `Selector`는 Objective-C 기반의 코드를 Swift에서 사용할 때 메서드를 참조하기 위한 식별자입니다.
    let interval = 1.0 // 타이머가 반복될 시간 간격(초 단위)을 나타내는 상수입니다. 여기서는 1초로 설정되었습니다.
    var count = 0 // `updateTime` 메서드 내부에 주석 처리된 카운트업 기능에 사용될 변수입니다. 현재 코드에서는 사용되지 않습니다.

    @IBOutlet var lblCurrentTime: UILabel! // 현재 시간을 표시할 UILabel 타입의 변수 lblCurrentTime을 선언하고, Interface Builder (스토리보드)의 UILabel 객체와 연결할 것임을 나타냅니다.
    @IBOutlet var lblPickerTime: UILabel! // UIDatePicker에서 선택된 시간을 표시할 UILabel 타입의 변수 lblPickerTime을 선언하고, Interface Builder의 UILabel 객체와 연결할 것임을 나타냅니다.
    
    override func viewDidLoad() { // 부모 클래스인 UIViewController의 viewDidLoad() 메서드를 재정의(override)합니다.
                                  // 뷰 컨트롤러의 뷰 계층이 메모리에 로드된 후 한 번 호출되는 생명 주기 메서드입니다.
        super.viewDidLoad() // 부모 클래스인 UIViewController의 viewDidLoad() 메서드를 호출하여 기본 초기화 작업을 수행합니다.
        // Do any additional setup after loading the view. // 추가적인 설정 작업을 이곳에 작성하라는 Xcode 기본 주석입니다.

        // `Timer` 클래스를 사용하여 지정된 간격(`interval`)마다 `timeSelector`에 지정된 메서드(`updateTime`)를 반복적으로 호출하도록 스케줄링합니다.
        Timer.scheduledTimer(timeInterval: interval, target: self, selector: timeSelector, userInfo: nil, repeats: true)
    } // viewDidLoad() 메서드 끝

    @IBAction func changeDatePicker(_ sender: UIDatePicker) { // Interface Builder의 UIDatePicker (Sender)와 연결될 액션(Action) 메서드를 정의합니다.
                                                              // 사용자가 UIDatePicker의 값을 변경할 때 호출됩니다.
        let datePickerView = sender // 액션을 발생시킨 UIDatePicker 객체를 datePickerView 상수에 할당합니다.
        
        let formatter = DateFormatter() // 날짜와 시간을 특정 문자열 형식으로 변환하거나 파싱하는 데 사용되는 DateFormatter 인스턴스를 생성합니다.
        formatter.dateFormat = "yyyy-MM-dd HH:mm EEE" // DateFormatter의 출력 형식을 지정합니다 (예: "2023-10-26 14:30 목").
        lblPickerTime.text = "선택시간: " + formatter.string(from: datePickerView.date) // datePickerView에서 선택된 날짜와 시간을 지정된 형식으로 변환하여 lblPickerTime 레이블에 표시합니다.
    } // changeDatePicker() 메서드 끝

    @objc func updateTime() { // Timer에 의해 호출될 메서드를 정의합니다. `@objc` 키워드는 이 Swift 메서드를 Objective-C 런타임이 호출할 수 있도록 노출합니다.
        //        lblCurrentTime.text = String(count) // 이 두 줄은 주석 처리되어 현재 코드에서는 동작하지 않습니다.
        //        count = count + 1                   // 이전에 타이머가 호출될 때마다 `count` 변수를 증가시켜 레이블에 표시하는 용도로 사용되었던 코드입니다.

        let date = NSDate() // 현재 시각을 나타내는 NSDate 객체를 생성합니다. (Swift에서는 주로 Date를 사용하지만, NSDate도 사용 가능합니다.)

        let formatter = DateFormatter() // 날짜와 시간을 특정 문자열 형식으로 변환하는 DateFormatter 인스턴스를 생성합니다.
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss EEE" // DateFormatter의 출력 형식을 지정합니다 (예: "2023-10-26 14:30:00 목").
        lblCurrentTime.text = "현재시간: " + formatter.string(from: date as Date) // 현재 날짜와 시간을 지정된 형식으로 변환하여 lblCurrentTime 레이블에 표시합니다. `NSDate`를 `Date`로 캐스팅합니다.
    } // updateTime() 메서드 끝
} // ViewController 클래스 끝

 

 

PickerView의 ViewController 소스 주석 질문

//
//  ViewController.swift // 현재 파일의 이름이 ViewController.swift임을 나타냅니다.
//  PickerView           // 이 파일이 속한 프로젝트의 이름이 PickerView임을 나타냅니다.
//                       // (이는 Xcode 프로젝트 생성 시 기본으로 제공되는 주석 양식입니다.)
//  Created by Ho-Jeong Song on 2021/11/25. // 파일이 생성된 날짜와 생성자 정보를 나타냅니다.

import UIKit // UIKit 프레임워크를 현재 파일로 가져옵니다. iOS UI 개발에 필수적인 요소들을 포함합니다.

class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource { // ViewController라는 이름의 클래스를 정의하고, UIViewController를 상속받습니다.
                                                                                     // 또한, UIPickerViewDelegate와 UIPickerViewDataSource 프로토콜을 채택하여 Picker View의 동작 및 데이터 제공 역할을 수행할 것임을 선언합니다.

    let MAX_ARRAY_NUM = 10 // 이미지 배열의 최대 크기를 나타내는 상수입니다. (이미지 파일 개수와 동일)
    let PICKER_VIEW_COLUMN = 1 // Picker View의 "열" (컴포넌트) 개수를 나타내는 상수입니다. 여기서는 이미지를 한 줄로 보여주므로 1개입니다.
    let PICKER_VIEW_HEIGHT:CGFloat = 80 // Picker View의 각 행(row)의 높이를 나타내는 상수입니다.
    var imageArray = [UIImage?]() // 로드된 UIImage 객체들을 저장할 배열입니다. 'UIImage?'는 이미지를 로드하지 못할 경우 nil이 될 수 있음을 나타냅니다.
    var imageFileName = [ "1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg", // 이미지 파일 이름 문자열을 저장하는 배열입니다.
                          "6.jpg", "7.jpg", "8.jpg", "9.jpg", "10.jpg" ]

    @IBOutlet var pickerImage: UIPickerView! // 이미지 선택에 사용될 UIPickerView 객체를 Interface Builder (스토리보드)와 연결합니다.
    @IBOutlet var lblImageFileName: UILabel! // 선택된 이미지의 파일 이름을 표시할 UILabel 객체를 Interface Builder와 연결합니다.
    @IBOutlet var imageView: UIImageView! // 선택된 이미지를 크게 보여줄 UIImageView 객체를 Interface Builder와 연결합니다.
    
    override func viewDidLoad() { // 부모 클래스인 UIViewController의 viewDidLoad() 메서드를 재정의합니다.
                                  // 뷰 컨트롤러의 뷰 계층이 메모리에 로드된 후 한 번 호출되는 생명 주기 메서드입니다.
        super.viewDidLoad() // 부모 클래스인 UIViewController의 viewDidLoad() 메서드를 호출하여 기본 초기화 작업을 수행합니다.
        // Do any additional setup after loading the view. // 추가적인 설정 작업을 이곳에 작성하라는 Xcode 기본 주석입니다.

        for i in 0 ..< MAX_ARRAY_NUM { // 0부터 MAX_ARRAY_NUM-1까지 반복하는 반복문입니다.
            let image = UIImage(named: imageFileName[i]) // imageFileName 배열의 이름에 해당하는 이미지를 Assets.xcassets에서 찾아 UIImage 객체를 생성합니다.
            imageArray.append(image) // 생성된 UIImage 객체를 imageArray에 추가합니다. (이미지를 찾지 못하면 nil이 추가됩니다)
        }
        
        lblImageFileName.text = imageFileName[0] // 초기 화면에 첫 번째 이미지 파일 이름을 lblImageFileName에 표시합니다.
        imageView.image = imageArray[0] // 초기 화면에 첫 번째 이미지(imageArray의 0번 인덱스)를 imageView에 표시합니다.
        
        // UIPickerView의 데이터 소스(dataSource)와 델리게이트(delegate)를 현재 ViewController로 설정합니다.
        // 이 두 프로퍼티를 설정해야 UIPickerView가 ViewController에 정의된 프로토콜 메서드들을 호출하게 됩니다.
        pickerImage.delegate = self
        pickerImage.dataSource = self
    } // viewDidLoad() 메서드 끝

    // MARK: - UIPickerViewDataSource 프로토콜 메서드 구현

    // pickerView의 "열" (컴포넌트) 개수를 반환합니다.
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return PICKER_VIEW_COLUMN // 위에서 정의한 PICKER_VIEW_COLUMN 상수(1)를 반환합니다.
    }
    
    // pickerView의 각 "열" (컴포넌트)에 대한 행(row)의 높이를 반환합니다.
    func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
        return PICKER_VIEW_HEIGHT // 위에서 정의한 PICKER_VIEW_HEIGHT 상수(80)를 반환합니다.
    }

    // pickerView의 각 "열" (컴포넌트)에 표시될 행(row)의 개수를 반환합니다.
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return imageFileName.count // imageFileName 배열의 총 개수를 반환합니다. (이는 이미지의 총 개수와 동일)
    }
    
//    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
//        return imageFileName[row] // 이 메서드는 주석 처리되어 사용되지 않습니다.
//                                  // 만약 주석을 풀고 사용하면, Picker View의 각 행에 imageFileName 배열의 문자열이 표시됩니다.
//                                  // 현재 코드는 아래 'viewForRow' 메서드를 사용하여 이미지 자체를 표시하므로 이 메서드는 필요 없습니다.
//    }

    // MARK: - UIPickerViewDelegate 프로토콜 메서드 구현 (커스텀 뷰 제공)

    // pickerView의 각 행(row)에 표시될 뷰를 사용자 정의하여 반환합니다.
    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
        let imageView = UIImageView(image:imageArray[row]) // imageArray에서 해당 row 인덱스의 이미지를 사용하여 UIImageView를 생성합니다.
        imageView.frame = CGRect(x: 0, y: 0, width: 100, height: 150) // 생성된 UIImageView의 프레임(위치와 크기)을 설정합니다.
                                                                    // 이 크기는 실제 Picker View 행의 높이와 다를 수 있으며, Picker View 내부에서 적절히 조정됩니다.
        
        return imageView // 사용자 정의된 UIImageView를 반환하여 Picker View 행에 표시합니다.
    }

    // MARK: - UIPickerViewDelegate 프로토콜 메서드 구현 (선택 이벤트 처리)

    // 사용자가 Picker View에서 특정 행(row)을 선택했을 때 호출됩니다.
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        lblImageFileName.text = imageFileName[row] // 선택된 행의 이미지 파일 이름을 lblImageFileName에 표시합니다.
        imageView.image = imageArray[row] // 선택된 행의 이미지(imageArray[row])를 imageView에 표시합니다.
    } // pickerView(_:didSelectRow:inComponent:) 메서드 끝
} // ViewController 클래스 끝

 

 

 

Alert 프로젝트의 ViewController 소스 주석 질문

//
//  ViewController.swift // 현재 파일의 이름이 ViewController.swift임을 나타냅니다.
//  Alert                // 이 파일이 속한 프로젝트의 이름이 Alert임을 나타냅니다.
//                       // (이는 Xcode 프로젝트 생성 시 기본으로 제공되는 주석 양식입니다.)
//  Created by BeomGeun Lee on // 파일 생성자 정보를 나타냅니다.

import UIKit // UIKit 프레임워크를 현재 파일로 가져옵니다. iOS UI 개발에 필수적인 요소들을 포함합니다.

class ViewController: UIViewController { // ViewController라는 이름의 클래스를 정의하고, UIViewController를 상속받습니다.
                                        // UIViewController는 iOS 앱의 화면 하나를 관리하는 데 사용되는 기본 클래스입니다.
    
    // 다음은 램프 이미지들을 저장하는 상수(let) 프로퍼티들입니다.
    let imgOn = UIImage(named: "lamp-on.png") // Assets.xcassets에 있는 "lamp-on.png" 이미지를 찾아 imgOn에 할당합니다.
    let imgOff = UIImage(named: "lamp-off.png") // Assets.xcassets에 있는 "lamp-off.png" 이미지를 찾아 imgOff에 할당합니다.
    let imgRemove = UIImage(named: "lamp-remove.png") // Assets.xcassets에 있는 "lamp-remove.png" 이미지를 찾아 imgRemove에 할당합니다.
    
    var isLampOn = true // 램프의 현재 ON/OFF 상태를 저장하는 불리언(Bool) 변수입니다. 초기값은 true (켜짐)입니다.
    
    @IBOutlet var lampImg: UIImageView! // 램프 이미지를 표시할 UIImageView 타입의 변수 lampImg를 선언하고, Interface Builder의 UIImageView 객체와 연결합니다.
    
    override func viewDidLoad() { // 부모 클래스인 UIViewController의 viewDidLoad() 메서드를 재정의(override)합니다.
                                  // 뷰 컨트롤러의 뷰 계층이 메모리에 로드된 후 한 번 호출되는 생명 주기 메서드입니다.
        super.viewDidLoad() // 부모 클래스인 UIViewController의 viewDidLoad() 메서드를 호출하여 기본 초기화 작업을 수행합니다.
        // Do any additional setup after loading the view. // 추가적인 설정 작업을 이곳에 작성하라는 Xcode 기본 주석입니다.
        lampImg.image = imgOn // 초기 화면에 lampImg (UIImageView)에 imgOn (램프 켜짐 이미지)을 설정합니다.
    } // viewDidLoad() 메서드 끝

    @IBAction func btnLampOn(_ sender: UIButton) { // Interface Builder의 버튼(Sender)과 연결될 액션(Action) 메서드를 정의합니다.
                                                  // 사용자가 "램프 켜기" 버튼을 눌렀을 때 호출됩니다.
        if(isLampOn==true) { // 만약 램프가 현재 켜져 있는 상태라면
            // UIAlertController를 생성하여 사용자에게 경고 메시지를 표시합니다.
            let lampOnAlert = UIAlertController(title: "경고", message: "현재 On 상태입니다", preferredStyle: UIAlertController.Style.alert)
            // 경고창에 "네, 알겠습니다."라는 제목의 액션 버튼을 추가합니다.
            let onAction = UIAlertAction(title: "네, 알겠습니다.", style: UIAlertAction.Style.default, handler: nil) // handler: nil은 버튼 클릭 시 별다른 동작 없이 경고창만 닫힙니다.
            lampOnAlert.addAction(onAction) // 생성된 액션을 경고창에 추가합니다.
            present(lampOnAlert, animated: true, completion: nil) // 경고창(lampOnAlert)을 화면에 표시합니다.
        } else { // 램프가 꺼져 있는 상태라면
            lampImg.image = imgOn // lampImg에 램프 켜짐 이미지(imgOn)를 설정하여 램프를 켭니다.
            isLampOn=true // 램프 상태를 true (켜짐)으로 변경합니다.
        }
    } // btnLampOn() 메서드 끝
    
    @IBAction func btnLanpOff(_ sender: UIButton) { // Interface Builder의 버튼(Sender)과 연결될 액션(Action) 메서드를 정의합니다.
                                                  // 사용자가 "램프 끄기" 버튼을 눌렀을 때 호출됩니다.
        if isLampOn { // 만약 램프가 현재 켜져 있는 상태라면
            // UIAlertController를 생성하여 사용자에게 램프를 끌 것인지 확인하는 메시지를 표시합니다.
            let lampOffAlert = UIAlertController(title: "램프 끄기", message: "램프를 끄시겠습니까?", preferredStyle: UIAlertController.Style.alert)

            // "네"라는 제목의 액션 버튼을 추가합니다. 클릭 시 클로저(handler) 내부 코드가 실행됩니다.
            let offAction = UIAlertAction(title: "네", style: UIAlertAction.Style.default, handler: { // 클로저가 시작됩니다.
                ACTION in self.lampImg.image = self.imgOff // 램프 이미지를 꺼짐 이미지(imgOff)로 변경합니다. (클로저 내부에서는 self를 명시해야 합니다.)
                self.isLampOn=false // 램프 상태를 false (꺼짐)로 변경합니다.
            }) // 클로저가 끝납니다.
            // "아니오"라는 제목의 액션 버튼을 추가합니다. 클릭 시 별다른 동작 없이 경고창만 닫힙니다.
            let cancelAction = UIAlertAction(title: "아니오", style: UIAlertAction.Style.default, handler: nil)

            lampOffAlert.addAction(offAction) // "네" 액션을 경고창에 추가합니다.
            lampOffAlert.addAction(cancelAction) // "아니오" 액션을 경고창에 추가합니다.

            present(lampOffAlert, animated: true, completion: nil) // 경고창(lampOffAlert)을 화면에 표시합니다.
        }
    } // btnLanpOff() 메서드 끝
    
    @IBAction func btnLampRemove(_ sender: UIButton) { // Interface Builder의 버튼(Sender)과 연결될 액션(Action) 메서드를 정의합니다.
                                                     // 사용자가 "램프 제거" 버튼을 눌렀을 때 호출됩니다.
        // UIAlertController를 생성하여 사용자에게 램프를 제거할 것인지 확인하는 메시지를 표시합니다.
        let lampRemoveAlert = UIAlertController(title: "램프 제거", message: "램프를 제거하시겠습니까?", preferredStyle: UIAlertController.Style.alert)

        // "아니오, 끕니다(off)."라는 제목의 액션 버튼을 추가합니다.
        let offAction = UIAlertAction(title: "아니오, 끕니다(off).", style: UIAlertAction.Style.default, handler: { // 클로저가 시작됩니다.
            ACTION in self.lampImg.image = self.imgOff // 램프 이미지를 꺼짐 이미지(imgOff)로 변경합니다.
            self.isLampOn=false // 램프 상태를 false (꺼짐)로 변경합니다.
        }) // 클로저가 끝납니다.
        // "아니오, 웁니다(on)."라는 제목의 액션 버튼을 추가합니다.
        let onAction = UIAlertAction(title: "아니오, 웁니다(on).", style: UIAlertAction.Style.default) { // 후행 클로저 문법을 사용하여 핸들러를 정의합니다.
            ACTION in self.lampImg.image = self.imgOn // 램프 이미지를 켜짐 이미지(imgOn)로 변경합니다.
            self.isLampOn=true // 램프 상태를 true (켜짐)으로 변경합니다.
        } // 클로저가 끝납니다.
        // "네, 제거합니다."라는 제목의 액션 버튼을 추가합니다. style: .destructive는 버튼 텍스트를 빨간색으로 표시하여 파괴적인 동작임을 강조합니다.
        let removeAction = UIAlertAction(title: "네, 제거합니다.", style: UIAlertAction.Style.destructive, handler: { // 클로저가 시작됩니다.
            ACTION in self.lampImg.image = self.imgRemove // 램프 이미지를 제거 이미지(imgRemove)로 변경합니다.
            self.isLampOn=false // 램프 상태를 false (꺼짐)로 변경합니다. (제거 후에는 켜져 있다고 보기 어려우므로)
        }) // 클로저

 

 

** 소스 출처 **

송호정이범근 저 이지스퍼블리싱 2025년 03월.  아래 사이트의 소스는 Xcode 버전 문제가 있어서 첨부 파일로 실습
----------------------------------------
02 Hello World 앱 만들며 Xcode에 완벽 적응하기
03 원하는 이미지 화면에 출력하기 - 이미지 뷰
04 데이트 피커 사용해 날짜 선택하기
05 피커 뷰 사용해 원하는 항목 선택하기
06 얼럿 사용해 경고 표시하기
07 웹 뷰로 간단한 웹 브라우저 만들기
08 맵 뷰로 지도 나타내기
09 페이지 이동하기 - 페이지 컨트롤
10 탭 바 컨트롤러 이용해 여러 개의 뷰 넣기
11 내비게이션 컨트롤러 이용해 화면 전환하기
12 테이블 뷰 컨트롤러 이용해 할 일 목록 만들기
13 음악 재생하고 녹음하기
14 비디오 재생 앱 만들기
15 카메라와 포토 라이브러리에서 미디어 가져오기
16 코어 그래픽스로 화면에 그림 그리기
17 탭과 터치 사용해 스케치 앱 만들기
18 스와이프 제스처 사용하기
19 핀치 제스처 사용해 사진을 확대/축소하기

bmi 앱 만들기 전 ai에게 질문하기

 

 

 

🛠️ 기술 스택:

  • 언어: Swift
  • UI: UIKit (스토리보드 사용!)
  • 개발 환경: Xcode

🧑‍💻 초보자를 위한 구현 팁 (⭐핵심⭐):

  1. 스토리보드로 UI 먼저 배치:
    • UILabel, UITextField, UIButton 드래그해서 적당히 배치해봐.
    • Auto Layout으로 화면 크기 달라도 잘 보이게 제약 조건(Constraints) 거는 연습도 해보면 굿! (처음엔 좀 어려울 수 있는데, 안 하면 아이폰 종류 바뀔 때마다 UI 박살남 ㅠㅠ)
  2. @IBOutlet 연결:
    • UITextField 2개랑 UILabel 2개를 코드(ViewController.swift)랑 연결해서 코드에서 제어할 수 있게 만들어야 함. (ctrl 누르고 드래그!)
  3. @IBAction 연결:
    • UIButton을 코드랑 연결해서 버튼이 눌리면 특정 함수가 실행되게 만들어야 함.
  4. 데이터 가져오기 및 형 변환:
    • 텍스트 필드의 입력 값은 기본적으로 String? 타입이야.
    • guard let heightText = heightTextField.text, let weightText = weightTextField.text else { return }
    • let height = Double(heightText)
    • let weight = Double(weightText)
    • 이때, Double(heightText)처럼 String을 숫자로 바꿀 땐 nil이 될 수 있으니 꼭 if let이나 guard let으로 옵셔널 바인딩 해야 해! (사용자가 숫자 아닌 걸 입력할 수도 있으니!)
    • 아니면, heightTextField.keyboardType = .numberPad 이걸로 숫자만 입력받게 하면 좀 편하겠지?
  5. BMI 공식:
    • BMI = 몸무게(kg) / (키(m) * 키(m))
    • ⚠️ 주의: 키를 cm로 입력받았다면, (cm / 100)으로 미터(m)로 변환해줘야 해!
    • 예시: let bmi = weight / (height / 100 * height / 100)
  6. BMI 판정 로직 (if-else if 사용):
    • 조건문 if, else if, else를 써서 BMI 수치에 따라 다른 판정 라벨 텍스트를 할당.
    • 예시 (세계보건기구 기준):
      • if bmi < 18.5: "저체중"
      • else if bmi < 24.9: "정상"
      • else if bmi < 29.9: "과체중"
      • else: "비만"
  7. 결과 UI 업데이트:
    • 계산된 BMI 값은 String(format: "%.1f", bmi) 처럼 소수점 첫째 자리까지만 표시되게 변환해서 라벨에 넣어줘.
    • 판정 결과도 해당 라벨에 텍스트로 넣어주면 끝!
  8. 에러 처리 (입력값 누락 등):
    • 만약 사용자가 키나 몸무게를 입력 안 하고 계산 버튼 누르면 앱 터지겠지? UITextField가 비어있으면 alert 팝업을 띄워서 "키와 몸무게를 모두 입력해주세요!" 라고 알려주는 센스!

 

init 개념 질문

오, 늠재! init 말이야? 그거 완전 Swift 객체 지향 프로그래밍의 핵.심. 중에 핵심이지! 😎

간단하게 말하면, init은 클래스, 구조체, 열거형의 새로운 인스턴스를 만들 때 사용되는 특별한 함수야. 쉽게 비유하면, '공장에서 물건 만들 때 초기 설정해주는 작업'이라고 생각하면 돼!

자, 그럼 init의 A to Z를 제대로 파헤쳐 보자고!


🚀 init이 뭔데? (기본 개념)

  • 역할: 클래스, 구조체, 열거형의 새 인스턴스를 만들고, 모든 저장 프로퍼티에 초기 값을 할당해서 인스턴스를 사용 가능한 상태로 만드는 거야.
  • 특징:
    • 반환 값이 없어. ()를 붙이지도 않아. 그냥 init 이야!
    • 새 인스턴스가 만들어지면 init 코드가 실행되면서 인스턴스의 초기 상태를 설정해 줘.

swift

// 기본 init 예시: 이름만으로 초기화하는 사람 구조체
struct Person {
    let name: String // 이름은 변경 불가능하게 상수 (let)로 선언

    // init: Person 인스턴스를 만들 때 name 값을 받아서 초기화!
    init(name: String) {
        self.name = name // 받은 name 값을 인스턴스의 name 프로퍼티에 할당
    }
}

// 사용 방법:
let jane = Person(name: "Jane") // 'Jane'이라는 이름을 가진 Person 인스턴스 생성!
print(jane.name) // 출력: Jane

위 코드에서 self.name = name 처럼 self를 붙이는 건, 매개변수 name과 인스턴스의 프로퍼티 name을 구분하기 위함이야. 보통 이름이 같을 때 self를 써서 '이 인스턴스의 name이야!'라고 명확히 해주는 거지.


✨ 구조체(Struct)에서의 init (멤버와이즈 이니셜라이저)

구조체는 킹왕짱 편리한 자동 멤버와이즈 이니셜라이저를 제공해! 모든 저장 프로퍼티를 매개변수로 받는 init을 자동으로 만들어준다는 얘기야.

swift

struct Product {
    var name: String
    var price: Int
}

// Product 구조체는 우리가 init을 따로 정의하지 않아도
// 다음과 같은 init을 자동으로 가지고 있음:
// init(name: String, price: Int) {
//     self.name = name
//     self.price = price
// }

let laptop = Product(name: "MacBook Air", price: 1500000)
print(laptop.name, laptop.price) // 출력: MacBook Air 1500000

근데 만약 네가 커스텀 init을 직접 정의하면, Swift는 이 자동 멤버와이즈 이니셜라이저를 더 이상 제공하지 않아!

swift

struct Book {
    var title: String
    var author: String
    var pages: Int = 0 // 기본값이 있는 프로퍼티

    // 커스텀 init 정의!
    init(bookTitle: String, bookAuthor: String) {
        self.title = bookTitle
        self.author = bookAuthor
        // pages는 기본값이 있으니 여기서 굳이 초기화 안 해도 됨!
    }
    
    // 만약 pages도 매개변수로 받고 싶다면 다른 init을 오버로드하면 됨!
    init(title: String, author: String, pages: Int) {
        self.title = title
        self.author = author
        self.pages = pages
    }
}

let swiftBook = Book(bookTitle: "Swift Programming", bookAuthor: "Apple Inc.")
print(swiftBook.title, swiftBook.pages) // 출력: Swift Programming 0

let advancedBook = Book(title: "Advanced Swift", author: "Hoge", pages: 500)
print(advancedBook.pages) // 출력: 500

// 이제 이 구조체는 init(title: String, author: String, pages: Int) 같은 자동 생성 이니셜라이저가 없어!
// Book(title: "...") 이렇게 기본값 있는 필드까지 매개변수로 받아서 초기화하려고 하면 에러남!

✨ 클래스(Class)에서의 init (지정 vs. 편의 이니셜라이저)

클래스는 구조체보다 init이 좀 더 복잡해! 상속이라는 개념 때문에 지정(Designated) 이니셜라이저와 편의(Convenience) 이니셜라이저가 나뉘어져.

1. 지정(Designated) 이니셜라이저

  • 역할: 클래스의 모든 저장 프로퍼티를 완전히 초기화하는 주된 init이야.
  • 특징: 상위 클래스의 init을 호출해야 할 책임이 있어 (만약 있다면). 클래스는 최소 하나 이상의 지정 이니셜라이저를 가지고 있어야 해.

swift

class Vehicle {
    var numberOfWheels: Int
    var color: String

    // 지정 이니셜라이저! 모든 프로퍼티를 초기화함.
    init(numberOfWheels: Int, color: String) {
        self.numberOfWheels = numberOfWheels
        self.color = color
    }
}

2. 편의(Convenience) 이니셜라이저

  • 역할: 부가적인 이니셜라이저. 특정 매개변수에 기본값을 부여하거나, 특정 사용 사례를 위한 init을 제공할 때 사용돼.
  • 특징: 항상 같은 클래스의 다른 init을 호출해야 해 (결국 최종적으로는 지정 이니셜라이저를 호출). convenience 키워드를 붙여.

swift

class Car: Vehicle {
    var hasSunroof: Bool

    // Car의 지정 이니셜라이저: Vehicle의 numberOfWheels, color와 Car의 hasSunroof 초기화!
    // 상위 클래스의 지정 이니셜라이저를 호출 (super.init)
    init(numberOfWheels: Int, color: String, hasSunroof: Bool) {
        self.hasSunroof = hasSunroof // 자기 프로퍼티 먼저 초기화
        super.init(numberOfWheels: numberOfWheels, color: color) // 상위 클래스 이니셜라이저 호출!
    }

    // 편의 이니셜라이저: 바퀴가 4개인 빨간색 자동차를 빠르게 만들 때 사용!
    convenience init(hasSunroof: Bool) {
        // 같은 클래스의 지정 이니셜라이저를 호출! (self.init)
        self.init(numberOfWheels: 4, color: "빨간색", hasSunroof: hasSunroof)
    }

    // 또 다른 편의 이니셜라이저: 기본적으로 선루프 없는 파란색 자동차!
    convenience init() {
        self.init(numberOfWheels: 4, color: "파란색", hasSunroof: false)
    }
}

let myCar = Car(numberOfWheels: 4, color: "검정", hasSunroof: true)
print(myCar.color) // 출력: 검정

let redCar = Car(hasSunroof: false) // 편의 이니셜라이저 사용!
print(redCar.color, redCar.numberOfWheels) // 출력: 빨간색 4

let basicCar = Car() // 또 다른 편의 이니셜라이저 사용!
print(basicCar.color) // 출력: 파란색

클래스 이니셜라이저 델리게이션 규칙 (초 중요! 🤯)

Swift는 안전하게 초기화하기 위해 델리게이션(위임) 규칙이 있어.

  1. 지정 init은 반드시 상위 클래스의 지정 init을 호출해야 한다. (super.init())
  2. 편의 init은 반드시 같은 클래스의 다른 init을 호출해야 한다. (self.init())
  3. 편의 init은 궁극적으로 같은 클래스의 지정 init을 호출해야 한다. (체이닝되어야 함)

✨ 실패 가능한 이니셜라이저 (init?)

init이 항상 성공하는 건 아니잖아? 특정 조건(예: 잘못된 데이터, 없는 파일)에서는 인스턴스 생성이 실패할 수도 있어. 이때 **실패 가능한 이니셜라이저 (init?)**를 사용해. 인스턴스 생성 실패 시 nil을 반환하지.

swift

enum TemperatureUnit {
    case celsius, fahrenheit
    
    // 실패 가능한 이니셜라이저!
    // 문자열을 받아서 해당 단위로 변환해줘. 모르는 문자열이면 nil 반환.
    init?(symbol: Character) {
        switch symbol {
        case "C":
            self = .celsius
        case "F":
            self = .fahrenheit
        default:
            return nil // 해당 심볼이 없으면 초기화 실패! nil 반환
        }
    }
}

let someUnit = TemperatureUnit(symbol: "C") // Optional(celsius)
let unknownUnit = TemperatureUnit(symbol: "X") // nil

if let unit = someUnit {
    print("성공적으로 \(unit) 단위로 초기화됨.") // 출력: 성공적으로 celsius 단위로 초기화됨.
}

✨ 필수 이니셜라이저 (required init)

클래스에서 특정 init이 모든 하위 클래스에서 반드시 구현되어야 함을 나타낼 때 required 키워드를 사용해.

swift

class SomeClass {
    var name: String

    required init(name: String) {
        self.name = name
    }
}

class SomeSubclass: SomeClass {
    var id: Int

    // SomeSubclass는 SomeClass의 required init(name:)을 반드시 구현해야 함.
    // required 키워드는 여기서도 붙여야 하고, override도 암시적으로 포함됨.
    required init(name: String) {
        self.id = 0 // 자기 프로퍼티 초기화
        super.init(name: name) // 상위 클래스 init 호출
    }
    
    // 만약 이 Subclass에서 required init(name:)을 구현하지 않으면 컴파일 에러!
}

✨ 요약 (핵심 콕콕!)

  • init: 인스턴스 생성 및 초기화의 핵심.
  • 구조체: init 없으면 멤버와이즈 이니셜라이저 자동 생성 (편함!). 커스텀 init 만들면 자동 생성 init 사라짐.
  • 클래스: 상속 때문에 지정과 편의로 나뉨.
    • 지정 (init): 모든 저장 프로퍼티 초기화 담당. 상위 init 호출 (super.init()).
    • 편의 (convenience init): 같은 클래스의 다른 init 호출 (self.init()).
    • 규칙: 상위/하위, 지정/편의 간 호출 관계 중요!
  • init?: 초기화 실패할 수 있을 때 사용 (nil 반환 가능).
  • required init: 모든 서브클래스가 반드시 구현해야 하는 init.

 

swiftUI와 storyboard의 차이점

🎨 1. Storyboard (UIKit 기반의 시각적 도구)

Storyboard는 iOS 앱의 UI를 시각적으로 디자인할 수 있는 도구야. 실제로는 Apple의 전통적인 UI 프레임워크인 UIKit 위에서 동작해.

  • ⚡️ 핵심 개념:
    • 시각적 디자인: Xcode 안에서 화면을 직접 보면서 UI 요소(버튼, 라벨, 이미지 등)를 드래그 앤 드롭해서 배치해.
    • 명령형 프로그래밍 (Imperative): "이 버튼을 여기에 놓아라", "이 라벨의 텍스트를 이렇게 바꿔라" 처럼, UI의 '상태 변화'를 직접 명령하는 방식이야.
  • 🛠️ 사용 방법 (주로 이런 흐름):
    1. Xcode에서 Main.storyboard 파일 열기.
    2. 오른쪽 라이브러리에서 UI 요소를 캔버스(Storyboard 화면)로 드래그!
    3. 오토 레이아웃(Auto Layout)으로 각 요소의 위치와 크기 제약 조건을 설정.
    4. @IBOutlet을 통해 UI 요소와 Swift 코드 파일을 연결.
    5. @IBAction을 통해 버튼 클릭 같은 이벤트와 Swift 코드 함수를 연결.
  • 👍 장점:
    • 시각적 직관성: 눈으로 직접 화면 레이아웃을 확인하면서 만들 수 있어서 초보자도 이해하기 쉬워.
    • 성숙한 생태계: 오랫동안 사용된 만큼 정보, 튜토리얼, 라이브러리가 엄청 많음. 문제 생겨도 검색하면 답이 금방 나옴!
    • 세밀한 제어: UIkit은 iOS의 모든 UI 기능을 커버하므로, 아주 특수하고 세밀한 UI 동작도 구현 가능.
  • 👎 단점:
    • XML 지옥 & 병합 충돌: 스토리보드 파일은 결국 복잡한 XML 텍스트 파일이야. 팀 프로젝트에서 여러 명이 한 스토리보드를 수정하면 merge conflict (충돌)이 자주 발생해서 골치 아플 수 있음.
    • 오토 레이아웃 학습 곡선: 초반에 오토 레이아웃 개념 잡고 능숙해지는 데 시간이 좀 걸려.
    • 코드와 분리: UI는 스토리보드에서, 로직은 코드에서 따로 관리되다 보니 왔다 갔다 해야 하는 번거로움이 있어.
    • 재사용성 낮음: 특정 UI 컴포넌트를 만들어서 다른 프로젝트에서 재사용하기가 좀 번거로워.

🚀 2. SwiftUI (선언형 프레임워크)

SwiftUI는 Apple이 2019년에 발표한 완전히 새로운 UI 프레임워크야. 코드만으로 UI를 만들 수 있도록 설계되었지.

  • ⚡️ 핵심 개념:
    • 코드 기반 & 선언형 프로그래밍 (Declarative): "이런 모습의 화면을 원해!", "이 데이터가 바뀌면 화면이 이렇게 변해야 해" 처럼, UI의 최종적인 '상태'를 코드적으로 선언하는 방식이야. UI를 그리는 과정을 명령하는 게 아니라, 최종 결과를 묘사해.
    • State Driven UI: 앱의 '상태(State)'가 변하면 UI가 자동으로 그에 맞춰 업데이트돼.
  • 🛠️ 사용 방법 (주로 이런 흐름):
    1. Swift 파일 열기.
    2. Swift 코드로 Text("안녕하세요"), Button("클릭") {} 처럼 UI 컴포넌트를 직접 작성.
    3. VStack, HStack, ZStack 같은 스택을 이용해 UI 요소들을 정렬.
    4. @State, @Binding 같은 프로퍼티 래퍼를 이용해 데이터와 UI를 연결하면, 데이터가 바뀔 때마다 UI가 자동으로 업데이트됨.
    5. Xcode의 Canvas 미리보기(Live Preview)를 통해 코드로 작성된 UI를 실시간으로 확인.
  • 👍 장점:
    • 쉬운 학습 곡선 (일부): 기본적인 UI를 구성하는 데 필요한 코드 양이 적고 직관적이야. (물론 깊게 파고들면 또 다른 난관이...)
    • 선언형의 장점: 코드가 훨씬 간결하고 가독성이 높아. 데이터만 바꾸면 UI가 알아서 업데이트되니 동적인 UI 만들기가 개꿀!
    • 실시간 미리보기: Canvas 기능을 통해 코드를 수정하는 즉시 UI 변화를 눈으로 확인할 수 있음.
    • 크로스 플랫폼: SwiftUI 코드는 iOS, iPadOS, macOS, watchOS, tvOS 모두에서 거의 그대로 사용 가능!
    • 높은 재사용성: 작은 컴포넌트를 만들어 쉽게 재활용할 수 있어.
  • 👎 단점:
    • 아직은 어린 프레임워크: 출시된 지 얼마 안 되어 UIKit에 비해 아직 미성숙한 부분, 버그, 레퍼런스가 부족할 수 있어.
    • Canvas의 불안정성: 미리보기가 가끔씩 오류를 내거나 잘 안 될 때가 있어.
    • 특정 기능 부재: UIKit에는 있지만 SwiftUI에는 아직 없는 기능이 있어서, 그럴 땐 UIViewRepresentable이나 UIViewControllerRepresentable을 통해 UIKit 코드를 섞어 써야 함.
    • 디버깅: 특정 레이아웃 문제 발생 시 스토리보드처럼 직접 눈으로 확인하기보다 코드 구조를 더 잘 이해해야 해서 디버깅이 좀 더 어려울 수 있어.

📊 주요 차이점 요약

특징Storyboard (UIKit)SwiftUI
개발 방식 시각적, 드래그앤드롭, 명령형 (Imperative) 코드 기반, 선언형 (Declarative)
메인 도구 스토리보드 캔버스, IBInspectable Swift 코드, Canvas Preview
배움의 곡선 오토 레이아웃이 초기 장벽, 전통적인 방식 기본적인 UI는 쉽지만, 깊이 있는 이해는 필요
유연성/동적 코드 없이는 유연성 떨어짐, 동적 변화 구현 어려움 코드 자체로 유연, 데이터 변화에 따른 UI 업데이트 쉬움
성숙도 매우 성숙하고 안정적, 레퍼런스 방대 계속 성장 중, 최신 OS 버전에서 최적화
팀 협업 Merge Conflict 위험 높음 코드 기반이라 비교적 Merge Conflict 적음
크로스 플랫폼 iOS/iPadOS에 국한 iOS, macOS, watchOS, tvOS 모두 지원

🔥 그래서 늠재, 뭐 써야 돼? (샤를의 조언)

  • 초보자라면, SwiftUI로 시작하는 걸 강추해!
    • Apple이 밀어주는 미래 기술이고, 배우기 쉽고 직관적인 부분이 많아서 개발에 대한 재미를 붙이기 좋을 거야.
    • Canvas 미리보기 덕분에 UI 만드는 재미가 쏠쏠할 걸?
    • 그리고 크로스 플랫폼 앱을 만들 수 있다는 건 엄청난 메리트야!
  • 하지만!
    • 정말 아주 세밀하고 복잡한 '특정' UI 컴포넌트가 필요하거나, 이미 만들어진 기존 프로젝트에 참여한다면 UIKit (Storyboard 또는 코드로 작성된 UIKit)을 알아야 할 때가 올 거야.

 

Textview는 입력받고 싶은 데이터 타입에 따라 키보드 타입도 바꿀 수 있다

ex) 휴대폰 쓸 때 전화번호 적어야하는 칸에는 숫자 키보드만 올라오는 것

 

    override func viewDidLoad() {

        super.viewDidLoad()

        print("시작")

    }

 

viewController에 있는, 앱이 실행될 때 자동으로 실행되는 메소드(시험 자주 등장)

'코딩공부' 카테고리의 다른 글

iOS 11주차  (0) 2025.11.17
iOS 9주차  (0) 2025.10.27
iOS 7주차  (0) 2025.10.13
iOS 3주차  (0) 2025.09.15
iOS 1주차  (0) 2025.09.01