
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 (꺼짐)로 변경합니다. (제거 후에는 켜져 있다고 보기 어려우므로)
}) // 클로저
** 소스 출처 **
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
🧑💻 초보자를 위한 구현 팁 (⭐핵심⭐):
- 스토리보드로 UI 먼저 배치:
- UILabel, UITextField, UIButton 드래그해서 적당히 배치해봐.
- Auto Layout으로 화면 크기 달라도 잘 보이게 제약 조건(Constraints) 거는 연습도 해보면 굿! (처음엔 좀 어려울 수 있는데, 안 하면 아이폰 종류 바뀔 때마다 UI 박살남 ㅠㅠ)
- @IBOutlet 연결:
- UITextField 2개랑 UILabel 2개를 코드(ViewController.swift)랑 연결해서 코드에서 제어할 수 있게 만들어야 함. (ctrl 누르고 드래그!)
- @IBAction 연결:
- UIButton을 코드랑 연결해서 버튼이 눌리면 특정 함수가 실행되게 만들어야 함.
- 데이터 가져오기 및 형 변환:
- 텍스트 필드의 입력 값은 기본적으로 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 이걸로 숫자만 입력받게 하면 좀 편하겠지?
- BMI 공식:
- BMI = 몸무게(kg) / (키(m) * 키(m))
- ⚠️ 주의: 키를 cm로 입력받았다면, (cm / 100)으로 미터(m)로 변환해줘야 해!
- 예시: let bmi = weight / (height / 100 * height / 100)
- BMI 판정 로직 (if-else if 사용):
- 조건문 if, else if, else를 써서 BMI 수치에 따라 다른 판정 라벨 텍스트를 할당.
- 예시 (세계보건기구 기준):
- if bmi < 18.5: "저체중"
- else if bmi < 24.9: "정상"
- else if bmi < 29.9: "과체중"
- else: "비만"
- 결과 UI 업데이트:
- 계산된 BMI 값은 String(format: "%.1f", bmi) 처럼 소수점 첫째 자리까지만 표시되게 변환해서 라벨에 넣어줘.
- 판정 결과도 해당 라벨에 텍스트로 넣어주면 끝!
- 에러 처리 (입력값 누락 등):
- 만약 사용자가 키나 몸무게를 입력 안 하고 계산 버튼 누르면 앱 터지겠지? 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는 안전하게 초기화하기 위해 델리게이션(위임) 규칙이 있어.
- 지정 init은 반드시 상위 클래스의 지정 init을 호출해야 한다. (super.init())
- 편의 init은 반드시 같은 클래스의 다른 init을 호출해야 한다. (self.init())
- 편의 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의 '상태 변화'를 직접 명령하는 방식이야.
- 🛠️ 사용 방법 (주로 이런 흐름):
- Xcode에서 Main.storyboard 파일 열기.
- 오른쪽 라이브러리에서 UI 요소를 캔버스(Storyboard 화면)로 드래그!
- 오토 레이아웃(Auto Layout)으로 각 요소의 위치와 크기 제약 조건을 설정.
- @IBOutlet을 통해 UI 요소와 Swift 코드 파일을 연결.
- @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가 자동으로 그에 맞춰 업데이트돼.
- 🛠️ 사용 방법 (주로 이런 흐름):
- Swift 파일 열기.
- Swift 코드로 Text("안녕하세요"), Button("클릭") {} 처럼 UI 컴포넌트를 직접 작성.
- VStack, HStack, ZStack 같은 스택을 이용해 UI 요소들을 정렬.
- @State, @Binding 같은 프로퍼티 래퍼를 이용해 데이터와 UI를 연결하면, 데이터가 바뀔 때마다 UI가 자동으로 업데이트됨.
- 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에 있는, 앱이 실행될 때 자동으로 실행되는 메소드(시험 자주 등장)