2024.10.28 - [개발노트/iOS] - SwiftUI의 Navigation(화면 전환) - 선언형 구조와 명령형 구조의 차이
유지보수 건으로 넘겨받은 프로젝트 중 아이패드를 지원하는 프로젝트가 있다.
alert popup을 fullScreenCover로 띄우고 있는데, 아니.. 버튼을 눌렀는데도 팝업이 뜨지를 않는다..
GPT한테 물어보니 NavigationView 안에서는 fullScreenCover가 제대로 동작하지 않을 수도 있다고 하더라.
iPad 쪽 프로젝트는 경험이 없어서 여기저기 얽혀있는 NavigationView의 코드 자체가 잘못된 건지, 내가 이해를 못하고 있는 건지..
그래서 아이패드는 아이폰과 네비게이션이 어떤 식으로 다른 건지,
애초에 fullScreenCover를 쓰면 안 되는 곳에서 사용하고 있는 건지, 코드 구조가 잘못 돼서 그런 것뿐인지 여기에 테스트 좀 해보려고 한다.
1. iPad 와 iPhone 의 Navigation 형태 차이
우선은 이전에 Navigation 공부를 하면서 만들어둔 코드를 수정 없이 그대로 패드에 실행해봤다.
![]() |
![]() |
iPhone | iPad |
오..
음 그렇지.. 당장 기본 앱 중 '설정' 앱만 들어가봐도 저런 형태로 나오긴 하지
1-a. NavigationLink 이동 시
![]() |
![]() |
iPhone | iPad |
별다른 커스텀 없이 NavigationLink를 통해 화면을 추가하면
패드 버전의 경우 자동으로 저렇게 destination 화면이 우측 빈 공간에 표시되는데,
1-b. hide back button
![]() |
![]() |
iPhone | iPad |
뒤로가기 버튼을 커스텀하고 싶어서 ```.navigationBarBackButtonHidden(true)```를 사용했다면..
패드 버전에서도 저렇게 뒤로갈 수 없는 화면에 뒤로가기 버튼이 표시되어 버리고..
심지어 상단 영역이 좀 떠 있는 것 같고..(아마도 내비게이션 타이틀 영역)
var body: some View {
VStack {
HStack {
// iPad 버전일 때는 SplitView 형태로 동작하므로 뒤로가기 버튼이 없어도 됨
// (커스텀 버튼이 아니라면 자동으로 처리되는 부분)
if UIDevice.current.userInterfaceIdiom == .phone {
Button(
action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "chevron.backward")
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.padding()
}
}
Spacer()
}
Spacer()
Text("나는 프로그래밍적으로 이동한 화면\n내비게이션 뒤로가기 버튼을 숨겼지!")
Spacer()
}
.navigationBarBackButtonHidden(true)
// SplitView 형태(iPad ver)에서는 자동으로 내비게이션 타이틀 영역이 잡히게 됨
.navigationBarHidden(true)
}
그래서 해당 부분을 반영하여 매만지자면 이런 식으로 된다
2. iPad의 Navigation도 iPhone처럼 동작하게 하기
💬 아이패드의 내비게이션 동작도 아이폰처럼 동작하게 할 수 있나요?
: 네, iPad에서도 iPhone과 같은 풀스크린 전환을 원할 때, 다음과 같은 방법들을 사용할 수 있습니다.
1. NavigationStack 사용 (iOS 16 이상)
iOS 16 이상에서는 NavigationStack을 사용하여 iPad에서 iPhone처럼 모든 화면을 푸시(Push) 방식으로 전환하도록 할 수 있습니다. NavigationStack은 split view 없이 각 뷰를 스택 구조로 쌓아 올리기 때문에, iPhone과 비슷한 사용자 경험을 제공합니다.
2. 강제로 NavigationViewStyle 설정
SwiftUI에서 NavigationViewStyle을 이용하여 iPad에서도 iPhone처럼 stack 스타일을 강제 설정할 수 있습니다.
3. UIKit을 이용한 풀스크린 전환
만약 SwiftUI와 UIKit을 함께 사용하고 있다면, UINavigationController를 직접 사용하여 각 화면을 iPhone처럼 풀스크린으로 전환할 수 있습니다. SwiftUI에서는 UIViewControllerRepresentable을 통해 UIKit 내비게이션 구조를 SwiftUI에 통합할 수 있습니다.
별도의 설정이 없을 경우,
iPhone 환경일 때는 화면 전환이 Stack 구조로 이뤄지고, iPad 환경일 때는 SplitView를 사용하게 되어있다.
따라서 iPad일 때도 화면 전환을 스택 구조로 동작하게 해준다면 iPhone과 동일한 형태의 화면 구조가 되는 것.
var body: some View {
NavigationView {
...
}
// iPad에서도 스택 구조의 화면 전환 사용하기
.navigationViewStyle(.stack)
}
![]() |
![]() |
3. SplitView 위에서의 Alert Popup
3-a. ZStack
빠른 테스트를 위해 다른 프로젝트에서 사용하던 CustomAlert 파일을 복사해왔다.
ViewModifier로 구현했고, ZStack으로 화면을 덮어 띄우도록 해뒀는데,
fileprivate struct BaseAlert<ContentView: View, ButtonView: View>: ViewModifier {
...
func body(content: Content) -> some View {
ZStack {
content
.disabled(isPresented)
.blur(radius: isPresented ? 2 : 0)
if isPresented {
// Background dimming
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
if (useBackgroundTapClose) {
isPresented = false
}
}
// Alert window
CustomAlertView(contentView: contentView,
buttonView: buttonView)
}
}
}
}
SplitView 위에서는 우측 화면만 덮게 되는구나
3-b. fullScreenCover
fullScreenCover로 띄우게 될 경우엔 modifier 이름답게 전체화면을 가리면서 뜬다.
다만 전체화면을 가리는 거라 ZStack처럼 뒤 화면이 비쳐보이진 않는다.
3-b-i. 자동으로 내비게이션 이동 후 fullScreenCover
지금 이 테스트를 하게 했던 문제의 프로젝트는 자동 로그인, 마지막 탭 저장 등의 동작 때문에 화면을 자동으로 이동시켜주는 동작이 많다.
이 프로젝트에서도 fullScreenCover 팝업이 잘 뜨는 경우가 있었으니,
혹시 자동 동작 때문에 문제가 되는 게 아닐까 싶어 비슷한 경우를 만들어보기로 함.
struct ContentView: View {
@Binding var rootViewType: RootViewType
var body: some View {
VStack(spacing: 24) {
Button {
rootViewType = .practiceMVI
} label: {
Text("Practice MVI")
}
Button {
rootViewType = .practiceNavigation
} label: {
Text("Practice Naivgation")
}
}
.padding()
// 화면 자동 이동 테스트
.onAppear {
rootViewType = .practiceNavigation
}
}
}
struct NavigationTestView: View {
@State var goToStartView = false
@State var goToAnotherView = false
@State var goToThirdView = false
var body: some View {
NavigationView {
...
}
// 화면 자동 이동 테스트
.onAppear {
goToAnotherView = true
}
}
}
음.. 그런데 잘 뜨더라
뭐지.. 역시 문제의 프로젝트는 스파게티 코드여서 어디선가 또 구조가 꼬인 걸까..
일단은 원인으로 의심되는 부분은 테스트 해봤으니 추후 다른 가설이 생기면 다시 이어서 테스트 해보는 게 좋을 것 같다.
4. Alert Popup을 구현하기 위해 보편적으로 쓰이는 방법?
💬 SwiftUI에서 커스텀 Alert Popup을 구현하기 위해 보편적으로 쓰이는 방법이 있을까요?
→ (ZStack 사용하는 방법 추천해줌)
💬 코드를 저렇게 짜면 splitView에서는 우측 화면 영역 위에만 알럿이 뜨게 되지 않나요?
→ (맞음. 그걸 방지하고 싶으면 NavigationView 바깥에서 ZStack을 사용하라고 함)
💬 이렇게되면 내비게이션 하위 화면에서 alert 창을 띄우려 할때 코드가 복잡해지지 않을까요?
→ (맞음. 그걸 방지하고 싶으면 별도의 화면으로 구현하고 **@Environment**나 **ObservableObject**를 활용하여 알림을 관리하라고 함)
💬 iOS의 기본 Alert popup (wifi 비밀번호 입력 창 등) 처럼 동작하게 할 수는 없는 걸까요?
→ (UIViewControllerRepresentable를 사용하여 UIAlertController를 구현하라고 하면서 코드를 줬지만 제대로 동작해먹는 코드가 아니었음)
.alert("나는 기본 alert modifier로 띄운 alert", isPresented: $isPresentedSystemAlert) {
Button("OK", role: .cancel) {
isPresentedSystemAlert = false
}
}
이렇게 기본으로 제공해주는 alert modifier를 사용하면 말끔하게 동작하긴 하지만,
웬만한 프로젝트에는 모두 커스텀이 들어가야 하니까..ㅎㅎ
구글 서치도 조금 해봤는데, ZStack을 사용하는 경우가 많은 것 같고, 종종 fullScreenCover를 사용하는 경우도 있는 것 같아 보였다.
지금까지 공부한 결과로는
ZStack을 사용하는 alert을 navigation 바깥에 두고 ObservableObject 사용하는게 제일 무난할 것 같은 생각이 든다.
'개발노트 > iOS' 카테고리의 다른 글
Xcode Preview - No Selected Scheme (0) | 2024.11.13 |
---|---|
SwiftUI의 Navigation(화면 전환) - LinkNavigator의 이모저모 (1) (3) | 2024.11.12 |
SwiftUI의 Navigation(화면 전환) - 선언형 구조와 명령형 구조의 차이 (0) | 2024.10.28 |
SwiftUI로 Video Player 만들기 - UIViewRepresentable (1) | 2024.10.25 |
SwiftUI와 MVI (2) (0) | 2024.10.20 |