https://developer.apple.com/documentation/swiftui/geometryreader
GeometryReader | Apple Developer Documentation
A container view that defines its content as a function of its own size and coordinate space.
developer.apple.com
GeometryReader의 필요성
SwiftUI로 개발을 하다 보면 애플에서 의도한 대로(?) 디바이스의 사이즈, 혹은 부모View의 사이즈를 전혀 생각하지 않고도 대부분 원하는 화면을 그려가게 되지만. 부모View의 사이즈를 알고 해당 좌표계에 따라서 그리고 싶을 수 있음.
GeometryReader는 자식View에 자신의 크기와 좌표공간을 제공해서 이를 이용해서 정의할 수 있도록 도와주는 View.
content 클로저로 자신의 size, frame(iOS17+), safeAreaInsets 등 자식View를 그리는데에 필요한 정보(GeometryProxy)를 전달.
struct GeometryReaderPractice: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
Text("geometry 정보")
.font(.title)
Text("size: ").bold() + Text("\(geometry.size)")
Text("frame: ").bold() + Text("\(geometry.frame(in: .local).dictionaryRepresentation)") // iOS17+
Text("safeAreaInsets: ").bold() + Text("\(geometry.safeAreaInsets)")
}
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.border(Color.blue, width: 3)
}
}
GeometryReader의 하위View
이 GeometryReader의 content 클로저 내에서 자식View들의 위치는 frame좌표계를 따르기 때문에
따로 정의해주지 않으면 (0, 0)으로 위치하게 됨.
위에서 하나로 묶어준 VStack을 제거하면 아래처럼 레이아웃이 됨.
struct GeometryReaderPractice: View {
var body: some View {
GeometryReader { geometry in
// VStack(alignment: .leading) {
Text("geometry 정보")
.font(.title)
Text("size: ").bold() + Text("\(geometry.size)")
Text("frame: ").bold() + Text("\(geometry.frame(in: .local).dictionaryRepresentation)") // iOS17+
Text("safeAreaInsets: ").bold() + Text("\(geometry.safeAreaInsets)")
// }
// .frame(maxWidth: .infinity, maxHeight: .infinity)
// .padding()
}
.border(Color.blue, width: 3)
}
}
아래처럼 frame, position, offset 등으로(잇몸으로?) 자식View들의 위치를 잡아줄 수도 있지만,
정신건강을 위해 자식View들은 처음 예시처럼 그려주고, GeometryProxy의 정보만 활용하는 것이 좋음.
(frame은 View의 width, maxWidth, height, maxHeight, alignment를
offset은 View content의 offset(x, y)를
position은 View center의 절대위치(x, y)를 정의)
struct GeometryReaderPractice: View {
var body: some View {
GeometryReader { geometry in
Text("geometry 정보")
.background(Color.red)
.font(.title)
.frame(width: geometry.size.width, alignment: .center) // frame 정의
Text("size: \(geometry.size)")
.background(Color.green)
.offset(y: 60) // offset 정의
Text("frame: \(geometry.frame(in: .local).dictionaryRepresentation)")
.background(Color.yellow)
.position(x: geometry.size.width/2, y: geometry.size.height/2) // position 정의
}
.padding()
.border(Color.blue, width: 3)
}
}
GeometryProxy의 safeAreaInsets
GeometryProxy의 safeAreaInsets 정보는 window의 safeAreaInsets가 아닌
해당 GeometryReader가 적용하고 있는 safeAreaInsets이므로 사용할 때 주의가 필요.
현재 디바이스의 절대적인 safeAreaInsets정보가 필요하다면 UIWindowScene으로부터 가져와야 함.
struct GeometryReaderPractice: View {
var body: some View {
let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
let safeAreaInsets = scene?.windows.first?.safeAreaInsets ?? .zero // Device의 safeAreaInsets
GeometryReader { geometry in
VStack(alignment: .leading) {
Text("safeAreaInsets: ").bold() + Text("\(geometry.safeAreaInsets)")
Text("safeAreaInsets(Device): ").bold() + Text("\(safeAreaInsets)")
}
.padding()
}
.border(Color.blue, width: 3)
GeometryReader { geometry in
VStack(alignment: .leading) {
Text("safeAreaInsets: ").bold() + Text("\(geometry.safeAreaInsets)")
}
.padding()
}
.border(Color.red, width: 3)
GeometryReader { geometry in
VStack(alignment: .leading) {
Text("safeAreaInsets: ").bold() + Text("\(geometry.safeAreaInsets)")
}
.padding()
}
.border(Color.green, width: 3)
}
}
GeometryProxy의 frame(iOS17+)
GeometryProxy의 frame의 정보는 global, local, named 세가지를 기준으로 가져올 수 있음.
frame(in: .global)과 frame(in: .local)
.global일 때는 window를 기준으로 한 GeometryProxy의 frame정보를 가져옴.
struct GeometryReaderPractice: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
Text("frame(in: .local): ").bold() + Text("\(geometry.frame(in: .local).dictionaryRepresentation)") // iOS17+
}
.foregroundColor(Color.blue)
.padding()
}
.border(Color.blue, width: 3)
GeometryReader { geometry in
VStack(alignment: .leading) {
Text("frame(in: .global): ").bold() + Text("\(geometry.frame(in: .global).dictionaryRepresentation)") // iOS17+
}
.foregroundColor(Color.green)
.padding()
}
.border(Color.green, width: 3)
}
}
frame(in: .named)
마지막으로 .named는 특정View에 .coordinateSpace(name:) modifier를 통해 이름을 붙여주고,
해당 View를 기준으로 frame을 가져올 수 있게 해줍니다.
아래는 ScrollView 내부에서 GeometryReader의 frame을 ScrollView를 기준으로 받아오는 예시.
struct GeometryReaderPractice: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(0..<20) { index in
ZStack {
Rectangle()
.foregroundColor(Color.blue)
GeometryReader { geometry in
Text("frame: \(geometry.frame(in: .named("기준점")).dictionaryRepresentation)")
.frame(width: geometry.size.width, height: 150, alignment: .leading)
.padding(.horizontal)
}
.frame(maxWidth: .infinity, maxHeight: 150)
}
.frame(height: 150)
.padding(16)
}
}
}
.coordinateSpace(name: "기준점")
}
}
GeometryReader 사용예시
자식View의 크기를 부모View의 size(혹은 width나 height만)에 비례해서 그리고 싶을 때,
struct GeometryReaderPractice: View {
var body: some View {
GeometryReader { geometry in
let size = geometry.size
Group() {
Rectangle() // 부모 view width의 절반 크기의 정사각형
.fill(.red)
.frame(width: size.width/2, height: size.width/2)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.border(Color.blue, width: 3)
}
}
frame좌표계를 사용하지 않고 Size만 가져오고 싶을 때
View의 Size만 가져와서 활용하고 싶을 때는,
Size를 알고 싶은 View의 background에 GeometryReader를 넣고
그 안에 Color(clear)를 넣은 뒤, onAppear에서 @State로 선언한 size에 반영해서 사용하면 됨.
예시.
struct GeometryReaderTest: View {
@State var size: CGSize = .zero
var body: some View {
VStack {
Text("View를 frame좌표계로 그리지 않으면서")
Text("View의 사이즈만 가져오고 싶을 때")
Text("\(size)")
}
.background(
GeometryReader { geometry in
Color.orange
.onAppear {
size = geometry.size
}
})
}
}