
우리는 이제 이론을 대충 알았으니 실전으로 넘어갈거임

이 뷰를 한번 만들어볼거임

Object Library에서 View Controller 생성

UILabel 3, UITextfield 1 적절히 배치

- UITextField fontSize: 70/Bold
- Aligment: Center
- PlaceHolder: Value
- Text Input Traits
- Keyboard: Decimal Pad
- Border Style: doted line(첫번째)

- UILabel fontSize: 36 or 70/Bold
- Alignment: Center

(추후에 디자이너가 정해준 색깔을 쓸때, 저렇게 HexColor쓰면댐)

- 테스트 방법 1
- 태스트 방법 2
- 얘네는 키보드 코드로 짜는거아니면 수동으로해줘야함
안드로이드에서 Xml을 Fragment나 Activity에 연결했던것처럼
새로 코드를 만들어주자.

New File From Template ㄱㄱ

좀 뭐가 다양하게많음 나중에 하나씩 살펴보셈

클래스 이름 정하고, 아래 Subclass of 를 통해 상속받을 클래스 (UIViewController, UITableViewCall 등등..)
을 정할 수 있음 이대로 ㄱ

위치 알아서 정하고

하면 이렇게 잘 나옴.

그럼 연결된거임

이런 구조

대체로 UIViewController의 라이프 사이클은 위와 같음.
조금 더 깊게 설명하자면,
1. 초기화 단계 (Initialization)
뷰 컨트롤러 객체가 처음 생성될 때 호출되는 메서드
init(coder:)required init?(coder: NSCoder) {
super.init(coder: coder)
print("스토리보드에서 뷰 컨트롤러 초기화")
// 여기서는 아직 뷰나 IBOutlet 등에 접근할 수 없음
}
init(nibName:bundle:)override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
print("코드로 뷰 컨트롤러 초기화")
// 여기서는 아직 뷰나 IBOutlet 등에 접근할 수 없음
}
2. 뷰 로딩 단계 (View Loading)
뷰 컨트롤러의 뷰가 메모리에 로드되는 단계입니다.
loadView()override func loadView() {
super.loadView()
print("뷰 계층 구조 생성")
// 코드로 뷰를 생성하는 경우
// self.view = CustomView() // 뷰를 직접 생성하여 할당
}
viewDidLoad()override func viewDidLoad() {
super.viewDidLoad()
print("뷰가 메모리에 로드됨")
// 초기화 작업 수행
// - UI 컴포넌트 초기 설정
// - 데이터 초기 로딩
// - 이벤트 리스너 등록
// - 네트워크 요청 초기화
setupTableView()
registerCells()
configureNavigationBar()
}
3. 뷰 표시 준비 단계 (View Will Appear)
뷰가 화면에 표시되기 직전에 호출되는 메서드입니다.
viewWillAppear(_:)override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("뷰가 화면에 표시되기 직전")
// 화면이 나타날 때마다 수행해야 할 작업
// - 데이터 새로고침
// - UI 상태 업데이트
// - 네비게이션 바 설정 변경
// - 알림 등록
refreshData()
updateUIState()
navigationController?.setNavigationBarHidden(false, animated: true)
NotificationCenter.default.addObserver(self, selector: #selector(handleNotification), name: .someNotification, object: nil)
}
4. 레이아웃 단계 (Layout)
뷰의 레이아웃이 결정되는 단계입니다.
viewWillLayoutSubviews()override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
print("서브뷰 레이아웃 직전")
// 레이아웃 변경 직전 작업
// - 특정 뷰의 프레임 조정
// - 레이아웃 제약조건 준비
prepareCustomLayoutIfNeeded()
}
viewDidLayoutSubviews()override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("서브뷰 레이아웃 완료")
// 레이아웃 완료 후 작업
// - 스크롤 위치 조정
// - 동적 크기 뷰 업데이트
// - 컨텐츠 오프셋 설정
updateScrollViewContentInset()
adjustContentIfNeeded()
}
5. 뷰 표시 완료 단계 (View Did Appear)
뷰가 화면에 완전히 표시된 후 호출되는 메서드입니다.
viewDidAppear(_:)override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("뷰가 화면에 완전히 표시됨")
// 화면이 완전히 표시된 후 작업
// - 애니메이션 시작
// - 타이머 시작
// - 사용자 입력 처리 시작
// - 추가 데이터 로딩
startAnimations()
startRefreshTimer()
becomeFirstResponder() // 키보드 표시 등
loadAdditionalData()
}
6. 뷰 제거 준비 단계 (View Will Disappear)
뷰가 화면에서 사라지기 직전에 호출되는 메서드입니다.
viewWillDisappear(_:)override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("뷰가 화면에서 사라지기 직전")
// 화면이 사라지기 전 작업
// - 키보드 숨기기
// - 타이머 일시 정지
// - 사용자 입력 저장
// - 알림 해제
view.endEditing(true)
pauseTimers()
saveUserInput()
NotificationCenter.default.removeObserver(self, name: .someNotification, object: nil)
}
7. 뷰 제거 완료 단계 (View Did Disappear)
뷰가 화면에서 완전히 사라진 후 호출되는 메서드입니다.
viewDidDisappear(_:)override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print("뷰가 화면에서 완전히 사라짐")
// 화면이 사라진 후 작업
// - 리소스 해제
// - 메모리 정리
// - 애니메이션 중단
// - 비용이 큰 작업 중단
stopAnimations()
releaseAudioResources()
cancelNetworkRequests()
}
8. 메모리 관리 단계 (Memory Management)
메모리 관리와 관련된 메서드들입니다.
didReceiveMemoryWarning()override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
print("메모리 부족 경고 발생")
// 메모리 확보 작업
// - 캐시 정리
// - 사용하지 않는 객체 해제
// - 이미지 캐시 정리
clearImageCache()
releaseUnusedObjects()
}
deinitdeinit {
print("뷰 컨트롤러 메모리 해제")
// 최종 정리 작업
// - 남은 옵저버 제거
// - 타이머 정지
// - 순환 참조 해제
NotificationCenter.default.removeObserver(self)
timer?.invalidate()
closure = nil // 클로저에 의한 순환 참조 해제
}
9. 중요 사항
viewWillAppear, viewDidAppear, viewWillDisappear, viewDidDisappear는 화면 전환마다 반복해서 호출됩니다.viewDidLoad는 뷰 컨트롤러 생성 후 한 번만 호출됩니다.super 버전을 호출해야 합니다.10. 다양한 시나리오
init → loadView → viewDidLoad → viewWillAppear → viewWillLayoutSubviews → viewDidLayoutSubviews → viewDidAppearviewWillDisappear → viewDidDisappearviewWillAppear → viewWillLayoutSubviews → viewDidLayoutSubviews → viewDidAppearviewWillAppear → viewDidAppearviewWillDisappear → viewDidDisappear → deinit라고 클로드랑 블로그가 알려줌 앱 자체의 생명주기랑은 다른거임
절대 헷갈리면안됌.
코드를 짜기전에

처음에 나올 화면을 저렇게 세팅해줘여함
안그럼 저기 첨에 만들었던 오른쪽화면이 나옴
-> 가 있어여함
이제 다음과 같은 코드를 넣어서 테스트해보자
class ConversionViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// view.subviews[0]이 UITextField 타입인지 검사 (is 연산자 사용)
if view.subviews[0] is UITextField {
// 조건이 참이면 UITextField로 다운캐스팅 후 옵셔널 체이닝(?.)을 통해 text 속성에 접근
(view.subviews[0] as? UITextField)?.text = "123"
}
// 화씨 온도(123.0)를 섭씨로 변환하는 계산식
// 5.0/9.0 * (F - 32) 공식 사용
let celsiusValue = 5.0 / 9.0 * (123.0 - 32)
// view.subviews[3]이 UILabel 타입인지 검사
if view.subviews[3] is UILabel {
// 강제 다운캐스팅(as!)을 사용해 UILabel로 변환 후 text 속성에 값 할당
// String() 생성자를 사용해 Double 값을 문자열로 변환
(view.subviews[3] as! UILabel).text = String(celsiusValue)
}
}
}

오른쪽 스토리보드의 Safe Area 밑에 View들은 사실 배열의 나열이다.

이런식으로
중요하게 볼 포인트는
- viewDidLoad()
뷰 컨트롤러의 뷰가 메모리에 로드된 직후 호출되는 메서드
UI 초기화와 데이터 설정에 적합한 위치- subviews 배열
view.subviews[0]: 첫 번째 서브뷰 (화씨 온도 입력 필드)
view.subviews[3]: 네 번째 서브뷰 (섭씨 결과를 표시할 레이블)- 타입 체크와 캐스팅
is UITextField: 객체가 UITextField 타입인지 확인
as? UITextField: 안전한 다운캐스팅 (실패 시 nil 반환)
as! UILabel: 강제 다운캐스팅 (실패 시 런타임 오류)- 옵셔널 체이닝
(view.subviews[0] as? UITextField)?.text: 캐스팅 성공 시에만 text 속성에 접근
만약, 객체의 타입이 더 명확할 땐,
강제 다운캐스팅(as!) 을 사용해주는게 더 깔끔 할 수도있음.
- Optional Chain을 사용하여 간단하게 해보자
• xxx?.yyy?.zzz?에서 하나라도 nil이면 전체가 nil임
class ConversionViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// if view.subviews[0] is UITextField {
// (view.subviews[0] as? UITextField)?.text = "123"
// }
// 어차피 view.subviews[0]가 UITextField이 아니라면 "123"은 나타나지 않는다.
(view.subviews[0] as? UITextField)?.text = "123"
let celsiusValue = 5.0 / 9.0 * (123.0 - 32)
// if view.subviews[3] is UILabel {
// (view.subviews[3] as! UILabel).text = String(celsiusValue)
// }
// 어차피 view.subviews[3]이 UITextField이 아니라면 "123"은 나타나지 않는다.
(view.subviews[3] as? UILabel)?.text = String(celsiusValue)
}
}
as! 강제 다운캐스팅을 사용해 타입 변환 후 속성에 직접 접근할 수 도 있음.
– Implicit Unrapping을 사용하여 ?를 최소화
• 명확한 것은 ?보다 !를 사용
class ConversionViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// (view.subviews[0] as? UITextField)?.text = "123"
// view.subviews[0]가 명확히 UITextField이기 때문에 as!를 사용
// 따라서 두번째 ?를 제거할 수 있다.
(view.subviews[0] as! UITextField).text = "123"
let celsiusValue = 5.0 / 9.0 * (123.0 - 32)
// (view.subviews[4] as? UILabel)?.text = String(celsiusValue)
// view.subviews[4]가 명확히 UILabel이기 때문에 as!를 사용
// 따라서 두번째 ?를 제거할 수 있다.
(view.subviews[4] as! UILabel).text = String(celsiusValue)
}
}
직접 참조를 하기에는 UView가 생길때마다 계속 일일이 해줘야함
따라서,
fahrenheitTextField와 celsiusLabel 프로퍼티를 클래스에 선언하고 초기화 후 재사용

class ConversionViewController: UIViewController {
var fahrenheitTextField: UITextField!
var celsiusLabel: UILabel?
// !와 ?의 차이가 무엇인지 아래에서 파악하라
override func viewDidLoad() {
super.viewDidLoad()
fahrenheitTextField = view.subviews[0] as! UITextField
celsiusLabel = view.subviews[4] as? UILabel
// (view.subviews[0] as! UITextField).text = "123"
fahrenheitTextField.text = "123"
let celsiusValue = 5.0 / 9.0 * (123.0 - 32)
// (view.subviews[4] as! UILabel).text = String(celsiusValue)
celsiusLabel?.text = String(celsiusValue)
}
}
근데 사실 이것도 사용 안함.
▪ 앞 방법의 문제점
▪ xcode에서 다소 편리한 기능제공
방법1
방법2
함수간 연결 방법
그러면 다음과 같은 화면이 보일꺼임
대부분의 이야기는 여기서 어느정도 설명했으므르 패스
class ConversionViewController: UIViewController {
@IBOutlet weak var celsiusLabel: UILabel!
@IBOutlet weak var fahrenheitTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func fahrenheitEditingChanged(_ sender: UITextField) {
// 여기서 sender이 fahrenheitTextField 객체이다.
if let text = sender.text { // 그내용이 뭔가 있으면, 옵셔널 바인딩
// 문자열 -> Double 형변환, Double(text)가 nil수도 있다
if let fahrenheitValue = Double(text){
let celsiusValue = 5.0/9.0*(fahrenheitValue - 32.0) // 변환
celsiusLabel.text = String(format: "%.2f", celsiusValue)
}else{
celsiusLabel.text = "???"
}
}
}
}
UIButton: 탭할 수 있는 버튼
UIDatePicker: 날짜와 시간 선택기
UIPageControl: 페이지 표시기
UISegmentedControl: 세그먼트 컨트롤
UITextField: 텍스트 입력 필드
UISlider: 슬라이더
UISwitch: 전환 스위치
등등...

얘는 이벤트 흐름뿐만 아니라 iOS 앱에서 사용자 상호작용의 전반적인 처리를 담당하는 핵심 클래스
UIView의 수퍼 클래스이다. → 모든 UI객체는 이 객체의 하위 객체임
터치 이벤트, 모션 이벤트, 원격 제어 이벤트, 프레스 이벤트 등 다양한 유형의 이벤트 처리
응답자 체인(Responder Chain)의 일부로 작동하여 이벤트 처리의 계층 구조 형성
첫 번째 응답자(First Responder) 기능 지원
• NSObject -> UIResponder -> (UIView, UIViewController, UIApplication 등)
UIView: 화면에 콘텐츠를 표시하는 모든 시각적 요소의 기본 클래스
UIViewController: 뷰의 콘텐츠를 관리하는 컨트롤러
UIApplication: 앱의 이벤트 루프를 관리하고 앱 수준의 기능 제공

Managing the Responder Chain(이벤트의 흐름)

▪ First Responder 지정
▪ First Responder의 해제
뭔소리냐면
▪ 핵심 관계
becomeFirstResponder(): First Responder 상태가 되도록 요청resignFirstResponder(): First Responder 상태를 포기요약하면, First Responder는 UIResponder의 특별한 상태이며, 이 상태를 가진 UIResponder가 이벤트를 최초로 수신하는 권한을 갖습니다. 이는 마치 여러 사람(UIResponder) 중에서 현재 발언권(First Responder)을 가진 한 사람이 우선적으로 발언(이벤트 수신)할 수 있는 것과 같습니다.
라고 클로드가 알려줌
마지막으로, IOS는 안드로이드 애뮬레이터마냥
알아서 키보드가 생기고 그러지않음 개쓰레기같은거
그래서 View에 Tap 제스처로 해결해야만함

– TextField외 어느 곳이든 클릭(Tapping)하면 호출되는 함수 지정
– UITapGestureRecognizer 객체 생성 및 view에 부착
Delegation: 특정 객체가 해야 하는 일을 대행하는 위임자(delegate)를 지정하는 디자인 패턴
목적: 객체 간 결합도를 낮추고 코드의 재사용성을 높임
구조: 하나의 delegate는 여러 개의 callback 함수를 포함할 수 있음

- 위임 관계 설정
위임하는 객체(Delegator)는 delegate 속성을 통해 위임받는 객체를 참조
delegate가 nil이면 위임하는 객체는 관련 이벤트에 대해 아무 동작도 수행하지 않음
- 프로토콜 정의
위임 관계는 프로토콜(Swift/Objective-C)로 정의됨
이 프로토콜은 위임받는 객체가 구현해야 할 메서드들을 선언
- 이벤트 처리
위임하는 객체에서 이벤트 발생 시, delegate의 적절한 메서드를 호출
위임받는 객체는 호출된 메서드에서 필요한 작업을 수행
EX : UITextFieldDelegate 프로토콜
– 프로토콜은 자바에서 인터페이스와 같다

– 아래 그림과 같이 별도의 extension에서 UITextFieldDelegate를 상속받는다.
– extension 내부({ })에서 textField를 타이핑하면 관련 딜리게이터 함수에 대한 팝 업창이 뜬다.
– 그곳에서 원하는 릴리게이터 함수를 선택한다.

완성 코드
//
// ConversionViewController.swift
import UIKit
class ConversionViewController: UIViewController {
@IBOutlet weak var fahrenheitTextField: UITextField!
@IBOutlet weak var celsiusLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
// – UITapGestureRecognizer 객체 생성 및 view에 부착
view.addGestureRecognizer(tapGesture)
//Delegator 지정
fahrenheitTextField.delegate = self
}
@IBAction func fahrenheitEditingChanged(_ sender: UITextField) {
if let text = sender.text { // 그내용이 뭔가 있으면, 옵셔널 바인딩
// 문자열 -> Double 형변환, Double(text)가 nil
if let fahrenheitValue = Double(text){
let celsiusValue = 5.0/9.0*(fahrenheitValue - 32.0) // 변환
celsiusLabel.text = String(format: "%.2f", celsiusValue)
}else{
celsiusLabel.text = "???"
}
}
}
}
// TextField외 어느 곳이든 클릭(Tapping)하면 호출되는 함수 지정
extension ConversionViewController{
@objc func dismissKeyboard(sender: UITapGestureRecognizer){
fahrenheitTextField.resignFirstResponder()
}
}
// UITextFieldDelegate 생성
extension ConversionViewController: UITextFieldDelegate{
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let existing = textField.text?.range(of: ".")
let comming = string.range(of: ".")
if existing != nil, comming != nil {
return false
}
return true
}
}