Lecture 5: Properties Layout @ViewBuilder

sun·2021년 10월 6일
2

# View 는 읽기 전용!

  • View 는 계속 생성되고 버려지므로 수명이 짧아 변경 가능한 엔티티가 불필요하기 때문에 읽기 전용이며, 어차피 Model 을 그리는 것이므로 그 자체는 stateless 하기 때문에 읽기 전용인 게 적합!
  • 하지만 Viewbody 는 해당 View 가 사라졌더라도 계속 존재할 수 있다

# 하지만 View 도 state 가 필요할 때가 있다!

  • 우리가 View 내부에서 매우 단기적/일시적인 변화를 추적해야할 때 state 가 필요하며 (e.g. 편집 모드에서 변화를 기록할 때) @State 를 변수 앞에 표기해 이를 추적할 수 있다

  • 단, 일시적인 추적이 필요한 경우에만 사용하며 지속적인 추적이 필요한 경우 이는 Model 에서 담당한다

  • @State 변수 는 모두 private ! 해당 View 내부에서만 사용 가능

  • @State 변수 는 일종의 포인터처럼 작동하므로 View 구조체 가 아니라 View 구조체의 body 의 수명 동안 스크린에 나타난다.

    • 즉, 해당 인스턴스가 사라지더라도 body 가 살아있다면 계속 스크린에 나타날 수 있다
    • 예컨대, property wrapper 로 인해 새로운 View 로 교체되는 경우, 이전의 View 는 사라지지만 body 는 잔존

# 레이아웃은 어떻게 결정되냐면요...

  1. Container View 들이 자신 내부의 View 들에게 공간을 제공하면
  1. View 들이 원하는 사이즈를 선택
    • 유연하지 않은 View부터 순차적으로
  1. 그러면 Container View 들이 그에 맞춰 View 의 자리를 정해주고
  1. 여기에 맞게 Container View 자신의 크기를 정한다(2번 과정부터 다시 반복)
  • 요약하면 가장 상위/바깥의 View 부터 순차적으로 내부 View 들에게 공간을 제시하면, 그에 맞춰서 자신의 사이즈를 선택!
  • LazyHStack, LazyVStack, modifiers 를 제외하면 자기 내부에 유연한(자신에게 주어진 공간을 다 차지할 수 있는) View 가 존재하는 경우 그 자신도 유연해진다!
    • LazyHStackLazyVStack 은 주로 ScrollView 내부에서 사용되므로 무한히 유연하면 안되고
    • modifier 는 보통 자기가 변환하는 View 의 사이즈를 그대로 리턴

# 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)  // 탄력적으로 흡수!
            }
        }
    }

# GeometryReader + LazyVGrid = 반응형 인터페이스

  • LazyVGridcolumns 인자의 사이즈 속성을 .adaptive 로 주는 경우, 해당 열의 너비 안에 들어간다면 한번에 여러 개의 아이템을 넣을 수 있다. 예를 들어 1열의 크기가 100에 .adaptive 속성을 갖고, 각각 크기가 10, 30, 50인 아이템이 있다면 모두 1열에 들어가게 된다.
  • 이러한 LazyVGrid 의 특성을 이용하면 뷰의 크기가 뷰의 개수, 화면 크기 등에 따라 변화하는 반응형 인터페이스를 구현할 수 있다. 과정은 다음과 같다.
  1. .adaptive 한 하나의 열만을 갖는 LazyVGrid 를 선언한다.

    • 열이 하나만 존재하면 열의 너비가 인터페이스의 너비와 같아진다. 따라서 각 행마다 인터페이스 너비에 맞춰 아이템(현재의 경우 각 카드)이 최대로 삽입된다.
    • 이를 이용해서 모든 카드가 한 화면에 들어오도록 인터페이스 너비에 따라 카드의 크기를 지정해주면 반응형 인터페이스를 만들 수 있다
  1. GeometryReader 를 이용해 현재 인터페이스의 너비와 높이를 구한다
  1. 현재 인터페이스의 가로 길이와 내부에 들어갈 뷰의 개수, 내부 뷰의 가로/세로 비율을 이용해 아이템의 개수와 가로/세로 비율에 따라 가장 적합한 길이를 도출한다.
    • 각 행에 오직 하나의 원소만 들어있는 nx1 형태의 행렬에서 시작해서 각 경우에 모든 요소를 하나의 화면에 넣을 수 있는 지 확인하고 안되는 경우 열을 하나씩 늘리는 방식으로 최적 너비를 도출 가능!

    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))
    }
}
  1. 위에서 구한 최적 뷰 너비를 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)
                }
            }
        }
    }
}

# Magic Number 는 구조체로 대체하기

  • 상수의 경우 어떤 의미인지 다른 사람들은 즉각적으로 알기 어려우므로 해당 상수의 역할을 구조체에 정의해서 대체하는 게 더 좋다
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
    }

☀️ TIL

  • 오늘은 새로운 기능을 추가하기 보다는 type alias 등을 이용해 코드를 깔끔하게 정리하는 시간이었다. functional programming 을 위해서는 어떤식으로 코드를 짜야하는 지 배웠고, 코드가 길면 이를 간단히 나타낼 수 있도록 분할해야함을 깨달았다.
  • 레이아웃에서 각 View 가 차지하는 공간이 결정되는 메커니즘을 공부할 수 있어서 유익했고, 이를 이해해야 GeometryReader 를 더 효과적으로 쓸 수 있다는 점을 느꼈다.



# Views are read-only

  • bc views are created and tossed out all the time
  • only their bodies stick around very long : the view that may that body was long ago thrown away
  • so they don't really live long enough to need to be mutable entities

# when views need state

  • for temp things going on that we have to keep track of
  • only for temporary storage
  • @State vars are all private: only for inside view
    • life time of body of the view is on screen not the view struct!

ex. editing mode -> col

# how is th space on-screen apportioned to the View?

  1. Container Views "offer" space to the Views inside them
  2. Views then choose what size they want to be
  3. Container Views then position the Views inside of them
  4. (and base on that, Container Views choose their own size as per #2 above)
    -> kinda like dfs?

52:50 for more details

HStack and VStack

  • if any of the Views in the stack are "very flexible", then the stack will also be "very Flexible"

LazyHStack and LazyVStack

  • they are not flexible even if they have flexibel views inside
  • this is because they are used inside a ScrollView

ScrollView

  • takes all the space offered to it

ZStack

  • sizes itself to fit its chldren
  • if even one of its children is fully flexible size, then the ZStack will be too

modifiers

  • View modifier functions themselves return a View
  • most them just pass the size offered to them along, but it's possilble for a modifier to be involved in the layout process itself
profile
☀️

0개의 댓글

관련 채용 정보