상단에 이미지를 고정 설정
pinnedViews

스크롤 시에도 화면 상단이나 하단에 고정시킬 수 있도록 도와주는 구조체 타입형태

pinnedViews는 SwiftUI에서 사용되는
LazyVStack / LazyHStack에서 특정 Header / Footer 스크롤 상단이나 하단에 고정시켜주는 기능
pinnedViews 역할
섹션 헤더가 스크롤에 따라 사라지지 않고 리스트 상단에 고정되도록하는데 사용이 됩니다
설정 앱이나 연락처 앱처럼 카테고리별 섹션이 고정되는 효과를 줄 수 있습니다

LazyVStack(pinnedViews: [.sectionHeaders]) {
Section {
// 섹션의 콘텐츠
} header: {
// 고정할 헤더 뷰
}
}
우선 pinnedViews에 대해서
코드로 보는게 더 정확하기 때문에 예시를 하나 알아봅시다

import SwiftUI
struct ColorData: Identifiable {
let id = UUID()
let name: String
let color: Color
let variations: [ShadeData]
struct ShadeData: Identifiable {
let id = UUID()
var brightness: Double
}
init(color: Color, name: String) {
self.name = name
self.color = color
//0.0-0.5 0.1씩 증가하는 밝기 생성
self.variations = stride(from: 0.0, to: 0.5, by: 0.1)
.map { ShadeData(brightness: $0) }
}
}
import SwiftUI
struct ColorSelectionView: View {
let sections = [
ColorData(color: .red, name: "Reds"),
ColorData(color: .green, name: "Greens"),
ColorData(color: .blue, name: "Blues")
]
var body: some View {
ScrollView {
//LazyVStack(spacing: 1) {
LazyVStack(spacing: 1, pinnedViews: [.sectionHeaders]) {
//각 섹션 반복
ForEach(sections) { section in
//section header와 content
Section(header: SectionHeaderView(colorData: section)) {
ForEach(section.variations) { variation in
section.color
.brightness(variation.brightness)
.frame(height: 20)
}
}
}
}
}
}
}
struct SectionHeaderView: View {
var colorData: ColorData
var body: some View {
HStack {
Text(colorData.name)
.font(.headline)
.foregroundColor(colorData.color)
Spacer()
}
.padding()
.background(.black)
}
}
#Preview {
ColorSelectionView()
}
근데 여기서 pinnedViews는 LazyVStack, LazyHStack에서만 사용이 가능합니다
Lazy Container 특성은 콘텐츠를 메모리를 효율적으로 렌더링하기 위해서
스크롤이 필요할 때만 뷰를 로드하는 방식으로 작동하기 때문에
리스트가 길어질 수록 메모리 사용량과 렌더링 성능을 개선할 수 있고
header, foooter view는 Lazy특성을 사용해서 구현이 됩니다
섹션별로 구분되어있는 뷰 계층 구조를 가지고 있기 때문에
header, footer로 고정하는게 가능하고
반면에 VStack, HStack은 모든 하위뷰에 한 번에 렌더링해서
Section과 같은 구조적 구분이 없기 때문에 스크롤 시 헤더를 고정하는 것과 같은 동작을
구현하기엔 어려움이 있습니다
스크롤 성능을 최적화하기 위해서 PinnedViews는 스크롤할 때 헤더나 푸터를 고정시키는 동작을 하는데
스크롤 이벤트를 지속적으로 감지하고 뷰의 위치를 동적으로 변경해야하는데 이 과정에서
Lazy Container는 스크롤 위치와 관련된 정볼르 활용하여 효율적으로 뷰를 업데이트할 수 있지만
VStack, HStack은 모든 하위 뷰를 한 번에 렌더링 하기 때문에 성능상 불리합니다
그 다음으로 알아야 하는 개념은
GeometryReader

뷰가 자신의 크기와 좌표 공간에 따라 콘텐츠를 정의할 수 있도록 해주는 컨테이너 뷰라고 하고 있습니다
GeometryReader를 사용하면 뷰의 부모 컨테이너의 크기와 위치 정보를 얻을 수 있고 자식 뷰의 레이아웃을 동적으로 조절이 가능합니다



import SwiftUI
struct GMRView: View {
var body: some View {
NavigationStack {
VStack {
Rectangle()
.fill(.green)
Group {
Text("Hello")
.background(.yellow)
Text("Goodbye")
.background(.purple)
}
.frame(width: 100, height: 100, alignment: .leading)
//geometry는 사용 가능한 공간이 있다면 동일한 사이즈를 가지게끔 화면에 할당해줌
//현재 Rectengle, Group, GeometryReader 총 3개의 section으로
//나누기 때문에 3등분 한 것을 볼 수 있다
GeometryReader { proxy in
Rectangle()
.fill(Color.brown)
//다른 컨테이너 뷰와 달리 left,top 기준으로 그려진다(alignment 무시)
//기본적으로 지오메트리 공간에서 좌표가 0,0 상단 앞 가장자리 원점에 배치하게됨
Rectangle()
.fill(Color.cyan)
.frame(width: 100, height: 100, alignment: .center)
Text("Where am I?")
}
.background(.blue)
//==============================4등분
GeometryReader { proxy in
Text("Where am I")
.position(x: 0,
y: 0)
VStack(alignment: .leading) {
// Text("\(proxy.frame(in: .local).debugDescription)")
// Text("\(proxy.frame(in: .global).debugDescription)")
// Text("\(proxy.frame(in: .named("VStack")).debugDescription)")
Text(proxy.info(space: .local))
Text(proxy.info(space: .global))
Text(proxy.info(space: .named("VStack")))
}
Text("center")
.position(x: proxy.frame(in: .local).midX,
y: proxy.frame(in: .local).midY)
}
}
.coordinateSpace(name: "VStack")
.background(.teal)
.navigationTitle("GeometryReader")
}
}
}
#Preview {
GMRView()
}

import SwiftUI
struct GMRView2: View {
var body: some View {
NavigationStack {
VStack {
Rectangle()
.fill(.green)
.frame(height:200)
.overlay {
Text("VStack Offset '208'")
.font(.title)
.foregroundStyle(.purple)
.fontWeight(.bold)
}
GeometryReader { proxy in
VStack(alignment: .leading) {
Text(proxy.info(space: .local))
Text(proxy.info(space: .global))
Text(proxy.info(space: .named("VStack")))
}
Text("midY '480 / 2 = '240'\n GlobalY Postion : 360 + 208 + 240 = 808 ")
.position(x: proxy.frame(in: .local).midX,
y: proxy.frame(in: .local).midY)
.font(.title3)
.foregroundStyle(.purple)
.fontWeight(.bold)
//position global Space
Text("VStack GlobalY")
.position(x: proxy.frame(in: .local).midX,
y: proxy.frame(in: .named("VStack")).midY)
}
}
//상대적 위치나 크기를 효과적으로 계산 가능
//좌표공간을 지정하고 하위 뷰가 이 좌표 공간을 참조할 수 있게함
.coordinateSpace(name: "VStack")
.background(.teal)
.navigationTitle("GeometryReader")
}
}
}
#Preview {
GMRView2()
}

화면이 여러 층으로 이루어져 있는데 화면의 위에 NavigationBar+Status Bar + Rectangle = 360(global Position)
GeometryReader의 시작위치는 VStack 내부에 Rectangle 208 아래에 위치하고 있습니다
그래서 240 + 208 = 448 + 360 = 808
import SwiftUI
struct ImageGridView: View {
@State private var numImages: Double = 7
var body: some View {
var imageNames: [String] {
Array(1...Int(numImages)).map { "user\($0)" }
}
VStack {
Slider(value: $numImages, in: 1...7, step: 1.0)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3)) {
ForEach(imageNames, id: \.self) { name in
Image(name)
.resizable()
.aspectRatio(1, contentMode: .fill)
.border(.black)
}
}
Spacer()
}
.padding()
}
}
#Preview {
ImageGridView()
}

GeometryReader를 통해서 설정해보도록 하겠습니다
import SwiftUI
struct ImageGridView: View {
@State private var numImages: Double = 7
var body: some View {
var imageNames: [String] {
Array(1...Int(numImages)).map { "user\($0)" }
}
VStack {
Slider(value: $numImages, in: 1...7, step: 1.0)
GeometryReader { proxy in
let minCellWidth: CGFloat = proxy.size.width / 4
let maxCellWidth: CGFloat = proxy.size.width / CGFloat(imageNames.count) //3,2,1장일 때
//둘 중 더 큰 값을 채택하여 사용함
let optimalCellwidth = max(minCellWidth, maxCellWidth)
let numberOfColumns = Int(proxy.size.width / optimalCellwidth)
//열 개수를 동적으로 생성하여 그리드 레이아웃을 구성
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: numberOfColumns)) {
ForEach(imageNames, id: \.self) { name in
Image(name)
.resizable()
.aspectRatio(1, contentMode: .fill)
.border(.black)
}
}
}
Spacer()
}
.padding()
}
}
#Preview {
ImageGridView()
}
이렇게 설정하게되면 3개 이하의 이미지가 보일 때


이미지 사이즈가 동적으로 적용됩니다
마지막으로 스크롤뷰에 대해서 알아보고자 합니다
import SwiftUI
struct ScrollingImageView: View {
@State private var numImages: Double = 7
var body: some View {
var imageNames: [String] {
Array(1...Int(numImages)).map { "user\($0)" }
}
GeometryReader { proxy in //부모뷰 크기 정보
ScrollView(.horizontal) {
HStack(spacing: 0) {
ForEach(imageNames, id: \.self) { name in
GeometryReader { gm in //각 이미지 위치와 크기 정보
Image(name)
.resizable()
.scaledToFill()
.frame(width: proxy.size.width)
//이미지가 이동할 때 각 이미지가 화면의 왼쪽 끝에 고정된 것처럼 보이게함
//고정된 위치에 계속 나타나게 하기 때문에 스크롤할 때 여러 이미지가 동시에보임
//뷰를 가로방향으로 이동(오른쪽 왼쪽) 뷰의 위치를 이동시킴
.offset(x: -gm.frame(in: .global).origin.x)
// 이미지가 오른쪽으로 800만큼 움직이기 때문에 대부분의 이미지가 밖으로 가가게됨 그래서 아주 일부분만 보이게됨
//.offset(x:800)
//이미지가 프레임 벗어나지 않도록 자름
.clipped()
VStack {
//화면 전체를 기준으로 좌표를 계산
Text(gm.xOrigin(space: .global))
.font(.largeTitle)
.foregroundStyle(.white)
.padding(100)
//이미지 자체 크기때문에 계속 0으로 고정
//좌표 변화를 감지하기 어려움
Text(gm.xOrigin(space: .local))
.font(.largeTitle)
.foregroundStyle(.white)
.padding(100)
}
}
.frame(width: proxy.size.width)
}
}
}
.scrollIndicators(.hidden)
}
.ignoresSafeArea()
}
}
#Preview {
ScrollingImageView()
}

PinnedScrollableViews | Apple Developer Documentation
A set of view types that may be pinned to the bounds of a scroll view.
developer.apple.com
Grouping data with lazy stack views | Apple Developer Documentation
Split content into logical sections inside lazy stack views.
developer.apple.com
Creating performant scrollable stacks | Apple Developer Documentation
Display large numbers of repeated views efficiently with scroll views, stack views, and lazy stacks.
developer.apple.com
'SwiftUI' 카테고리의 다른 글
SwiftUI | Property Wrappers + Life Cycle (0) | 2024.09.29 |
---|---|
SwiftUI | Final Project _ 3일차 (1) | 2024.09.24 |
SwiftUI | 프로젝트에서 info.plist가 누락되었을 때 (0) | 2024.08.22 |
SwiftUI | 회원가입 만들기 (0) | 2024.08.22 |
SwiftUI | SwiftData Tutorials - List / NavigationStack / DatePicker (0) | 2024.08.08 |