View
는 계속 생성되고 버려지므로 수명이 짧아 변경 가능한 엔티티가 불필요하기 때문에 읽기 전용이며, 어차피 Model
을 그리는 것이므로 그 자체는 stateless
하기 때문에 읽기 전용인 게 적합!View
의 body
는 해당 View
가 사라졌더라도 계속 존재할 수 있다View
도 state 가 필요할 때가 있다!우리가 View
내부에서 매우 단기적/일시적인 변화를 추적해야할 때 state
가 필요하며 (e.g. 편집 모드에서 변화를 기록할 때) @State
를 변수 앞에 표기해 이를 추적할 수 있다
단, 일시적인 추적이 필요한 경우에만 사용하며 지속적인 추적이 필요한 경우 이는 Model
에서 담당한다
@State 변수
는 모두 private
! 해당 View
내부에서만 사용 가능
@State 변수
는 일종의 포인터처럼 작동하므로 View 구조체
가 아니라 View 구조체의 body
의 수명 동안 스크린에 나타난다.
body
가 살아있다면 계속 스크린에 나타날 수 있다property wrapper
로 인해 새로운 View
로 교체되는 경우, 이전의 View
는 사라지지만 body
는 잔존Container View
들이 자신 내부의 View
들에게 공간을 제공하면View
들이 원하는 사이즈를 선택View
부터 순차적으로Container View
들이 그에 맞춰 View
의 자리를 정해주고Container View
자신의 크기를 정한다(2번 과정부터 다시 반복)View
부터 순차적으로 내부 View
들에게 공간을 제시하면, 그에 맞춰서 자신의 사이즈를 선택!LazyHStack
, LazyVStack
, modifiers
를 제외하면 자기 내부에 유연한(자신에게 주어진 공간을 다 차지할 수 있는) View
가 존재하는 경우 그 자신도 유연해진다!LazyHStack
과 LazyVStack
은 주로 ScrollView
내부에서 사용되므로 무한히 유연하면 안되고modifier
는 보통 자기가 변환하는 View
의 사이즈를 그대로 리턴GeometryReader
라는 Container View
를 이용하면 내장된 geometryProxy
를 통해 컨테이너 뷰의 내부 크기에 접근할 수 있다. nameForProxy.size.width
, nameForProxy.size.height
를 이용! var body: some View {
GeometryReader { geometry in
VStack {
let width = geometry.size.width
let height = geometry.size.height
}
}
}
GeometryReader
는 항상 자신에게 주어진 공간을 전부 차지하기 때문에 만약에 내부에 LazyVStack
과 같이 비탄력적인 View
들만 있다면 Spacer
등을 통해 내부 또한 탄력적일 수 있도록 해야 한다 var body: some View {
GeometryReader { geometry in
VStack {
...
LazyVGrid(columns: [adaptiveGridItem(width: width)], spacing: 0) {
....
}
}
Spacer(minLength: 0) // 탄력적으로 흡수!
}
}
}
LazyVGrid
의 columns
인자의 사이즈 속성을 .adaptive
로 주는 경우, 해당 열의 너비 안에 들어간다면 한번에 여러 개의 아이템을 넣을 수 있다. 예를 들어 1열의 크기가 100에 .adaptive
속성을 갖고, 각각 크기가 10, 30, 50인 아이템이 있다면 모두 1열에 들어가게 된다. LazyVGrid
의 특성을 이용하면 뷰의 크기가 뷰의 개수, 화면 크기 등에 따라 변화하는 반응형 인터페이스를 구현할 수 있다. 과정은 다음과 같다. .adaptive
한 하나의 열만을 갖는 LazyVGrid
를 선언한다.
GeometryReader
를 이용해 현재 인터페이스의 너비와 높이를 구한다
private func widthThatBestFits(itemCount: Int, in size: CGSize, itemAspectRatio: CGFloat) -> CGFloat {
var columnCount = 1
var rowCount = itemCount
repeat {
let itemWidth = size.width / CGFloat(columnCount)
let itemHeight = itemWidth / itemAspectRatio
if CGFloat(rowCount) * itemHeight < size.height {
break
}
columnCount += 1
rowCount = Int(ceil(Double(itemCount) / Double(columnCount)))
} while columnCount < itemCount
if columnCount > itemCount {
columnCount = itemCount
}
return floor(size.width / CGFloat(columnCount))
}
}
LazyVGrid
의 열에 들어갈 GridItem
(나의 경우 개별 카드 한 장)의 너비로 지정한다! struct AspectVGrid<Item, ItemView>: View where ItemView: View, Item: Identifiable {
let items: [Item]
let aspectRatio: CGFloat
let content: (Item) -> ItemView
init(items: [Item], aspectRatio: CGFloat, @ViewBuilder content: @escaping (Item) -> ItemView) {
self.items = items
self.aspectRatio = aspectRatio
self.content = content
}
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
let width: CGFloat = max(widthThatBestFits(itemCount: items.count, in: geometry.size, itemAspectRatio: aspectRatio), 80)
LazyVGrid(columns: [adaptiveGridItem(width: width)], spacing: 0) {
ForEach(items) {
content($0)
.aspectRatio(aspectRatio, contentMode: .fit)
}
}
Spacer(minLength: 0)
}
}
}
}
}
var body: some View {
GeometryReader { geometry in
ZStack {
let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
if card.isFaceUp {
...
shape.strokeBorder(lineWidth: DrawingConstants.lineWidth)
Text(card.content).font(font(in: geometry.size))
}
...
}
}
}
private struct DrawingConstants {
static let cornerRadius: CGFloat = 20
static let lineWidth: CGFloat = 3
static let fontScale: CGFloat = 0.7
}
type alias
등을 이용해 코드를 깔끔하게 정리하는 시간이었다. functional programming
을 위해서는 어떤식으로 코드를 짜야하는 지 배웠고, 코드가 길면 이를 간단히 나타낼 수 있도록 분할해야함을 깨달았다.View
가 차지하는 공간이 결정되는 메커니즘을 공부할 수 있어서 유익했고, 이를 이해해야 GeometryReader
를 더 효과적으로 쓸 수 있다는 점을 느꼈다.ex. editing mode -> col
52:50 for more details