Where who wants to meet someone

[SwiftUI] .sheet의 이것저것 알아보기 본문

Apple Developer/iOS(SwiftUI)

[SwiftUI] .sheet의 이것저것 알아보기

Lust3r 2024. 2. 8. 12:34
728x90
 

sheet(isPresented:onDismiss:content:) | Apple Developer Documentation

Presents a sheet when a binding to a Boolean value that you provide is true.

developer.apple.com

modal로 보여줘야 하는 View가 있어서 찾던 중, sheet이 적합하다고 여겨져서 사용을 했다.

 

하지만 기본값은 디바이스 높이만큼 View가 올라오는 것이었기에 조정이 필요했고, Indicator와 좀 더 굴곡진 표현 역시 원했기에 메서드를 찾아보았다.

 

밋밋하고 너무 높은 기본값

 

위 애플 공식문서에서 좌측 메뉴를 보면, Modal presentations에서 사용할 수 있는 다양한 메서드를 안내하고 있다.


 

먼저 높이는 Configuring a sheet's height에서 찾을 수 있었다.

 

 

presentationDetents(_:) | Apple Developer Documentation

Sets the available detents for the enclosing sheet.

developer.apple.com

 

시트에 사용 가능한 detent를 설정하는 메서드라고 하는데, detent가 무엇일까?

 

설명으로는 이를 하나 이상 제공하면 사람들이 드래그하여 크기를 조정할 수 있다고 한다.

 

글보다는 실제로 테스트해서 비교하는 것이 더 이해가 쉬울 것 같아 하나씩 확인해봤다.

 

struct ContentView: View {
    @State private var showSettings = false


    var body: some View {
        Button("View Settings") {
            showSettings = true
        }
        .sheet(isPresented: $showSettings) {
            SettingsView()
                .presentationDetents([.medium, .large])
        }
    }
}

 

sheet에서 보여줄 Content 아래에 presentationDetents modifier를 사용하여 [] 안에 detent를 제공하면 된다.

 

 

detent로 사용할 수 있는 값은 위 이미지처럼 있다.

 

기본값인 large를 포함해서 medium, custom, fraction, height 등.

 

그렇다면 기본값인 large를 위에서 봤으니 medium을 해보자

 

 

처음 봤었던 높이와 다르게 화면의 절반까지만 올라오는 것을 볼 수 있다.

 

그렇다면 애플 예제코드와 같이 2가지를 제공하면 어떻게 될까?

 

 

위 영상처럼 modal을 끌어서 제공한 detent 내에서 사이즈를 조절할 수 있다.

 

또는 위에서 본 다른 메서드를 사용해서 특정한 사이즈로도 만들수도 있다.

 

 

위 모달은 detent로 .height(500)을 해준 것이다.


두 번째로 Indicator! 여러 앱을 사용하다보면 저런 모달에는 상단에 작대기 모양이 하나씩 달려있는 것을 볼 수 있다.

 

그것은 어떻게 추가할 수 있을까?

 

 

presentationDragIndicator(_:) | Apple Developer Documentation

Sets the visibility of the drag indicator on top of a sheet.

developer.apple.com

 

presentationDragIndicator(_:)를 사용하면 sheet의 상단에 drag indicator를 보이게 할 수 있다.

 

struct ContentView: View {
    @State private var showSettings = false


    var body: some View {
        Button("View Settings") {
            showSettings = true
        }
        .sheet(isPresented: $showSettings) {
            SettingsView()
                .presentationDetents([.medium, .large])
                .presentationDragIndicator(.visible)
        }
    }
}

 

위처럼 .visible 옵션을 주게 되면?

 

 

익숙한 Indicator가 상단에 보이게 된다.


마지막으로, 기본값도 굴곡이 들어가있긴 하지만 거의 없어서 직사각형처럼 보이기 때문에 좀 더 주고싶은데 어떤 메서드를 쓸 수 있을까?

 

Styling a sheet and its background에 답이 있었다.

 

 

presentationCornerRadius(_:) | Apple Developer Documentation

Requests that the presentation have a specific corner radius.

developer.apple.com

 

presentationCornerRadius(_:)를 사용하면 파라미터로 전달한 CGFloat값 만큼 View에 반영해준다.

(nil의 경우 시스템 기본값 사용)

 

struct ContentView: View {
    @State private var showSettings = false


    var body: some View {
        Button("View Settings") {
            showSettings = true
        }
        .sheet(isPresented: $showSettings) {
            SettingsView()
                .presentationDetents([.medium, .large])
                .presentationCornerRadius(21)
        }
    }
}

 

예제코드처럼 21값을 주면 어떻게 변할까?

 

 

이전보다는 좀 더 둥글게 변한 것을 볼 수 있다.


+ HIG

 

 

Modality | Apple Developer Documentation

Modality is a design technique that presents content in a separate, dedicated mode that prevents interaction with the parent view and requires an explicit action to dismiss.

developer.apple.com

 

HIG 내 Modality에서는 modal을 어떤 경우에 사용할 수 있는지, 어떻게 쓰는 것이 좋은지 안내하고 있다.

 

그 중 하기와 같은 내용이 있다.

 

Always give people an obvious way to dismiss a modal view.
In general, it works well to follow the platform conventions people already know. For example, in iOS, iPadOS, and watchOS apps, people typically expect to find a button in the navigation bar or swipe down; in macOS and tvOS apps, people expect to find a button in the main content view.

 

항상 사람들에게 modal view를 해제할 수 있는 명확한 방법을 제공하라는 말이다.

 

이미 모달은 스와이프 방식 혹은 빈 공간 터치를 통해 내릴 수 있지만, 명확하게 버튼을 통해(예를 들어 Done, 닫기 등) 제공할 수도 있는 것이다.

 

그렇다면 버튼을 Content로 둔다면, action에서 어떻게 해줘야 모달을 해제할 수 있을까?

 

이 역시 아까 Modal presentations 세부 항목에서 찾을 수 있다.

 

거의 마지막 부분에 위치한 Dismissing a presentation에 var dismiss: DismissAction 이라고 있음을 볼 수 있다.

 

 

dismiss | Apple Developer Documentation

An action that dismisses the current presentation.

developer.apple.com

 

dismiss라는 Environment value를 사용하여 현재 Environment에 대한 DismissAction 인스턴스를 가져오고, 이를 호출하여 해제를 수행한다고 한다.

 

private struct SheetContents: View {
    @Environment(\.dismiss) private var dismiss


    var body: some View {
        Button("Done") {
            dismiss()
        }
    }
}

 

위 예제 코드를 보면 dismiss라는 변수의 프로퍼티 래퍼로 @Environment(\.dismiss)를 사용하고, 버튼의 액션으로 dismiss()를 호출한다.

 

간단히 Environment는 뷰 계층 구조를 통해 전파되는 환경 값의 모음으로 뷰를 구성하는 데 사용할 수 있는 SwiftUI의 값 컬렉션이다.

 

 

EnvironmentValues | Apple Developer Documentation

A collection of environment values propagated through a view hierarchy.

developer.apple.com

 

예제코드에서는 그 값 컬렉션 중 dismiss라는 값의 키 패스를 특정하여 dismiss 변수를 선언한 것이다.

 

여기서 궁금해지는 점이, 변수인데 메서드처럼 ()로 호출할 수 있는가? 일 것이다.

 

이는 다음 설명에 나오는데, 인스턴스를 호출할 때 Swift가 호출하는 callAsFunction() 메서드를 정의한다고 한다.

 

callAsFunction? 이게 뭔가 싶지만 이 역시 DismissActionCalling the action 항목에 설명을 해놓았다.

 

 

callAsFunction() | Apple Developer Documentation

Dismisses the view if it is currently presented.

developer.apple.com

 

 

이 메서드는 직접 호출하지 말라고 한다. 실제로 dismiss를 입력 후 .을 눌러보면 callAsFunction()이라고 제안사항에 나오는 것을 볼 수 있다.

 

 

인스턴스를 메서드처럼 호출한다- 의미인 것 같은데 SwiftUI가 그렇게 해준다는 설명 외에는 큰 설명이 있지 않다.

 

이 내용은 callAsFunction() 페이지 하단의 Methods with Special Names 링크에서 설명해준다고 한다.

 

 

Documentation

 

docs.swift.org

 

클래스, 구조체 또는 열거형 타입은 dynamicCallable에 설명된 대로 메서드를 정의하거나 callAsFunction을 정의함으로써 함수 호출 구문을 지원할 수 있다고 한다.

 

예시에 나온 것처럼, 어떤 역할을 하는 callAsFunction을 가진 구조체를 정의하고, 이를 변수에 담으면, 변수를 메서드처럼 호출하거나 인자를 전달함으로써 callAsFunction을 호출할 수 있는 것이다.

 

@Environment(\.dismiss)를 눌러서 들어가보면

 

SwiftUI의 EnvironmentValues의 확장안에 DismissAction 타입으로 선언되어 있음을 볼 수 있다.

 

documentation으로 적어놓은 내용에는 이 환경 값을 사용하여 현재 환경에 대한 DismissAction 인스턴스를 가져온다고 한다.

 

그렇다면 이제 DismissAction은 무엇인가?

 

 

dynamicCallable 예시처럼 callAsFunction을 가진 구조체임을 볼 수 있다.

 

즉, @Environment(\.dismiss) private var dismiss는 시트나 팝오버와 같은 모달 프레젠테이션을 해제하거나, NavigationStack에서 현재 View를 pop하거나, WindowGroup 또는 Window로 만든 창을 닫는 동작을 하는 DismissAction이라는 구조체를 타입으로 갖는 환경값 dismiss를 private var dismiss의 프로퍼티 래퍼로 사용한 것이다.

 

그리고 DismissAction은 callAsFunction을 정의했기 때문에 인스턴스를 함수처럼 호출할 수 있어 dismiss()로 사용함을 알 수 있었다.

 

돌고 돌았지만, 이렇게 구성된 dismiss() 메서드를 Button의 action에 넣어 배치한다면 HIG에서 말한 modal view를 해제할 수 있는 명확한 방법을 제시할 수 있을 것이다.

 

- 끝 -