티스토리 뷰

728x90
반응형

내용

  • UISheetPresentationController 을 사용해서 바텀시트 만들기
  • 높이를 커스텀하여 내가 원하는 바텀시트의 높이를 설정해보자
  • 바텀시트의 둥글기, grabber의 유무에 대해서 설정해보자

WWDC21 Customize and resize sheets in UIKit 에서 소개된 UISheetPresentationController을 활용한 바텀시트에 대해서 알아보겠습니다.

1

개발자 문서를 살펴보겠습니다.

UISheetPresentationController | Apple Developer Documentation

iOS 15와 iPadOS 15부터 적용가능합니다. UISheetPresentationController 를 사용하면 뷰컨트롤러를 sheet 로 표현할 수 있습니다.

다음과 같이 sheetPresentationController 프로퍼티를 통해 구성할 수 있습니다. 해당 프로퍼티는 옵셔널 형태입니다. sheet 형식으로 보여줄 때는 필요가 없기 때문에 nil 이 됩니다.

// In a subclass of UIViewController, customize and present the sheet.
func showSheet() {
    let viewControllerToPresent = MyViewController()

    if let sheet = viewControllerToPresent.sheetPresentationController {
    // ✅ 다음 프로퍼티들은 찬찬히 알아가봅시다.
        sheet.detents = [.medium(), .large()]
        sheet.largestUndimmedDetentIdentifier = .medium  // nil 기본값
        sheet.prefersScrollingExpandsWhenScrolledToEdge = false  // true 기본값
        sheet.prefersEdgeAttachedInCompactHeight = true // false 기본값
        sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true // false 기본값
    }

    // ✅ sheet present.
    present(viewControllerToPresent, animated: true, completion: nil)
}

위의 코드를 통해 시트가 자연스럽게 리셋되는 높이인 detent 를 기준으로 시트의 크기를 정하게됩니다.

🧑‍🏭 개발자 문서에 나온 코드이니 순서대로 알아봅시다. 그만큼 중요하다는 거겠죠?

detent?

detent 라는 용어를 멈춤쇠라고 해석하기에 조금 부족함이 있다고 생각했습니다. 그러던 중 WWDC22 세션 "Build a productivity app for Apple Watch" 에서 듣게 되어서 발췌했습니다.

detent!

detent 는 기계적 용어로 움직일 만큼 충분한 힘이 가해질 때까지 무언가를 제자리에 고정시키는 메커니즘입니다. 예를 들어, 차 문을 열 때 안착되는 ‘정지’ 위치가 있습니다. 문을 조금 더 세게 밀어 또 다른 ‘정지’ 위치까지 더 열 수도 있습니다.

문을 닫으려면 ‘정지’에서 빼낼 수 있을 만큼 세게 당겨서 저항을 이겨내야 합니다. 그렇지 않으면 문은 ‘정지’ 위치로 돌아갑니다. 이것이 detent 입니다.

시스템 detent 는 large, medium 이 있습니다.

2

(출처: https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/detent)

아래에서 살펴보겠지만 custom(identifier:resolver:) 메서드를 사용해서 detent를 커스텀할 수 있습니다.

iPad 와 iPhone 의 sheet 는 다음과 같습니다.

3

(출처: https://developer.apple.com/videos/play/wwdc2021/10063/)

👉 다음은 user interaction 에 대한 매니징입니다.

4

👉 largestUndimmedDetentIdentifier

The largest detent that doesn’t dim the view underneath the sheet.

즉, sheet 아래의 뷰를 dim(흐리게) 만들지 않는 가장 큰 detent 를 지정할 수 있습니다.

기본값은 nil 이고, 이는 모든 detent 에서 dimming view 를 추가한다는 것을 의미합니다. 예를 들어, medium detent 에 dimming view 를 추가하지 않기 위해서 medium 으로 값을 설정할 수 있습니다.

예를 들어, 지도 앱을 살펴보겠습니다.(지도앱의 초기 detent 는 medium이 아니긴 합니다.)

이외에도 dimming view 와 관련된 중요한 점이 있습니다.

원래라면 dimming view 를 터치해서 sheet 를 없앨 수 있지만, dimming view 가 있지 않으면 유저와 상호작용 할 수 있는 nonmodal 경험을 제공합니다.

👉 prefersScrollingExpandsWhenScrolledToEdge

A Boolean value that determines whether scrolling expands the sheet to a larger detent.

스크롤이 더 큰 detent 로 확장하는지 여부를 결정합니다.

true 기본값. 즉, 스크롤할때 더 큰 detent 로 확장할 수 있다면 가장 큰 detent 로 확장되어 스크롤이 시작됩니다.

위에서 확인한 지도 앱처럼 바로 스크롤할때 더 큰 detent 로 확장되는 것을 보여줍니다.

👉 다음은 appearance 에 대한 매니징입니다.

6

👉 prefersGrabberVisible

A Boolean value that determines whether the sheet shows a grabber at the top.

기본값은 flase. sheet 위의 grabber 를 표시할지 여부를 결정합니다.

7

👉 prefersPageSizing

A Boolean value that indicates whether the sheet sizes itself for readable content.

iOS 17 이상부터 사용가능합니다.

기본값은 true 입니다. 즉, 기본값은 sheet 가 읽을 수 있는 너비를 따르는 UIModalPresentationStyle.pageSheet 동작을 사용함을 의미합니다.

값이 false 로 설정되면, sheet는 UIModalPresentationStyle.formSheet 동작을 사용합니다. 여기서 sheet 의 크기는 presented view controller 의 preferredContentSize 를 따릅니다.

👉prefersEdgeAttachedInCompactHeight

A Boolean value that determines whether the sheet attaches to the bottom edge of the screen in a compact-height size class.

아래 WWDC21 영상의 일부처럼 compact-height size(iPhone의 가로모드에 해당) 에서 sheet 가 화면의 bottom edge 에만 붙는지 여부를 결정합니다.
8

기본값은 false. 전체 화면 모양으로 설정됨을 의미합니다.

👉 widthFollowsPreferredContentSizeWhenEdgeAttached

A Boolean value that determines whether the sheet’s width matches its view controller’s preferred content size.

기본값은 false. sheet 의 너비가 container 의 safe area 와 동일함을 의미합니다.

true 로 설정하면 view controller 의 preferredContentSize 를 통해서 sheet 의 너비가 결정됩니다.

9

sheet 가 compact-width 와 regular-height size 인 경우(iPhone의 세로모드에 해당) 혹은 prefersEdgeAttachedInCompactHeight 가 false 인 경우에는 효과가 없습니다.

(아래는 HIG문서에서 확인할 수 있는 디바이스들의 사이즈 클래스입니다.)

10

(출처 : https://developer.apple.com/design/human-interface-guidelines/layout)

👉 preferredCornerRadius

The corner radius that the sheet attempts to present with.

기본값은 nil. 하지만, 어느정도 둥글기가 적용되어 있습니다.

sheet 가 sheet stack 의 맨 앞에 있는 경우에만 유효합니다.

👉 이번 글에서는 사용되지 않지만 그 외에도 있습니다.

11

✋ 만들어보자

자, 이제 만들어봅시다. 몇가지 목표를 가지고 적용해보겠습니다.

목표

  • detent 를 커스텀
  • corner radius 를 적용
  • 스크롤을 통해서 detent 조절(두 가지 이상의 Detent를 사용해보자)
  • grabber 보이지 않게 구현

Creating a custom detent

13mini 기준으로 436 라는 높이값을 가집니다.(사실, 이는 safe area bottom 34 를 포함한 값입니다.) 이 값은 기기별로 다릅니다.

12

이제, 원하는 높이의 custom detent 를 만들어봅시다.

custom(identifier:resolver)

custom(identifier:resolver:) | Apple Developer Documentation

iOS 16 이상부터 사용 가능합니다.

custom(identifier:resolver:)

// Creates a custom detent for a sheet by computing its value according to the properties of the provided context.

@MainActor
static func custom(
    identifier: UISheetPresentationController.Detent.Identifier? = nil,
    resolver: @escaping (_ context: UISheetPresentationControllerDetentResolutionContext) -> CGFloat?
) -> UISheetPresentationController.Detent
  • identifier : detent 의 식별자. 식별자를 지정하지 않으면 시스템에서 랜덤한 식별자가 생성됩니다.
  • resolver : 이 클로저에서 반환되는 값은 safe area 내의 높이입니다. 예를 들어 200 반환하면 시트가 가장자리에 닿을 때 safeAreaInsets.bottom 을 더한 값을 반환하고(13mini 기준 200+34), 혹은 시트가 띄어져있는 경우 200을 반환합니다.
    detent 가 비활성화되도록 지정하려면 nil 반환하면 됩니다.
    클로저가 외부 입력에 따라 달라지는 경우, sheet 에서 vaildateDetents() 를 호출하면 됩니다.
    클로저를 실행하는 동안 UISheetpresentationController 어떤 속성도 설정하면 안됩니다.
if #available(iOS 16.0, *) {
    let detentIdentifier = UISheetPresentationController.Detent.Identifier("customDetent")
    // identifier 매개변수는 옵셔널이기 때문에 채우지 않으면 시스템이 임의의 식별자를 설정합니다.
    let customDetent = UISheetPresentationController.Detent.custom(identifier: .init("customDetent")) { _ in
        // 13mini 기준 최종적으로 safeAreaInsets.bottom을 더한 634높이를 가집니다.
        return 600
    }

    if let sheet = tagSheet.sheetPresentationController {
         sheet.detents = [customDetent, .large()] // detent 설정
    }
}

이러한 기기별로 다른 detent 높이 값을 사용할 수는 없을까요?

detent 의 값을 가져와 이를 통해서 custom(identifier:resolver:) 메서드 안에서 활용하여 custom detent 를 구성할 수도 있습니다.

resolvedValue(in:)

iOS 16 이상부터 사용할 수 있습니다.

detent 값을 반환하고, 비활성화된 경우 nil 반환합니다. mediumlarge detent 값을 출력해보겠습니다.

let customDetent = UISheetPresentationController.Detent.custom(identifier: "customDetent") { context in
    print("medium: ",UISheetPresentationController.Detent.medium().resolvedValue(in: context))
    print("large: ", UISheetPresentationController.Detent.large().resolvedValue(in: context))
    return nil
}

// 13mini 기준
// medium:  Optional(402.08000000000004)
// large:  Optional(718.0)

구현해보자

let sheetVC = SheetVC()

// ✅ custom detent 생성
let detentIdentifier = UISheetPresentationController.Detent.Identifier("customDetent")
let customDetent = UISheetPresentationController.Detent.custom(identifier: detentIdentifier) { _ in
    // safe area bottom을 구하기 위한 선언.
    let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
    let safeAreaBottom = windowScene?.windows.first?.safeAreaInsets.bottom ?? 0    

    // ✅ 모든 기기에서 항상 높이가 600인 detent를 만들어낼 수 있다.
    return 600 - safeAreaBottom
}

if let sheet = sheetVC.sheetPresentationController {
    sheet.detents = [customDetent, .large()] // detent 설정
    sheet.preferredCornerRadius = 30 // 둥글기 수정

    // ✅ grabber를 보이지 않게 구현.(UI를 위해 이미지로 대체)
    // sheet.prefersGrabberVisible = false // 기본값

    // ✅ 스크롤 상황에서 최대 detent까지 확장하는 여부 결정.
    // sheet.prefersScrollingExpandsWhenScrolledToEdge = true // 기본값
}

// ✅ 기본값 automatic. 대부분의 뷰 컨트롤러의 경우 pageSheet 스타일에 매핑.
// sheetVC.modalPresentationStyle = .pageSheet
present(sheetVC, animated: true)
728x90
반응형
댓글
최근에 올라온 글
최근에 달린 댓글
글 보관함
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
링크
Total
Today
Yesterday