Ver3.5 ๐ Auto Layout๊ณผ Size Classes๋ฅผ ํ์ฉํ๋ฐ์ํ UI ์ค๊ณ ์ ๋ต

๐ Auto Layout๊ณผ Size Classes๋ฅผ ํ์ฉํ
๋ฐ์ํ UI ์ค๊ณ ์ ๋ต
iPhone๋ถํฐ iPad๊น์ง, ๋ชจ๋ ํ๋ฉด์ ์ ๋ณตํ๋ ๋ ์ด์์ ๋ง์คํฐ ๊ฐ์ด๋ ๐
iPad์์ ์ด์ด๋ณด๋๊น ๋ ์ด์์์ด ์์ ๋ฐ์ด๋ฌ์ด ๐ญ
์ง๊ธ ๋ฐ๋ก ์๋ ค์ค๊ฒ ใฑใฑ
iOS ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์ด๋ฐ ์ํฉ ์ง์ง ๋ง์ด ๊ฒช์ฃ ใ
ใ
. ๋์์ธ ์ด์ฌํ ํ๋๋ฐ ๊ธฐ๊ธฐ ๋ฐ๊พธ๋ฉด UI๊ฐ ์์ ์๋ง์ด ๋๋ ๊ทธ ์ฐธ๋ดํจ... ๐
์ค๋์ Auto Layout๊ณผ Size Classes๋ฅผ ์ ๋๋ก ํํค์ณ์, ์ด๋ค ๊ธฐ๊ธฐ์์๋ ์๋ฒฝํ๊ฒ ๋์ํ๋ ๋ฐ์ํ UI๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์์๋ณผ๊ฒ์!
Auto Layout์ iOS 6๋ถํฐ ๋์ ๋ ์ ์ฝ ๊ธฐ๋ฐ(Constraint-based) ๋ ์ด์์ ์์คํ ์ด์์. ์ฝ๊ฒ ๋งํ๋ฉด, "์ด ๋ทฐ๋ ๋ถ๋ชจ ๋ทฐ์ ์ผ์ชฝ์์ 16pt ๋จ์ด์ ธ ์์ด์ผ ํด" ๊ฐ์ ๊ท์น(์ ์ฝ)์ ์ ์ํ๋ฉด, ์์คํ ์ด ์์์ ๋ชจ๋ ํ๋ฉด ํฌ๊ธฐ์ ๋ง๊ฒ ๊ณ์ฐํด์ฃผ๋ ๊ฑฐ์ฃ ใ ใ
top bottom
centerX centerY
aspectRatio
equalWidth equalHeight
์ ์ฝ์๋ 1~1000 ์ฌ์ด์ ์ฐ์ ์์๊ฐ ์์ด์. ์ถฉ๋์ด ์๊ธฐ๋ฉด ๋์ ์ฐ์ ์์๊ฐ ์ด๊ฒจ์!
| ์ฐ์ ์์ | ์ด๋ฆ | ์ฉ๋ |
|---|---|---|
| 1000 | Required | ์ ๋ ์ด๊ฒจ์ ์ ๋๋ ํ์ ์ ์ฝ |
| 750 | High | ๊ธฐ๋ณธ Content Hugging Priority |
| 500 | Medium | ์ผ๋ฐ์ ์ธ ์ ํ์ ์ ์ฝ |
| 250 | Low | ๊ธฐ๋ณธ Content Compression Resistance |
Interface Builder(์คํ ๋ฆฌ๋ณด๋)๋ก๋ ํ ์ ์์ง๋ง, ์์ฆ ํธ๋ ๋๋ ์ฝ๋ ๊ธฐ๋ฐ ๋ ์ด์์์ด์ฃ ! ํ์ ํ ๋ ์คํ ๋ฆฌ๋ณด๋ ์ถฉ๋ ๋๋ ๊ฑฐ ์ง์ง ์คํธ๋ ์ค์์์ ใ ใ ใ
// ๊ธฐ๋ณธ NSLayoutConstraint ์ฌ์ฉ๋ฒ
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false // โญ ํ์!
view.addSubview(button)
NSLayoutConstraint.activate([
button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
button.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
button.heightAnchor.constraint(equalToConstant: 50)
])
// Anchor ๋ฐฉ์ - ํ์
์์ ํ๊ณ ๊ฐ๋
์ฑ ์ข์
let titleLabel = UILabel()
titleLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(titleLabel)
NSLayoutConstraint.activate([
// ๋น์จ ๊ธฐ๋ฐ ๋๋น ์ค์
titleLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24),
// ์ต์/์ต๋ ๋์ด ์ค์
titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 44)
])
// UIView Extension์ผ๋ก ํธ์ ๋ฉ์๋ ๋ง๋ค๊ธฐ
extension UIView {
func pinToEdges(of view: UIView, padding: UIEdgeInsets = .zero) {
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
topAnchor.constraint(equalTo: view.topAnchor, constant: padding.top),
leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding.left),
trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding.right),
bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -padding.bottom)
])
}
func center(in view: UIView) {
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
centerXAnchor.constraint(equalTo: view.centerXAnchor),
centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
// ์ฌ์ฉ ์์
contentView.pinToEdges(of: containerView, padding: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16))
์ด ๋ ๊ฐ๋ ์ Auto Layout์์ ์ง์ง ํท๊ฐ๋ฆฌ๋ ๋ถ๋ถ์ธ๋ฐ, ์ ๋๋ก ์ดํดํ๋ฉด ๋ ์ด์์ ๋ฌธ์ ์ ์ ๋ฐ์ ํด๊ฒฐ๋ผ์ ใ ใ
๊ฐ์ด ๋์์๋ก ์์ ์ ๊ณ ์ ํฌ๊ธฐ(intrinsic size)๋ฅผ ์ ์งํ๋ ค ํด์.
๊ธฐ๋ณธ๊ฐ: 250 (์ํ)
๊ธฐ๋ณธ๊ฐ: 250 (์์ง)
๊ฐ์ด ๋์์๋ก ์๋ฆฌ๊ฑฐ๋ ์์ถ๋๋ ๊ฑธ ๊ฑฐ๋ถํด์.
๊ธฐ๋ณธ๊ฐ: 750 (์ํ)
๊ธฐ๋ณธ๊ฐ: 750 (์์ง)
// ์ค์ ์์: ๋ ๋ ์ด๋ธ์ด ๋๋ํ ์์ ๋
// ์ผ์ชฝ ๋ ์ด๋ธ์ ๊ณ ์ , ์ค๋ฅธ์ชฝ ๋ ์ด๋ธ์ ๋์ด๋์ผ ํ ๋
let leftLabel = UILabel()
let rightLabel = UILabel()
// ์ผ์ชฝ ๋ ์ด๋ธ: ์๊ธฐ ํฌ๊ธฐ ์ ์ง (Hugging ๋๊ฒ)
leftLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
leftLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
// ์ค๋ฅธ์ชฝ ๋ ์ด๋ธ: ๋์ด๋๋ ๋จ (Hugging ๋ฎ๊ฒ)
rightLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
// ์ด๋ ๊ฒ ํ๋ฉด leftLabel์ ํ
์คํธ ํฌ๊ธฐ๋งํผ๋ง,
// rightLabel์ ๋๋จธ์ง ๊ณต๊ฐ์ ์ฑ์์!
Size Classes๋ iOS 8๋ถํฐ ๋์ ๋ ๊ฐ๋ ์ผ๋ก, ํ๋ฉด์ ํฌ๊ธฐ๋ฅผ Compact์ Regular ๋ ๊ฐ์ง๋ก ๋ถ๋ฅํด์. ์ ํํ ํฝ์ ํฌ๊ธฐ ๋์ ์๋์ ์ธ ํฌ๊ธฐ ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฑฐ์ฃ !
// UITraitCollection์ผ๋ก ํ์ฌ Size Class ํ์ธ
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// ์ํ Size Class ๋ณ๊ฒฝ ๊ฐ์ง
if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
updateLayoutForSizeClass()
}
}
func updateLayoutForSizeClass() {
switch traitCollection.horizontalSizeClass {
case .compact:
// iPhone ์ธ๋ก ๋ชจ๋ ๋ ์ด์์
setupCompactLayout()
case .regular:
// iPad ๋๋ iPhone Plus ๊ฐ๋ก ๋ ์ด์์
setupRegularLayout()
case .unspecified:
break
@unknown default:
break
}
}
// ์ค์ ์์: ์ปฌ๋ผ ์ ๋์ ๋ณ๊ฒฝ
func setupCompactLayout() {
// 1์ปฌ๋ผ ๋ ์ด์์
collectionViewFlowLayout.minimumInteritemSpacing = 0
collectionViewFlowLayout.itemSize = CGSize(
width: collectionView.bounds.width - 32,
height: 120
)
}
func setupRegularLayout() {
// 2์ปฌ๋ผ ๋ ์ด์์
let itemWidth = (collectionView.bounds.width - 48) / 2
collectionViewFlowLayout.itemSize = CGSize(width: itemWidth, height: 150)
}
// iOS 17+์์๋ registerForTraitChanges ์ฌ์ฉ ๊ถ์ฅ
// (traitCollectionDidChange๋ deprecated ์์ )
override func viewDidLoad() {
super.viewDidLoad()
// iOS 17+ ์๋ก์ด ๋ฐฉ์
if #available(iOS 17.0, *) {
registerForTraitChanges([UITraitHorizontalSizeClass.self]) {
(self: Self, previousTraitCollection: UITraitCollection) in
self.updateLayoutForSizeClass()
}
}
}
// ํน์ Size Class์์๋ง ์ ์ฉํ ์ ์ฝ ๊ด๋ฆฌ
var compactConstraints: [NSLayoutConstraint] = []
var regularConstraints: [NSLayoutConstraint] = []
func setupConstraints() {
// ๊ณตํต ์ ์ฝ
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16)
])
// Compact ์ ์ฉ ์ ์ฝ (iPhone ์ธ๋ก)
compactConstraints = [
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
subtitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16)
]
// Regular ์ ์ฉ ์ ์ฝ (iPad)
regularConstraints = [
titleLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.topAnchor),
subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 20)
]
activateConstraintsForCurrentSizeClass()
}
func activateConstraintsForCurrentSizeClass() {
if traitCollection.horizontalSizeClass == .compact {
NSLayoutConstraint.deactivate(regularConstraints)
NSLayoutConstraint.activate(compactConstraints)
} else {
NSLayoutConstraint.deactivate(compactConstraints)
NSLayoutConstraint.activate(regularConstraints)
}
}
UIStackView๋ Auto Layout์ ํจ์ฌ ์ฝ๊ฒ ๋ง๋ค์ด์ฃผ๋ ์ปจํ ์ด๋ ๋ทฐ์์. ๋ด๋ถ ๋ทฐ๋ค์ ์๋์ผ๋ก ๋ฐฐ์นํด์ฃผ๊ธฐ ๋๋ฌธ์ ์ ์ฝ์ ์ผ์ผ์ด ์ค์ ํ ํ์๊ฐ ์์ด์ ใ ใ ์ง์ง ํธํจ!
distribution ํฌ๊ธฐ ๋ถ๋ฐฐ ๋ฐฉ์
alignment ์ ๋ ฌ ๋ฐฉ์
spacing ๊ฐ๊ฒฉ
isLayoutMarginsRelativeArrangement
fillEqually ๊ท ๋ฑ ๋ถ๋ฐฐ
fillProportionally ๋น์จ ๋ถ๋ฐฐ
equalSpacing ๋์ผ ๊ฐ๊ฒฉ
equalCentering ์ค์ฌ ๋์ผ ๊ฐ๊ฒฉ
// ๋ฐ์ํ ์นด๋ ๋ ์ด์์ ์์
class AdaptiveCardViewController: UIViewController {
private lazy var mainStackView: UIStackView = {
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.spacing = 16
stack.isLayoutMarginsRelativeArrangement = true
stack.layoutMargins = UIEdgeInsets(top: 20, left: 16, bottom: 20, right: 16)
return stack
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
view.addSubview(mainStackView)
NSLayoutConstraint.activate([
mainStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
mainStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
mainStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
// ์นด๋ ๋ทฐ๋ค ์ถ๊ฐ
["์นด๋ 1", "์นด๋ 2", "์นด๋ 3"].forEach { title in
mainStackView.addArrangedSubview(makeCard(title: title))
}
updateStackViewAxis()
}
private func updateStackViewAxis() {
// Size Class์ ๋ฐ๋ผ ๋ฐฉํฅ ๋ณ๊ฒฝ!
if traitCollection.horizontalSizeClass == .regular {
mainStackView.axis = .horizontal
mainStackView.distribution = .fillEqually
} else {
mainStackView.axis = .vertical
mainStackView.distribution = .fill
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
UIView.animate(withDuration: 0.3) {
self.updateStackViewAxis()
self.view.layoutIfNeeded() // ์ ๋๋ฉ์ด์
๊ณผ ํจ๊ป ๋ ์ด์์ ์
๋ฐ์ดํธ!
}
}
}
layoutIfNeeded(): "์ง๊ธ ๋น์ฅ ์ ๋ฐ์ดํธํด์ค" (๋๊ธฐ์ )
์ ๋๋ฉ์ด์ ๊ณผ ํจ๊ป ๋ ์ด์์ ๋ณ๊ฒฝํ ๋๋ animate ๋ธ๋ก ์์์ layoutIfNeeded()๋ฅผ ํธ์ถํด์ผ ํด์!
์์ฆ์ SwiftUI๋ ๋ง์ด ์ฐ์์์! SwiftUI์์๋ Auto Layout ๊ฐ๋ ์ด ๋ค๋ฅด๊ฒ ํํ๋๋๋ฐ, ์ด๊ฒ๋ ์์๋๋ฉด ์ข์์ ใ ใ
// SwiftUI์์ Size Class ํ์ฉ
struct AdaptiveContentView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
var body: some View {
Group {
if horizontalSizeClass == .compact {
// iPhone ์ธ๋ก: ์์ง ๋ ์ด์์
VStack(spacing: 16) {
CardView(title: "์นด๋ 1")
CardView(title: "์นด๋ 2")
CardView(title: "์นด๋ 3")
}
.padding()
} else {
// iPad / iPhone Plus ๊ฐ๋ก: ์ํ ๋ ์ด์์
HStack(spacing: 20) {
CardView(title: "์นด๋ 1")
CardView(title: "์นด๋ 2")
CardView(title: "์นด๋ 3")
}
.padding()
}
}
}
}
// ViewThatFits - iOS 16+์์ ์๋ ์ ์!
struct SmartButtonView: View {
var body: some View {
ViewThatFits {
// ๊ณต๊ฐ์ด ์ถฉ๋ถํ๋ฉด ๊ฐ๋ก ๋ฐฐ์น
HStack {
Button("์ทจ์") { }
Button("ํ์ธ") { }
}
// ๊ณต๊ฐ์ด ๋ถ์กฑํ๋ฉด ์ธ๋ก ๋ฐฐ์น
VStack {
Button("ํ์ธ") { }
Button("์ทจ์") { }
}
}
}
}
// GeometryReader๋ก ์ ๋ฐํ ํฌ๊ธฐ ์ ์ด
struct ResponsiveGridView: View {
var body: some View {
GeometryReader { geometry in
let columns = geometry.size.width > 600 ? 3 : 2
let itemWidth = (geometry.size.width - CGFloat(columns + 1) * 16) / CGFloat(columns)
LazyVGrid(columns: Array(repeating: GridItem(.fixed(itemWidth)), count: columns), spacing: 16) {
ForEach(0..<12) { index in
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(0.2))
.frame(height: itemWidth * 0.75)
.overlay(Text("์์ดํ
\(index + 1)"))
}
}
.padding(16)
}
}
}
iOS 13+์์ ๋์ ๋ Compositional Layout์ Size Class์ ๋ฐ๋ฅธ ๋ฐ์ํ ๋ ์ด์์ ๊ตฌํ์ ์ต๊ฐ์ด์์!
// Size Class์ ๋ฐ๋ผ ๋์ ์ผ๋ก ๋ณํ๋ CollectionView ๋ ์ด์์
func createAdaptiveLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
// layoutEnvironment.container.effectiveContentSizeCategory ํ์ฉ!
let isRegular = layoutEnvironment.traitCollection.horizontalSizeClass == .regular
// ์ปฌ๋ผ ์ ๊ฒฐ์
let columnCount = isRegular ? 3 : 2
// ์์ดํ
ํฌ๊ธฐ ์ค์
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0 / CGFloat(columnCount)),
heightDimension: .fractionalHeight(1.0)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
// ๊ทธ๋ฃน ์ค์
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(isRegular ? 200 : 150)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitems: [item]
)
// ์น์
์ค์
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)
// ํค๋ ์ถ๊ฐ (Regular์์๋ง)
if isRegular {
let headerSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(60)
)
let header = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top
)
section.boundarySupplementaryItems = [header]
}
return section
}
return layout
}
// iPad์์ ์ฌ์ด๋๋ฐ, iPhone์์ ํญ๋ฐ๋ก ์๋ ์ ํ
class MainSplitViewController: UISplitViewController {
init() {
super.init(style: .doubleColumn)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
// ๊ธฐ๋ณธ ํ์ ๋ชจ๋ ์ค์
preferredDisplayMode = .oneBesideSecondary
// iPhone์์ ์๋์ผ๋ก ํญ๋ฐ ์คํ์ผ๋ก ์ ํ
preferredSplitBehavior = .tile
// ์ต์/์ต๋ ๋๋น ์ค์
minimumPrimaryColumnWidth = 280
maximumPrimaryColumnWidth = 320
preferredPrimaryColumnWidthFraction = 0.35
// ๋ทฐ์ปจํธ๋กค๋ฌ ์ค์
setViewController(SidebarViewController(), for: .primary)
setViewController(ContentViewController(), for: .secondary)
setViewController(CompactTabBarController(), for: .compact)
}
}
// Safe Area๋ฅผ ๊ณ ๋ คํ ์๋ฒฝํ ๋ ์ด์์
class SafeAreaAwareViewController: UIViewController {
private let bottomButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
setupBottomButton()
}
private func setupBottomButton() {
bottomButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bottomButton)
NSLayoutConstraint.activate([
bottomButton.leadingAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.leadingAnchor,
constant: 16
),
bottomButton.trailingAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.trailingAnchor,
constant: -16
),
// Safe Area ํ๋จ ๊ธฐ์ค์ผ๋ก ๋ฐฐ์น (ํ ์ธ๋์ผ์ดํฐ ์)
bottomButton.bottomAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: -16
),
bottomButton.heightAnchor.constraint(equalToConstant: 52)
])
}
// Safe Area ๋ณ๊ฒฝ ๊ฐ์ง (iPad ๋ฉํฐํ์คํน ๋ฑ)
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
// Safe Area๊ฐ ๋ฐ๋ ๋ ์ถ๊ฐ ์ฒ๋ฆฌ
print("Safe Area ๋ณ๊ฒฝ: \(view.safeAreaInsets)")
}
}
Auto Layout ์๋ฌ ๋๋ฒ๊น ์ ์ง์ง ๊ณ ํต์ค๋ฝ์ฃ ใ ใ ใ ๊ทธ๋๋ ์ด ๋ฐฉ๋ฒ๋ค ์๋ฉด ํจ์ฌ ํธํด์!
UIViewAlertForUnsatisfiableConstraints ์๋ฌ๊ฐ ๋จ๋ฉด ์ ์ฝ ์ถฉ๋์ด์์. ์๋ฌ ๋ฉ์์ง์์ ๋ทฐ ์ฃผ์๋ฅผ ์ฐพ์ po 0x7f8a1234๋ก ์ด๋ค ๋ทฐ์ธ์ง ํ์ธํ์ธ์!
๋ทฐ์ accessibilityIdentifier = "myButton" ์ค์ ํ๋ฉด ์๋ฌ ๋ฉ์์ง์์ ์ด๋ฆ์ผ๋ก ์๋ณ ๊ฐ๋ฅํด์!
Debug โ View Debugging โ Capture View Hierarchy๋ก 3D ๋ทฐ ๊ณ์ธต ํ์ธ! ๊ฒน์น๊ฑฐ๋ ์๋ฆฐ ๋ทฐ๋ฅผ ์๊ฐ์ ์ผ๋ก ์ฐพ์ ์ ์์ด์.
view.constraints๋ก ํ์ฌ ํ์ฑํ๋ ์ ์ฝ ๋ชฉ๋ก ํ์ธ, UIView._autolayoutTrace()๋ก ์ ์ฒด ๊ณ์ธต ํธ๋ ์ด์ค ๊ฐ๋ฅ!
// ๋๋ฒ๊น
์ฉ ์ ํธ๋ฆฌํฐ Extension
extension UIView {
// ๋ทฐ ํ
๋๋ฆฌ ์์์ผ๋ก ๋ ์ด์์ ์๊ฐํ
func debugLayout(color: UIColor = .red) {
#if DEBUG
layer.borderColor = color.cgColor
layer.borderWidth = 1
subviews.forEach { $0.debugLayout(color: color.withAlphaComponent(0.7)) }
#endif
}
// ํ์ฌ ์ ์ฝ ์ถ๋ ฅ
func printConstraints() {
#if DEBUG
print("=== \(type(of: self)) ์ ์ฝ ๋ชฉ๋ก ===")
constraints.forEach { print($0) }
#endif
}
}
// ์ฌ์ฉ๋ฒ
override func viewDidLoad() {
super.viewDidLoad()
// DEBUG ๋น๋์์๋ง ๋์
view.debugLayout()
}
2๏ธโฃ ์ ์ฝ์ activate ์ ํ๊ณ ๊ทธ๋ฅ ์์ฑ๋ง ํ๊ธฐ
3๏ธโฃ safeAreaLayoutGuide ๋์ view์ ์ง์ ์ ์ฝ ๊ฑธ์ด์ ๋ ธ์น์ ๊ฐ๋ฆฌ๊ธฐ
Auto Layout์ด ํธ๋ฆฌํ๊ธด ํ๋ฐ, ์๋ชป ์ฐ๋ฉด ์ฑ๋ฅ ์ด์๊ฐ ์๊ธธ ์ ์์ด์. ํนํ ์คํฌ๋กค๋ทฐ ์์์ ๋ณต์กํ ์ ์ฝ์ด ๋ง์ผ๋ฉด ํ๋ ์ ๋๋์ด ๋ ์ ์๊ฑฐ๋ ์!
- โ ์ ์ฝ ์ ์ต์ํ
- โ UIStackView ์ ๊ทน ํ์ฉ
- โ ๋ถํ์ํ ๋ทฐ ๊ณ์ธต ์ค์ด๊ธฐ
- โ ์ ์ฌ์ฌ์ฉ ์ ์ ์ฝ ์ฌ์์ฑ ๊ธ์ง
- โ ๋ณต์กํ ์ ์ ์๋ ๋ ์ด์์ ๊ณ ๋ ค
- โ ๋งค ํ๋ ์๋ง๋ค ์ ์ฝ ๋ณ๊ฒฝ
- โ ๋ถํ์ํ ์ ์ฝ activate/deactivate
- โ ์ค์ฒฉ๋ UIStackView ๊ณผ๋ค ์ฌ์ฉ
- โ ์ ๋ง๋ค ์ ์ ์ฝ ์์ฑ
- โ ๋ณต์กํ ์ ์ฝ ๊ณ์ฐ์ ๋ฉ์ธ ์ค๋ ๋์์
// ์ฑ๋ฅ ์ต์ ํ: ์
์ฌ์ฌ์ฉ ์ ์ ์ฝ ๊ด๋ฆฌ
class OptimizedTableViewCell: UITableViewCell {
private let titleLabel = UILabel()
private var dynamicHeightConstraint: NSLayoutConstraint?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupOnce() // ์ ์ฝ์ ํ ๋ฒ๋ง ์ค์ !
}
required init?(coder: NSCoder) {
fatalError()
}
private func setupOnce() {
titleLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(titleLabel)
// ๊ณ ์ ์ ์ฝ์ ํ ๋ฒ๋ง activate
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12)
])
// ๋์ ์ผ๋ก ๋ณํ๋ ์ ์ฝ์ ์ฐธ์กฐ ๋ณด๊ด
dynamicHeightConstraint = titleLabel.heightAnchor.constraint(equalToConstant: 44)
dynamicHeightConstraint?.isActive = true
}
func configure(with height: CGFloat) {
// ์ ์ฝ ์ฌ์์ฑ ์์ด constant๋ง ๋ณ๊ฒฝ! (ํจ์ฌ ๋น ๋ฆ)
dynamicHeightConstraint?.constant = height
}
}
Auto Layout๊ณผ Size Classes๋ฅผ ์ ๋๋ก ํ์ฉํ๋ฉด iPhone SE๋ถํฐ iPad Pro๊น์ง ํ๋์ ์ฝ๋๋ฒ ์ด์ค๋ก ์๋ฒฝํ ๋ฐ์ํ UI๋ฅผ ๋ง๋ค ์ ์์ด์!
โข ์ ์ฝ ๊ธฐ๋ฐ ์ํ์ ๋ ์ด์์
โข Priority๋ก ์ถฉ๋ ํด๊ฒฐ
โข Anchor API๋ก ๊ฐ๋ ์ฑ ํฅ์
โข translatesAutoresizingMask = false ํ์
โข Compact vs Regular ๋ ๊ฐ์ง
โข traitCollectionDidChange ๊ฐ์ง
โข ์ ์ฝ ๊ทธ๋ฃน activate/deactivate
โข iOS 17+ registerForTraitChanges
UIStackView axis ๋์ ๋ณ๊ฒฝ โ Compositional Layout โ UISplitViewController โ Safe Area ๋์ โ ์ฑ๋ฅ ์ต์ ํ(์ ์ฝ ์ฌ์ฌ์ฉ)
Auto Layout๊ณผ Size Classes๋ ์ฒ์์ ์ง์ง ์ด๋ ต๊ฒ ๋๊ปด์ง๋๋ฐ, ํต์ฌ ๊ฐ๋ ๋ง ์ก์ผ๋ฉด ์๊ฐ๋ณด๋ค ์ฒด๊ณ์ ์ด์์ ใ ใ . ์ ์ฝ์ด ์ํ ๋ฐฉ์ ์์ด๋ผ๋ ๊ฑธ ์ดํดํ๊ณ , Size Class๋ก ๊ธฐ๊ธฐ ํ๊ฒฝ์ ๋ถ๋ฅํ๋ ๊ฐ๋ ๋ง ์ก์ผ๋ฉด ๋๋จธ์ง๋ ์์ฉ์ด๊ฑฐ๋ ์!
iOS ๊ฐ๋ฐ ์ค๋ ฅ์ ํค์ฐ๊ณ ์ถ๋ค๋ฉด ์ฌ๋ฅ๋ท์์ Swift/iOS ๊ฐ๋ฐ ๊ด๋ จ ๋ฉํ ๋ง์ด๋ ์ฝ๋ ๋ฆฌ๋ทฐ ์๋น์ค๋ฅผ ์ฐพ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์์. ์ค์ ํ๋ก์ ํธ ๊ฒฝํ์ด ์๋ ๊ฐ๋ฐ์์๊ฒ ์ง์ ํผ๋๋ฐฑ์ ๋ฐ์ผ๋ฉด ์ฑ์ฅ ์๋๊ฐ ํจ์ฌ ๋นจ๋ผ์ง๊ฑฐ๋ ์! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.