Apple Developer Documentations 의 Declaring a custom view 문서를 정독하며 정리한 내용입니다.
참고 https://developer.apple.com/documentation/swiftui/declaring-a-custom-view
struct MyView: View {
// 뷰 모습을 묘사
var body: some View {
VStack {
Text("안녕")
} // 이처럼 여러개의 자식 뷰를 갖는 경우 `@ViewBuilder` 속성이 있는 클로져를 사용하면 된다.(1)
}
}
https://developer.apple.com/documentation/swiftui/composing_custom_layouts_with_swiftui
리더보드는 투표수와 득표율을 보여줌 -> Grid
를 사용한다.
GridRow
∈ ForEach
∈ Grid
GridRow
는 Column cell 생성Grid
의 정렬은 모든 셀에 적용gridColumnAlignment(_:)
을 사용해서 정렬을 오버라이드 할 수 있음Grid(alignment: .leading) {
ForEach(model.pets) { pet in
GridRow {
Text(...)
ProgressView(...)
Text(...)
.gridColumnAlignment(.trailing)
}
Divider()
}
}
투표 버튼
각 버튼이 같은 너비를 가지려면 Layout
프로토콜을 준수하는 커스텀 레이아웃을 만들어야함. -> MyEqualWidthHStack
1. sizeThatFits(proposal:subviews:cache:)
: 컨테이너 사이즈와 주어진 하위뷰들 정보를 알려줌.
이 메소드는 하위뷰들간의 수평 공백과 각 방향별 가장 큰 사이즈를 결합하여 컨테이너 전체 사이즈를 찾음.
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Void
) -> CGSize {
guard !subviews.isEmpty else { return .zero }
let maxSize = maxSize(subviews: subviews) // 가장 큰 사이즈
let spacing = spacing(subviews: subviews) // 수평 공백
let totalSpacing = spacing.reduce(0) { $0 + $1 }
return CGSize(
width: maxSize.width * CGFloat(subviews.count) + totalSpacing, // 가장 큰 사이즈 기준으로 하위뷰를 잡고 모든 공백을 더함 -> 너비가 초과하진 않은가...?
height: maxSize.height
)
}
placeSubviews(in:proposal:subviews:cache:)
: 하위뷰들이 레이아웃 내 어디에서 떠야하는지 알려주는 용도func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Void
) {
guard !subviews.isEmpty else { return }
let maxSize = maxSize(subviews: subviews)
let spacing = spacing(subviews: subviews)
let placementProposal = ProposedViewSize(width: maxSize.width, height: maxSize.height)
var nextX = bounds.minX + maxSize.width / 2
for index in subviews.indices {
subviews[index].place(
at: CGPoint(x: nextX, y: bounds.midY),
anchor: .center,
proposal: placementProposal
)
nextX += maxSize.width + spacing[index]
}
}
ViewThatFits
ViewThatFits
는 사용가능한 공간에 맞게 수평으로 정렬할지, 수직으로 정렬할지 SwiftUI에 의해 정하도록 합니다.ViewThatFits {
MyEqualWidthHStack {
Buttons()
}
MyEqualWidthVStack {
Buttons()
}
}
Layout
프로토콜은 양방향 캐싱 파라미터를 가짐. 이 캐시는 특정 레이아웃 인스턴스의 모든 메소드 간 공유되는 옵셔널 저장소에 접근을 제공합니다.// storage 를 위한 타입 정의
struct CacheData {
let maxSize: CGSize
let spacing: [CGFloat]
let totalSpacing: CGFloat
}
그런 다음, makeCache(subviews:)
옵셔널 프로토콜 메소드를 사용해서 하위뷰들을 계산하고 위에서 정의한 타입의 값으로 리턴.
func makeCache(subviews: Subviews) -> CacheData {
let maxSize = maxSize(subviews: subviews)
let spacing = spacing(subviews: subviews)
let totalSpacing = spacing.reduce(0) { $0 + $1 }
return CacheData(
maxSize: maxSize,
spacing: spacing,
totalSpacing: totalSpacing
)
}
만약 하위뷰들에 변화가 생기면, SwiftUI 는 updateCache(_:subviews:)
메소드를 호출합니다. 이 메소드의 기본 구현부는 makeCache(subviews:)
를 호출하도록 되어있고, 이는 데이터를 재계산 하게 됩니다.
그런다음 sizeThatFits(proposal:subviews:cache:)
와 placeSubviews(in:proposal:subviews:cache:)
메소드에서 cache
파라미터를 사용해서 데이터를 가져옵니다.
// placeSubviews(in:proposal:subviews:cache:)
let maxSize = cache.maxSize
let spacing = cache.spacing
Note
대부분의 간단한 레이아웃은 캐싱 사용에서 큰 효율을 얻진 못합니다. Instruments 를 사용해서 앱을 프로파일링 하면 캐싱하면 좋은 레이아웃이 뭔지 알아낼 수 있습니다.