안녕하세요 주니오스입니다. ✋✋✋✋
iOS개발을 해보신 분이라면 SafeArea에 대해 한번쯤은 들어보셨을 것이라고 생각합니다.
정확한 개념이 잡히지 않는 것같아 글로 정리해 보려고 합니다.
SafeArea란?
최신? iPhone은 상단에는 다이나믹 아일랜드가 하단에는 탭을 닫을 수 있는 바(Bar)가 있는 것을 확인할 수 있습니다.
이러한 요소들이 화면상에 명확하게 표시되는 것을 보장하기위해 SafeAreaInset이라는 개념이 존재합니다.
SafeAreaInset은 화면전체 영역과 SafeArea간의 간격을 의미합니다.
그렇습니다. SafeArea란 해당 간극(Insets)을 배재한 공간을 의미합니다.
하지만 주의해야 할 점이 있습니다.
모든 뷰가 같은 Inset을 가지는 것이 아닙니다.
앞서 설명한 위 다이나믹 아일랜드와 아래 바를 배제한 부분은 뷰계층의 최상단 뷰가 가지는 SafeArea입니다.
자세한 설명은 아래 더 설명하겠습니다.
아래 사진에 빨간색 border영역은 SafeArea입니다.
해당 빨간영역과 디바이스의 전체화면간의 간극이 SafeAreaInset들 입니다.
해당 간극을 출력하는 간단한 방법을 소개하겠습니다.
아래 수정자를 SafeAreaInset들을 알고 싶은 뷰에 부착하여 출력할 수 있습니다.
출처: https://fatbobman.medium.com/mastering-safe-area-in-swiftui-a183b8ad04d0
위 뷰의 간극을 한번 출력해 보겠습니다.
struct TestNavigationView: View {
var body: some View {
NavigationView(content: {
NavigationLink(destination: DestinationView()) { Text("Navigate") }
})
.border(.red)
.printSafeAreaInsets(id: "NavigationView")
}
}
/// NavigationView insets:EdgeInsets(top: 59.0, leading: 0.0, bottom: 34.0, trailing: 0.0)
위아래로 출력된 크기의 Inset들을 가지고 있는 것을 확인할 수 있습니다.
아래 뷰는 네비게이션뷰에서 DestinationView입니다.
상단에 네비게이션 바가 있는 것을 확인할 수 있는데요.
이 뷰의 SafeAreaInset들을 출력해 보겠습니다.
struct DestinationView: View {
var body: some View {
ZStack {
Color.cyan
Text("Destination")
}
.printSafeAreaInsets(id: "DestinationView")
}
}
/// DestinationView insets:EdgeInsets(top: 97.6..., leading: 0.0, bottom: 33.9..., trailing: 0.0)
Inset들중 bottom은 네비게이션뷰와 근소한 차이(무시해도될 수준 같습니다.)가 나타나지만
top부분의 인셋은 큰 차이가 발생하는 것을 확인할 수 있습니다. 😮
아까 제가 설명을 생략한 부분을 여기서 마저할 수 있는데요.
DestinationView의 SafeArea는 네비게이션 바에 의해 루트뷰보다 축소된 SafeArea를 가지게 됩니다.
즉 앱의 모든 뷰가 같은 SafeAreaInset을 가지는 것이 아니라는 것을 알 수 있습니다.
결론적으로
'SafeArea는 애플에서 지정한 뭔가를 표시하기 위해 전체 화면에서 축소된 영역입니다' 라고 이해할 수 있습니다. 🙌
ignoreSafeArea
이 수정자는 제생각에는 워딩이 조금 혼란을 줍니다.
ignoreSafeAreaInsets가 더 맞는말 아닐까요? 🤔 🤔 🤔
다들 아시다 시피 해당 수정자에는 region정보와 edge정보를 전달할 수 있습니다.
여기서 region 매개변수에는 오직 아래 3가지 값만 전달할 수 있습니다.
container, keyboard, all
all은 분명 앞의 두개를 모두 포함한다는 말일텐데 나머지 두개는 무슨 의미일까요? keyboard? 🤯
container가 의미하는 것은
다이나믹 아일랜드, 하단 이동바, 네비게이션 뷰 같은 요소들을 표시하기 위한 공간을 무시한다 라는 의미입니다.
region매개변수의 기본값 입니다.
DestinationView에 ignoreSafeArea(.container) 수정자를 부착해 보겠습니다.
그리고 ZStack에 border수정자를 부착해 보겠습니다.
ZStack {
Color.cyan
Text("Destination")
}
.ignoresSafeArea(.container)
.printSafeAreaInsets(id: "DestinationView")
.border(.red)
/// Text insets:EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 0.0)
/// Color insets:EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 0.0)
/// ZStack insets:EdgeInsets(top: 97.6..., leading: 0.0, bottom: 33.9..., trailing: 0.0)
DestinationView가 네비게이션 바가 차지하는 영역을 무시한 것을 확인할 수 있습니다.
여기서 확인할 수 있는 사실은 2가지 있습니다.
1. 컨테이너(ZStack)에는 ignoreSafeArea수정자가 적용되지 않습니다. 대신 내부 뷰들에 해당 수정자를 적용합니다.
(border랜더링 영역과 Inset 출력값으로 확인가능합니다.)
2. ZStack내부 뷰에는 해당 수정자가 모두 적용되에 Inset값들이 모두 0으로 설정됩니다.
keyboard
keyboard값을 전달하면 어떤 동작을 유도할 수 있을까요? 🤔
해당 동작을 이해하기 위해선 우선 soft키보드가 뷰에 나타날시 어떤 동작이 발생하는 지 이해해야 합니다.
아래 코드는 TextField를 포함하는 뷰입니다.
ZStack {
Rectangle()
.fill(.clear)
.background(
Image("japanView")
.resizable()
.scaledToFill()
.ignoresSafeArea(.container)
)
TextField(text: $inputStr) {
Text("입력해주세요!")
}
.textFieldStyle(.roundedBorder)
.border(.red)
.padding(.horizontal, 20)
}
키보드가 올라오기전 TextField는 화면의 중앙에 위치한 것을 확인할 수 있습니다.
TextField가 조금 위로 올라간 것을 확인할 수 있습니다. 또한 뒷배경도 뭔가 밀린느낌이 들지 않나요?? 🤔
그 이유는 키보드가 표시되는 영역만큼 SafeAreaInset이 추가되었기 때문입니다.
키보드가 올라왔을 때 SafeAreaInset을 출력해 보겠습니다.
ZStack {
Rectangle()
.fill(.clear)
.background(
Image("japanView")
.resizable()
.scaledToFill()
.ignoresSafeArea(.container)
)
.printSafeAreaInsets(id: "Rectangnle in ZStack")
...
}
.printSafeAreaInsets(id: "ZStack")
/// ZStack insets:EdgeInsets(top: 59.0, leading: 0.0, bottom: 336.0, trailing: 0.0)
/// Rectangnle in ZStack insets:EdgeInsets(top: 59.0, leading: 0.0, bottom: 336.0, trailing: 0.0)
ZStack과 내부뷰인 Rectangle의 Inset들을 확인한 결과 bottom영역의 Inset이 크게 증가한 것을 확인할 수 있습니다.
키보드가 뷰를 밀어내지 못하도록 하기(수정자+특별한 조건)
해당동작을 구현하기위해 아래와 같은 코드를 보신적이 있으실 겁니다. 🙌
.ignoresSafeArea(.keyboard, edges: .bottom)
이제 이 코드의 의미가 이해가 되시나요? 😚
네, 바로 soft키보드에 의해 늘어난 Inset을 무시하겠다는 의미입니다!
하지만 이 코드를 그냥 사용하기만 하면 아무런 효과도 없다는 것을 아실겁니다. 😓
앞서 제시한 뷰의 ZStack에 해당 수정자를 적용해 보겠습니다.
ZStack {
Rectangle()
.fill(.clear)
.background(
Image("japanView")
.resizable()
.scaledToFill()
)
✋✋✋✋✋
.ignoresSafeArea(.all)
.printSafeAreaInsets(id: "Rectangnle in ZStack")
TextField(text: $inputStr) {
Text("입력해주세요!")
}
.textFieldStyle(.roundedBorder)
.border(.red)
.padding(.horizontal, 20)
}
✋✋✋✋✋
.ignoresSafeArea(.keyboard, edges: .bottom)
.printSafeAreaInsets(id: "ZStack")
Rectangle에 all을 적용한 것은 배경이 container의 Inset도 무시하게 하려는 이유때문입니다.
뷰가 밀려나지 않는 것을 확인할 수 있습니다! 😎
이렇게 키보드가 화면을 밀어내지 않도록 하려면 수정자 추가하는 것 이외의 조건이 필요합니다.
바로 "TextField를 둘러싼 영역이 축소가능한가?" 입니다.
위의 경우 ZStack의 크기가 고정된 크기가 아닌 Flexible하기 때문인거죠.
예시로 보는 것이 훨씬 이해가 쉬울 것 같습니다.
아래 경우 키보드에 의한 화면 밀림이 발생하지 않습니다.
struct ContentView: View {
@State private var name: String = ""
var body: some View {
VStack {
Color.red //flexible
TextField("Name:", text: $name)
Color.yellow //flexible
}
.padding()
.ignoresSafeArea(.keyboard)
}
}
아래의 경우 수정자를 설정했음에도 불구하고 화면이 밀립니다.
struct ContentView: View {
@State private var name: String = ""
var body: some View {
VStack {
Color.red
.frame(height: 314) //fixed
TextField("Name:", text: $name)
Color.yellow
.frame(height: 314) //fixed
}
.padding()
.ignoresSafeArea(.keyboard)
}
}
조건과 수정자를 잘 사용하여 원하는 동작을 유도하시기 바랍니다.
추천하는 방식
사실 키보드가 TextField를 밀어내지 않는 것은 딱히 이상적인 동작은 아닙니다.
따라서 제생각엔 키보드가 배경은 밀어내지 않으면서 TextField를 밀어내게 구현하는 것이 가장 바람직하다고 생각합니다.
ZStack {
Rectangle()
.fill(.clear)
.background(
Image("japanView")
.resizable()
.scaledToFill()
)
.ignoresSafeArea(.all) ✋✋✋✋✋
TextField(text: $inputStr) {
Text("입력해주세요!")
}
.textFieldStyle(.roundedBorder)
.border(.red)
.padding(.horizontal, 20)
}
제일 깔끔하지 않나요? 🙌
주의할 점
제가 개발을 하다가 알게된 부분이라 공유드립니다.
Image뷰와 ScaleToFill
해당수정자를 ZStack컨테이너 내부의 뷰에 사용하게 될 경우 다른 뷰의 SafeAreaInset에 이상이 생깁니다.
ZStack {
Image("japanView")
.resizable()
.scaledToFill() ✋✋✋✋✋
.ignoresSafeArea(.all)
TextField(text: $inputStr) {
Text("입력해주세요!")
}
.textFieldStyle(.roundedBorder)
.border(.red)
.padding(.horizontal, 20)
}
레이아웃이 이상하게 망가진 것을 확인할 수 있습니다.
정확한 이유는 모르겠지만 해결방법은 background수정자 내부에 배경을 구현한 것입니다.
제가 앞서 제시한 코드들에는 Rectangle뷰에 background수정자를 사용하여 배경을 구현하였습니다.
컨테이너 크기 조정 + ignoreSafeArea
앞서제가 컨테이너에는 ignoreSafeArea수정자가 적용되지 않는다는 것을 말씀드렸지요?
컨테이너에 padding / frame수정와 같이 크기를 제한하는 수정자를 적용하면
ignoreSafeArea는 의도되로 작동하지 않습니다.
여기까지 SafeArea와 관련된 수정자에 대해 알아보았습니다.
좋은 하루 되세요 🙌
아래 글을 읽고 많은 도움이 되었습니다 한번 읽어 보시길 강추드려요!
'SwiftUI' 카테고리의 다른 글
[SwiftUI] View좌표계이해를 통한 addArc사용 (0) | 2023.09.09 |
---|---|
[SwiftUI] 뷰(View)가 여러개인 경우 효율적인 애니메이션 스케쥴링 방법 (0) | 2023.08.03 |
[SwiftUI] Transition사용법 심층정리 - 주니오스의 iOS어드벤쳐 (0) | 2023.06.27 |
[SwiftUI] UIViewRepresentable사용 (0) | 2023.06.22 |