커스텀 뷰 선언하기

x_0o0·2023년 11월 28일
0
post-thumbnail

커스텀 뷰 선언하기

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

Screenshot 2023-10-02 at 11 09 47 AM

리더보드는 투표수와 득표율을 보여줌 -> Grid 를 사용한다.

  • GridRowForEachGrid
  • 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
    )
}
  1. 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]
    }
}
  1. ViewThatFits
    투표 버튼의 크기는 텍스트의 너비에 따라 정해집니다. ViewThatFits 는 사용가능한 공간에 맞게 수평으로 정렬할지, 수직으로 정렬할지 SwiftUI에 의해 정하도록 합니다.
ViewThatFits {
    MyEqualWidthHStack {
        Buttons()
    }
    MyEqualWidthVStack {
        Buttons()
    }
}
  1. 캐싱
    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 를 사용해서 앱을 프로파일링 하면 캐싱하면 좋은 레이아웃이 뭔지 알아낼 수 있습니다.

참고

  1. https://developer.apple.com/documentation/swiftui/declaring-a-custom-view#:~:text=Views%20that%20take%20multiple%20input%20child%20views%2C%20like%20the%20stack%20in%20the%20example%20above%2C%20typically%20do%20so%20using%20a%20closure%20marked%20with%20the%20ViewBuilder%20attribute.
profile
Swift 를 공부합니다

0개의 댓글