안녕하세요, iOS 개발하는 루피입니다.
오늘은 인터페이스를 관리하고, 앱의 콘텐츠 간 네비게이션을 구현하는 데 필요한 View Controller에 대해 정리해 보도록 하겠습니다. 바로 시작합니다. ( 편의상 ViewController를 VC라고 하겠습니다. )
View Controller란?
VC는 UIKit 앱의 인터페이스를 관리하는 핵심 요소입니다. 모든 iOS 앱은 최소 하나 이상의 VC를 가지고 있으며, 이는 앱의 메인 윈도우를 채우는 역할을 하죠! 간단히 말하자면, VC가 하는 일은 다음과 같습니다.
- 앱 UI의 특정 부분, 즉 View를 관리합니다.
- 해당 UI와 데이터 간의 상호작용을 담당합니다.
- 앱 내에서 서로 다른 UI 간의 전환 처리를 담당합니다. (네비게이션은 이에 해당하겠죠?)
각 VC는 하나의 루트 뷰를 관리하며, 이 뷰 안에는 여러 개의 subview가 포함될 수 있습니다. 뷰 계층구조에서 발생하는 사용자 인터랙션은 VC가 처리하고, 필요시 앱의 다른 객체들과 협력합니다. VC 혼자서 모든 일을 처리하지는 않는다는 거죠!!
근데 다른 객체가 뭔데??
예를 들어 데이터 로딩은 NetworkManager에게, 사용자 정보 관리는 UserManager에게 위임하는 방식으로 VC는 역할을 위임합니다. 역시 코드로 보는게 좀 더 편하겠죠?
1) UserManager를 이용한 데이터 관리 객체와 협력
class ProfileViewController: UIViewController {
func loadUserData() {
// UserManager라는 다른 객체와 협력
UserManager.shared.fetchUserProfile { [weak self] user in
self?.updateUI(with: user)
}
}
}
2) NetworkManager를 이용한 네트워크 매니저와 협력
class PostListViewController: UIViewController {
func refreshPosts() {
// NetworkManager와 협력해서 데이터를 가져옴
NetworkManager.shared.fetchPosts { [weak self] posts in
self?.posts = posts
self?.tableView.reloadData()
}
}
}
3) 다른 VC와 협력
class MainViewController: UIViewController {
@IBAction func showDetail() {
// DetailViewController와 협력
let detailVC = DetailViewController()
detailVC.delegate = self // 서로 소통할 수 있게 설정
present(detailVC, animated: true)
}
}
Etc
그 외에도 위치 서비스는 CLLocationManager , 카메라는 AVCaptureSession, 알림은 NotificationCenter 같은 객체들을 이용해 VC는 사용자 인터렉션을 처리합니다.
ViewController의 종류
VC는 크게 2가지의 종류로 나뉩니다.
1) Content View Controller
앱의 특정 콘텐츠를 관리하는 VC로, 개발자가 주로 생성하는 기본적인 VC입니다.
class ProductDetailViewController: UIViewController {
@IBOutlet weak var productImageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var priceLabel: UILabel!
var product: Product?
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
guard let product = product else { return }
// 자신의 루트 뷰에 직접 콘텐츠 관리
titleLabel.text = product.title
priceLabel.text = product.formattedPrice
productImageView.loadImage(from: product.imageURL)
}
}
2) Container View Controller
다른 VC의 정보를 모아 탐색하기 쉽게 하거나, 자식 VC의 콘텐츠를 새로운 방식으로 표시하는 역할을 하는 VC입니다.
class CustomTabBarController: UIViewController {
private var viewControllers: [UIViewController] = []
private var currentIndex = 0
override func viewDidLoad() {
super.viewDidLoad()
setupContainer()
}
private func setupContainer() {
let homeVC = HomeViewController()
let searchVC = SearchViewController()
let profileVC = ProfileViewController()
viewControllers = [homeVC, searchVC, profileVC]
// 자식 VC들을 Container에 추가
for (index, childVC) in viewControllers.enumerated() {
addChild(childVC)
view.addSubview(childVC.view)
childVC.didMove(toParent: self)
// 첫 번째 VC만 보이도록 설정
childVC.view.isHidden = (index != 0)
}
}
func switchToViewController(at index: Int) {
guard index < viewControllers.count else { return }
// Container는 자식 VC의 Root View만 관리
viewControllers[currentIndex].view.isHidden = true
viewControllers[index].view.isHidden = false
currentIndex = index
}
}
Container VC에는 몇 가지 특징이 있는데요.
- 자식 VC의 Root View만 관리하며, 콘텐츠를 직접 관리하지 않습니다
- 자식 뷰의 크기를 조정하고 Container의 디자인에 따라 배치합니다
- 예를 들어 UINavigationController, UITabBarController, UISplitViewController 가 있습니다.
이들은 자신만의 콘텐츠는 없지만, 다른 화면을 관리하는 역할을 하고 있죠!!
View controller의 핵심 역할 5가지
뷰 관리 (View Management)
VC의 가장 중요한 역할은 View 계층 구조를 관리하는 것입니다.
모든 VC는 하나의 Root View를 가지고 있으며, 이는 해당 VC의 모든 콘텐츠를 감싸는 역할을 합니다. Root View에 필요한 하위 View들을 추가하여 콘텐츠를 화면에 표시합니다.
이처럼 VC는 항상 자신의 Root View에 대한 참조를 가지고 있으며, Root View는 각 Subview들을 강한 참조로 관리하는데요. Content VC의 경우 자신이 가진 모든 뷰를 직접 관리하고, Container VC는 자신의 View 뿐만 아니라 하나 이상의 자식 VC의 Root View도 함께 관리합니다.
하지만 Container VC는 자식 VC의 콘텐츠를 직접 관리하지 않습니다. 대신 자식 뷰의 Root View만 관리하며, 이를 Container의 디자인에 따라 크기를 조정하고 배치하곤 하죠!
예를 들어 보겠습니다.
Content View Controller
class ContentViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 자신의 루트 뷰에 직접 콘텐츠 추가
view.backgroundColor = .white
let label = UILabel()
label.text = "This is a Content View Controller"
label.textAlignment = .center
label.frame = view.bounds
view.addSubview(label)
}
}
Container View Controller
import UIKit
// Master (왼쪽 화면)
class MasterViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .lightGray
let label = UILabel()
label.text = "Master View"
label.textAlignment = .center
label.frame = view.bounds
view.addSubview(label)
}
}
// Detail (오른쪽 화면)
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let label = UILabel()
label.text = "Detail View"
label.textAlignment = .center
label.frame = view.bounds
view.addSubview(label)
}
}
// Split View Controller (컨테이너)
class SplitViewExample: UISplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Master와 Detail을 자식으로 추가
let masterVC = MasterViewController()
let detailVC = DetailViewController()
self.viewControllers = [masterVC, detailVC]
}
}
데이터 중계(Data Marshaling)
VC는 자신이 관리하는 View와 앱의 데이터를 연결하는 중간 역할을 합니다.
이에 대해 VC는 몇 가지 원칙을 가지고 있습니다.
- VC와 데이터 객체 간에는 항상 명확한 책임 분리를 유지해야 합니다.
- 데이터 구조의 무결성을 보장하는 로직은 데이터 객체에 속해야 합니다.
- VC는 주로 입력값 검증과 데이터 형식 변환 역할을 담당합니다.
우리가 흔히 아는 MVC 패턴을 통해 우리는 이러한 VC의 역할을 좀 더 명확하게 알고 있죠!!
사용자 상호작용(User Interactions)
VC는 Responder Object로, Responder Chain을 통해 전달되는 이벤트를 처리할 수 있습니다.
Responder Object
iOS에서 이벤트를 처리할 수 있는 객체를 말합니다.
- UIViewController, UIView, UIWindow 등은 모두 Responder Object입니다.
- 이벤트가 발생하면 iOS는 이를 적절한 응답자 객체로 전달하여 처리합니다.
Responder Chain
이벤트가 발생했을 때, 이를 처리할 적절한 객체를 찾기 위해 iOS가 사용하는 체계입니다.
- 터치 이벤트가 View에서 발생하면, 해당 View가 먼저 이벤트를 처리하려고 시도합니다.
- View가 이벤트를 처리하지 못하면, 이벤트는 해당 View의 상위 View나 VC로 전달됩니다.
- 최종적으로 UIApplication이나 UIWindow까지 전달될 수 있습니다.
하지만, VC가 직접 터치 이벤트를 처리하는 경우는 드뭅니다. 대신, 일반적으로 뷰가 자신의 터치 이벤트를 처리한 후, 그 결과를 관련된 Delegate 메서드나 Target 객체의 메서드로 전달합니다. 이 Target 객체가 보통 VC입니다.
따라서 VC에서 대부분의 이벤트는 Delegate 메서드나 Action 메서드를 통해 처리 되게 되는 것이죠.
리소스 관리(Resource Management)
VC는 자신이 생성한 View와 객체들에 대한 모든 책임을 집니다.
UIViewController 클래스는 대부분의 View 관리 작업을 자동으로 처리합니다. 예를 들어, UIKit은 더 이상 필요하지 않은 View 관련 리소스를 자동으로 해제하게 되는 거죠!!
하지만, UIViewController를 서브클래싱하여 개발자가 명시적으로 생성한 객체는 직접 관리해야 하는데요..? 좀 더 살펴보도록 하겠습니다.
직접 관리해야 하는 경우
class MyViewController: UIViewController {
var imageCache: [String: UIImage] = [:] // 캐시 데이터
override func viewDidLoad() {
super.viewDidLoad()
// 이미지 캐시 초기화
imageCache["example"] = UIImage(named: "exampleImage")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// 메모리가 부족할 때 캐시 데이터 정리
imageCache.removeAll()
print("Image cache cleared!")
}
}
UIKit은 사용 가능한 메모리가 부족할 경우, 앱에게 더 이상 필요하지 않은 리소스를 해제하도록 요청하고, 이를 위해 UIKit은 VC의 didReceiveMemoryWarning 메서드를 호출합니다. 이 메서드에서 다음과 같은 작업을 수행할 수 있습니다
- 더 이상 필요하지 않은 객체에 대한 참조를 제거합니다.
- 나중에 쉽게 다시 생성할 수 있는 캐시 데이터를 삭제합니다.
iOS 6부터 viewDidUnload와 viewWillUnload가 deprecated되었습니다. 이제 View는 메모리 경고 시 자동으로 언로드되지 않으므로, 개발자는 캐시 데이터 정리에만 집중하면 됩니다.
저메모리 상황에서는 가능한 많은 메모리를 해제하는 것이 중요합니다. 메모리를 과도하게 사용하는 앱은 시스템에 의해 강제로 종료될 수 있기 때문이죠!
적응성(Adaptivity)
View Controller는 아이폰, 아이패드 등 다양한 환경 변화에 맞춰 화면을 최적의 모습으로 조정하는 역할을 합니다. 모든 기기에서 앱이 자연스럽게 보이도록 만드는 것이 바로 적응성의 핵심입니다.
- 📱 iPhone SE: 320pt
- 📱 iPhone 15 Pro Max: 430pt
- 🖥️ iPad Pro: 1024pt
하나의 코드로 이 모든 화면에 대응해야 합니다.
적응성 처리 방법: '시스템 설계' vs '직접 제어'
iOS에서 적응성을 처리하는 방법은 크게 두 가지 철학으로 나뉩니다.
- 대규모 변화 (시스템 설계): Size Class라는 보편적인 규칙을 만들어 여러 상황에 재사용하는 방식입니다.
- 세부적 변화 (직접 제어): Auto Layout이나 코드를 이용해 특정 이벤트에 맞춰 UI를 정밀하게 조정하는 방식입니다.
대규모 변화: Size Class라는 절대 기준
대규모 변화는 iOS가 정해놓은 Size Class라는 절대적인 공간 기준이 바뀔 때 대응하는 방식입니다. Size Class는 화면 공간이 넓은 편(Regular)인지, 좁은 편(Compact)인지를 나타내는 약속같은 것입니다.
개발자는 이 약속을 믿고, 각 규격에 맞는 레이아웃을 한 번만 만들어두고 해당 사이즈에 부합한다면 레이아웃을 재사용할 수 있는 거죠.
- Compact 설계도: 아이폰처럼 좁은 공간을 위한 레이아웃
- Regular 설계도: 아이패드처럼 넓은 공간을 위한 레이아웃
이렇게 준비만 해두면, iOS가 상황에 맞게 최적의 설계도를 자동으로 적용해 줍니다. 예를 들어, 우리가 만든 Compact 설계도는 다음과 같은 모든 상황에서 재사용됩니다.
- 모든 아이폰의 세로 모드
- 일반 아이폰의 가로 모드
- 아이패드를 분할 화면(Split View)으로 사용해 앱이 좁아졌을 때
"아이폰용으로 만든 메일 앱 UI가 아이패드 분할 화면의 좁은 공간에서 그대로 보이는 것"이 바로 이 원리 덕분입니다. 개발자는 한 번 작업했을 뿐인데, 시스템이 알아서 여러 상황에 적용해 준 것이죠. 아래 사진을 보면 완전 UI가 재사용되었음을 확인할 수 있습니다. 해당 스플릿 뷰는 동일한 Compact 유형이기 때문입니다.
Size Class 표
| 기기 | 세로 모드 | 가로 모드 |
| iPhone (일반) | Compact × Regular | Compact × Compact |
| iPhone Plus/Max | Compact × Regular | Regular × Compact |
| iPad (모든 모델) | Regular × Regular | Regular × Regular |
// iOS 16 이하에서 사용하는 방식
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// 현재 Size Class를 확인하여 적합한 레이아웃을 활성화합니다.
if previousTraitCollection?.horizontalSizeClass != traitCollection.horizontalSizeClass {
if traitCollection.horizontalSizeClass == .regular {
setupRegularLayout() // 'Regular 설계도'를 적용
} else {
setupCompactLayout() // 'Compact 설계도'를 적용
}
}
}
iOS 17+ (새로운 방식)
// iOS 17.0 이상 권장 방식
override func viewDidLoad() {
super.viewDidLoad()
// 특정 trait 변경만 관찰하도록 등록
registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: Self, previousTraitCollection) in
if self.traitCollection.horizontalSizeClass == .regular {
self.setupRegularLayout()
} else {
self.setupCompactLayout()
}
}
// 초기 레이아웃 설정
configureView()
}
iOS 17 변경사항: traitCollectionDidChange가 deprecated되었습니다. 새로운 registerForTraitChanges API는 특정 trait만 관찰할 수 있어 성능이 향상되었습니다.
세부적 변화: Auto Layout과 코드로 직접 제어
세부적 변화는 Size Class라는 큰 기준은 바뀌지 않았지만, 화면 회전처럼 구체적인 상황이 변했을 때 개발자가 직접 개입하여 UI를 제어하는 방식입니다. 가장 흔한 예로, 일반 아이폰을 가로로 돌리면 Width Size Class는 여전히 Compact입니다. 즉, 시스템 입장에서는 '대규모 변화'가 아닙니다. 하지만, 우리는 가로모드에 따라 다른 UI를 확인할 수 있죠!!
- Auto Layout: 가장 기본적인 제어입니다. 우리가 설정한 "왼쪽에서 20pt 떨어져라" 같은 제약 조건에 따라 UI 요소들의 크기와 위치가 자동으로 재계산됩니다.
- 코드 직접 작성 (viewWillTransition): Auto Layout만으로 부족한 커스텀 동작을 구현합니다. 예를 들어 유튜브 앱처럼, "가로 모드가 되면 동영상 외의 모든 UI를 숨기고, 플레이어를 전체 화면으로 만들어라" 같은 특별 주문은 개발자가 코드로 직접 구현해야 합니다.
이 방식은 재사용성보다는 해당 화면의 특정 경험을 극대화하는 데 목적이 있습니다.
iOS 13+ viewIsAppearing 활용
iOS 13부터 백포트된 viewIsAppearing 메서드는 더 정확한 trait collection 값을 제공합니다.
// iOS 13+
override func viewIsAppearing(_ animated: Bool) {
super.viewIsAppearing(animated)
// View가 hierarchy에 추가된 후 호출되어
// 정확한 trait collection 값을 보장
configureViewBasedOnTraits()
}
Ref
https://developer.apple.com/kr/videos/play/wwdc2017/812/
Size Classes and Core Components - WWDC17 - 비디오 - Apple Developer
Designing for multiple screen sizes can seem complicated, difficult, and time-consuming. Learn how size classes, dynamic type, and UIKit...
developer.apple.com
https://developer.apple.com/documentation/uikit/uiviewcontroller
UIViewController | Apple Developer Documentation
An object that manages a view hierarchy for your UIKit app.
developer.apple.com
https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/index.html
View Controller Programming Guide for iOS: The Role of View Controllers
View Controller Programming Guide for iOS
developer.apple.com
https://developer.apple.com/videos/play/wwdc2023/10057/ (iOS 17 Trait System)
Unleash the UIKit trait system - WWDC23 - Videos - Apple Developer
Discover powerful enhancements to the trait system in UIKit. Learn how you can define custom traits to add your own data to...
developer.apple.com
https://developer.apple.com/documentation/uikit/uitraitenvironment/1623516-traitcollectiondidchange
traitCollectionDidChange(_:) | Apple Developer Documentation
Reports changes in the iOS interface environment.
developer.apple.com
오늘도 화이팅입니다!
'iOS > UIKit' 카테고리의 다른 글
| [UIKit] The View Controller Hierarchy (1) | 2025.01.14 |
|---|---|
| [UIKit] Container View Controller (0) | 2025.01.12 |
| [UIKit] ViewController의 생명주기 (0) | 2025.01.11 |
| [UIKit] Preparing your UI to run in the foreground (0) | 2025.01.10 |
| [UIKit] 공식문서로 App의 LifeCycle 관리에 대해 알아 보자구요 (0) | 2025.01.10 |