sheet, transition, animation을 사용해서 popover 뷰를 만들어보겠습니다.

 

sheet

 

sheet는 흔히 밑에서 위로 올라오는 뷰를 의미하며 isPresented 의 결과가 true로 바뀌었을 경우 sheet가 나타납니다

파라미터

isPresented

: Boolean 타입의 값과 바인딩을 하여 해당 값이 true일 때 클로저 내부에 해당하는 콘텐츠를 나타냅니다.

 

onDismiss

: sheet를 없앨 때 실행하는 클로저입니다. sheet가 없어진 이후 동작할 코드를 작성하면 됩니다.

 

content

: sheet에 포함할 콘텐츠를 넣습니다.

 

struct ContentView: View {
    
    @State var showNewScreen: Bool = false
    
    var body: some View {
        ZStack {
            Color.orange
                .ignoresSafeArea()
            
            VStack {
                Button("BUTTON") {
                    showNewScreen = true
                }
                Spacer()
            }
            // METHOD 1 - SHEET
            .sheet(isPresented: $showNewScreen, content: {
                NewScreen()
            })
            
        }
    }
}

struct NewScreen: View {
    
    @Environment(\.dismiss) var dismiss
    var body: some View {
        ZStack(alignment: .topLeading) {
            Color.purple
                .ignoresSafeArea()
            
            Button(action: {
                dismiss()
            }, label: {
                Image(systemName: "xmark")
                    .foregroundStyle(.white)
                    .font(.largeTitle)
                    .padding(20)
            })
        }
    }
}

 

 

 

transition

transition은 뷰와 전환을 연관시킵니다

 

뷰가 나타나거나 사라질 때 animation을 사용하여 transition 이 적용됩니다.

 

struct ContentView: View {
    
    @State var showNewScreen: Bool = false
    
    var body: some View {
        ZStack {
            Color.orange
                .ignoresSafeArea()
            
            VStack {
                Button("BUTTON") {
                    withAnimation(.spring) {
                        showNewScreen = true
                    }
                }
                Spacer()
            }
            
            // METHOD 2 - TRANSITION
            ZStack {
                if showNewScreen {
                    NewScreen(showNewScreen: $showNewScreen)
                        .padding(.top, 100)
                        .transition(.move(edge: .bottom))
                }
            }
            .zIndex(2.0)
            
        }
    }
}

struct NewScreen: View {
    
    @Environment(\.dismiss) var dismiss
    @Binding var showNewScreen: Bool
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            Color.purple
                .ignoresSafeArea()
            
            Button(action: {
                withAnimation(.spring) {
                    showNewScreen.toggle()
                }
            }, label: {
                Image(systemName: "xmark")
                    .foregroundStyle(.white)
                    .font(.largeTitle)
                    .padding(20)
            })
        }
    }
}

 

 

알고가면 좋은점 zIndex

 

위에서 zIndex 를 사용하였습니다. 이때 zIndex란 ZStack에서의 뷰에 나타내는 우선 순위를 의미합니다.

ZStack {
    if showNewScreen {
        NewScreen(showNewScreen: $showNewScreen)
            .padding(.top, 100)
            .transition(.move(edge: .bottom))
    }
}
.zIndex(2.0)

 

만약 뷰의 앞뒤 순서를 제어하려면 zIndex(_:)를 사용합니다

파라미터

value

: 앞에서 뒤로 보내는 우선순위를 나타내는 값으로 기본값은 0입니다.

 

이 예제에는 두 개의 겹치는 회전된 직사각형이 있습니다. 가장 앞쪽은 더 큰 인덱스 값으로 표현된다. (즉 큰값이 사용자한테 먼저 보여짐)

VStack {
    Rectangle()
        .fill(Color.yellow)
        .frame(width: 100, height: 100, alignment: .center)
        .zIndex(1) // Top layer.


    Rectangle()
        .fill(Color.red)
        .frame(width: 100, height: 100, alignment: .center)
        .rotationEffect(.degrees(45))
        // Here a zIndex of 0 is the default making
        // this the bottom layer.
}

 

다음식으로 말이죠. 

offset

지정된 수평 및 수직 거리로 이 뷰를 이동할 수 있게 됩니다

 

파라미터

x

: x축으로 뷰의 위치를 어느정도 이동시킬 것인지를 결정합니다

y

: y축으로 뷰의 위치를 어느정도 이동시킬 것인지를 결정합니다

 

반환값

설정한 x축과 y축을 기준으로 이동한 view를 반환합니다

 

Offset(x:y:)를 사용하여 표시된 내용을 x 및 y 매개 변수에 지정된 위치로 이동할 수 있습니다. 아래의 예제에서 이 보기에 의해 그려진 회색 테두리는 텍스트의 원래 위치를 둘러싸고 있습니다. (x축이 증가하면 밑으로, x축이 감소하면 위로 이동하며 y도 동일합니다)

 

Text("Offset by passing horizontal & vertical distance")
    .border(Color.green)
    .offset(x: 20, y: 50)
    .border(Color.gray)

 

import SwiftUI

struct ContentView: View {
    
    @State var showNewScreen: Bool = false
    let window = UIApplication.shared.connectedScenes.first as! UIWindowScene
    
    var body: some View {
        ZStack {
            Color.orange
                .ignoresSafeArea()
            
            VStack {
                Button("BUTTON") {
                    withAnimation(.spring) {
                        showNewScreen = true
                    }
                }
                Spacer()
            }
            
            // METHOD 3 - ANIMATION OFFSET
            
            NewScreen(showNewScreen: $showNewScreen)
                .padding(.top, 100)
                .offset(y: showNewScreen ? 0 : window.screen.bounds.height)
            
        }
    }
}

struct NewScreen: View {
    
    @Environment(\.dismiss) var dismiss
    @Binding var showNewScreen: Bool
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            Color.purple
                .ignoresSafeArea()
            
            Button(action: {
                withAnimation(.spring) {
                    showNewScreen.toggle()
                }
            }, label: {
                Image(systemName: "xmark")
                    .foregroundStyle(.white)
                    .font(.largeTitle)
                    .padding(20)
            })
        }
    }
}

 

 

 

 

그렇다면 왜 다르게 사용하느냐?

위에서 보았듯이 결국 3개 다 동일하게 동작합니다. 하지만 커스텀화하기 위해서는 sheet 사용하지 않고 transition 또는 offset을 사용해야할 수 있습니다.

 

왜냐면 sheet는 아래서 위로 즉 transition(.move(edge: .bottom)), offset(y: )와 같이 아래서 위로 올라가는 기능밖에 하지 못하지만 나머지 2개의 경우는 다르게 설정할 수 있기 때문입니다.

 

transition을 다르게 구현해보기

만약 transition(.move(edge: .top))으로 동작하도록 구현한다면 아래처럼 동작할 것입니다.

struct ContentView: View {
    
    @State var showNewScreen: Bool = false
    let window = UIApplication.shared.connectedScenes.first as! UIWindowScene
    
    var body: some View {
        ZStack {
            Color.orange
                .ignoresSafeArea()
            
            VStack {
                Button("BUTTON") {
                    withAnimation(.spring) {
                        showNewScreen = true
                    }
                }
                Spacer()
            }

            // METHOD 2 - TRANSITION
            ZStack {
                if showNewScreen {
                    NewScreen(showNewScreen: $showNewScreen)
                        .padding(.top, 100)
                        .transition(.move(edge: .top))
                }
            }
            .zIndex(2.0)
        }
    }
}

struct NewScreen: View {
    
    @Environment(\.dismiss) var dismiss
    @Binding var showNewScreen: Bool
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            Color.purple
                .ignoresSafeArea()
            
            Button(action: {
                withAnimation(.spring) {
                    showNewScreen.toggle()
                }
            }, label: {
                Image(systemName: "xmark")
                    .foregroundStyle(.white)
                    .font(.largeTitle)
                    .padding(20)
            })
        }
    }
}

 

offset을 다르게 해보기 

만약 위에 기능을 똑같이 offset으로 구현한다면 아래와 같습니다

import SwiftUI

struct ContentView: View {
    
    @State var showNewScreen: Bool = false
    let window = UIApplication.shared.connectedScenes.first as! UIWindowScene
    
    var body: some View {
        ZStack {
            Color.orange
                .ignoresSafeArea()
            
            VStack {
                Button("BUTTON") {
                    withAnimation(.spring) {
                        showNewScreen = true
                    }
                }
                Spacer()
            }

            // METHOD 3 - ANIMATION OFFSET
            
            NewScreen(showNewScreen: $showNewScreen)
                .padding(.top, 100)
                .offset(y: showNewScreen ? 0 : -window.screen.bounds.height)
            
        }
    }
}

struct NewScreen: View {
    
    @Environment(\.dismiss) var dismiss
    @Binding var showNewScreen: Bool
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            Color.purple
                .ignoresSafeArea()
            
            Button(action: {
                withAnimation(.spring) {
                    showNewScreen.toggle()
                }
            }, label: {
                Image(systemName: "xmark")
                    .foregroundStyle(.white)
                    .font(.largeTitle)
                    .padding(20)
            })
        }
    }
}

 

총 코드

import SwiftUI

struct ContentView: View {
    
    @State var showNewScreen: Bool = false
    let window = UIApplication.shared.connectedScenes.first as! UIWindowScene
    
    var body: some View {
        ZStack {
            Color.orange
                .ignoresSafeArea()
            
            VStack {
                Button("BUTTON") {
//                    showNewScreen = true
                    withAnimation(.spring) {
                        showNewScreen = true
                    }
                }
                Spacer()
            }
            // METHOD 1 - SHEET
//            .sheet(isPresented: $showNewScreen, content: {
//                NewScreen()
//            })
            
            // METHOD 2 - TRANSITION
//            ZStack {
//                if showNewScreen {
//                    NewScreen(showNewScreen: $showNewScreen)
//                        .padding(.top, 100)
//                        .transition(.move(edge: .top))
//                }
//            }
//            .zIndex(2.0)
            
            // METHOD 3 - ANIMATION OFFSET
            
            NewScreen(showNewScreen: $showNewScreen)
                .padding(.top, 100)
                .offset(y: showNewScreen ? 0 : -window.screen.bounds.height)
            
        }
    }
}

struct NewScreen: View {
    
    @Environment(\.dismiss) var dismiss
    @Binding var showNewScreen: Bool
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            Color.purple
                .ignoresSafeArea()
            
            Button(action: {
//                dismiss()
                withAnimation(.spring) {
                    showNewScreen.toggle()
                }
            }, label: {
                Image(systemName: "xmark")
                    .foregroundStyle(.white)
                    .font(.largeTitle)
                    .padding(20)
            })
        }
    }
}

#Preview {
    ContentView()
}

 

ytw_developer