본문 바로가기
PROGRAMMING CODE/SWIFT

[SwiftUI] CustomSheet (FullScreen + middle, bottom) 만들기!

by daye_ 2024. 2. 21.

 

 

 

 

 

참고 자료

 

Drag & Drop을 이용해서 CustomSheet를 만들었다!

 

[SwiftUI] Drag&Drop (커스텀 뷰 기초, offset과 DragGesture)

Drag & Drop은 사용자의 Gesture에 따라 움직이는 뷰를 만들때 사용한다. Custom으로 뷰를 만들 때 응용하면 아주 좋음! public struct DragGesture : Gesture { ... } DragGesture에는 위 두가지 함수가 있음! 그리고 그

da-ye.tistory.com

 

 

 

제작 이유
프로젝트 진행중에 지도 위에 띄울 시트는 기본 시트로 만들기엔 부족한 점이 몇 가지 있었다.
1. 백그라운드 터치시 시트가 닫히지 않아야 함
2. bottom, middle, full 총 3가지 타입이 합쳐져야함

 

일단 최대한 찾아본 바로는 백그라운드 터치했을때 기본 시트가 닫히지 않게 하거나, Shadow를 없애는 방법과

fullScreen 과 다른 detent를 합치는 방법은 없는걸로 알고있다...

 

 

 

 

 

 

실패 경험

 

사실 전 프로젝트에서 DragGesture를 이용해서 사이드바를 만든적이 있었는데

DragGesture의 방향만 지정해서 화면크기의 절반 이상을 특정 방향으로 움직이면 사이드바가 닫히거나 나오도록 구현했었다.

 

그때는 오프셋 값을 구해서 바로 업데이트 하기 보다는 뷰에서 .transition을 이용했음

당시 SwiftUI로 Custom View를 처음 만들어봤던 나로써는 .transition을 사용하는것이 구현하기에 간단해 보였기 때문,,ㅎ

 

근데 커스텀 뷰는 엄청 세심하게 만들어야 한다는것을 깨달았다 ^^,,

 ChatSideBar(isMenuOpen: $isMenuOpen, isExitButtonAlert: $isExitButtonAlert, chatRoomName: chatRoom.chatRoomName ?? "\(targetUserInfos[0].nickName)", targetUserInfos: targetUserInfos)
                    .offset(x: x)
                    .transition(isMenuOpen ? .move(edge: .trailing) : .move(edge: .leading))
                    .safeAreaPadding(.top, 50)
                    .gesture(DragGesture().onEnded({ (value) in
                        if value.translation.width > 0{
                            isMenuOpen = false
                        }
                    }))

당시 다음과 같이 코드를 짜니 오히려 자잘하게 처리 해 줄게 많았다.

 

예를들어, 오른쪽 방향으로 드래그 하다가 떼지 않고 왼쪽으로 드래그 했을 때 오른쪽 방향으로 더 세게 적용되는 경우와

Shadow 처리가 어려워지는 경우 등..

 

마감기간이 다 돼서 결국 닫을때만 처리되도록 했는데 아쉬움이 남았던 기억이,,,

 

 

 

 

 

진정한 . 커스텀. 시트 .

 

여튼 이번엔 gesture.transition으로 offset값을 제대로 계산해서 만들었다!

먼저 원하는 SheetView 모양을 만든 후 같이 쓰일 뷰에 추가해준다.

 

 

struct ContentView: View {
    @State private var dragOffset: CGFloat =  0
    @State private var currentOffset: CGFloat = 0
    
    var body: some View {
        ZStack {
           // 백그라운드 뷰 추가 위치
            CustomSheetView()
                .frame(height: UIScreen.main.bounds.size.height)
                .offset(y: dragOffset)
                .gesture(DragGesture()
                    .onChanged { gesture in
                            dragOffset = gesture.translation.height + currentOffset
                    }
                    .onEnded { gesture in
                            dragOffset = gesture.translation.height + currentOffset
                            currentOffset = dragOffset
                    })
        }
}

일단 요까지는 이 전에 진행했던 Drag & Drop 에서 Offset의 y만 변경되도록 설정했다.

요것까지 하면 다음과 같이 드래그에 따라 움직이는 시트가 됨!

 

 

 

 

 

 

 

 

 

시트 높이(타입)과 시트 타입을 결정할 경계를 먼저 선언해준다.

enum SheetHeight {
    static let full = getSafeAreaTop()
    static let middle = UIScreen.main.bounds.size.height * (1/2)
    static let bottom = UIScreen.main.bounds.size.height * (6/7)
}
enum SheetBoundary {
    static let high = UIScreen.main.bounds.size.height * (1/4)
    static let low = UIScreen.main.bounds.size.height * (3/4)
}

 

offset은 세심한 계산이 필요하다.

그래서 반응형으로 만들기 위해 UIScreen으로 화면 높이에 따라 처리해줬다.

 

 

 

.onAppear {
            dragOffset = SheetHeight.bottom
            currentOffset = SheetHeight.bottom
        }

 

기본적으로 나타나는 위치는 SheetView가 y = 0의 offset을 가지게 나오니까 내가 선언한 것 중 full 타입이다.

첫 화면에서 bottom위치에 뜨기를 원해서 bottom으로 설정해줬다.

 

(offset의 기준은 뷰의 중심)

 

 

 

 

private func decideSheetHeight() {
        withAnimation(dragOffset > SheetBoundary.high  ?
            .spring(response: 0.3, dampingFraction: 0.7, blendDuration: 0): .easeInOut) {
                
                if dragOffset > SheetBoundary.low {
                    dragOffset = SheetHeight.bottom 
                } else if dragOffset < SheetBoundary.low && dragOffset > SheetBoundary.high {
                    dragOffset = SheetHeight.middle
                } else {
                    dragOffset = SheetHeight.full
                }
            }
        currentOffset = dragOffset
    }

그리고 시트위치를 if문으로 나눴다.

enum으로 정리했으면 깔끔할것같다!

 

이 코드를 .onEnded의 currentOffset = dragOffset 대신에 넣어주면 된다.

 

 

 

 

저렇게만 해주면 시트 완성 ^.^ 

플러스로 시트가 상단에 닿았을 때 완전히 풀스크린으로 보이게 하려면

 

 var body: some View {
    ZStack {
 	...
    }
    .overlay(
     	SafetyAreaTopScreen, alignment: .top
    )
}
 
 private var SafetyAreaTopScreen: some View {
        Group {
            Rectangle()
                .foregroundColor(.white)
                .frame(height: getSafeAreaTop())
                .edgesIgnoringSafeArea(.all)
                .opacity(dragOffset < SheetHeight.full + getSafeAreaTop() ? 1 : 0)
        }
}

 

 

이렇게 해주면 풀스크린 완성

풀스크린일 때 닫는 방법은 자유,,

 

 

 

 

 

 

 

현재 코드에서 Custom Sheet의 크기가 

 .frame(height: UIScreen.main.bounds.size.height)

 

이렇게 스크린의 높이로 딱 맞게 지정되어있기 때문에 indicator가 가려질 만큼 올리면 아래가 비게 되어

저렇게 구현한 것!

 

 

 

 

 

 

 

 

다른 방법으로는 frame 넉넉히 지정 후, 

GeometryReader를 이용하여 상단이 보이지 않을 만큼 sheet를 올리는 방법도 있다.

sheet의 offset 기준을 상단으로 재정의하면

원래 Offset인 중심을 기준으로 음수부터 양수까지 사용하며 코드 짤 때 조금 더 헷갈리지 않을 수 있지만

GeometryReader 자잘한 오류가 많아서 최대한 이용하지 않아야한다는 글을 보고 안쓰려고 하는 중이다!

 

 

 

 

 

 

 

 

 

 

DragGesture + ScrollView를 같이 사용하면 .onEnded가 호출되지 않는 버그가 있다고 한다.

적용하게 된다면 블로그에 써보겠다.

 

갑자기 프로젝트에서 사라지게돼서 언제 쓰게 될지는 모르겠찌만.,,