[Swift] UISheetPresentationController 뿌시기

물론 iOS 15 이상부터요 ◠‿◠

naljin
16 min readAug 19, 2022

들어가기전에

개-하!

UIKit 에서 기본적으로 다른 뷰를 modal 형식으로 띄울때 present(_:animated:completion:) 을 사용해왔을거예여

요런 식으로?

let modalViewController = ModalViewController()
self.present(modalViewController, animated: true)

이때 우리 어떤 모달 스타일로 뷰를 띄울건지 modalPresentationStyle 를 설정할 수 있는데요,

let modalViewController = ModalViewController()
modalViewController.modalPresentationStyle = .fullScreen
self.present(modalViewController, animated: true)

위와 같이 fullScreen 으로 설정을 하면, 하단의 전체 뷰를 덮는 식으로 모달이 노출됩니다.

별도의 값을 설정하지 않으면 적용되는 modalPresentationStyledefault 값은 automatic 입니다.

automatic 이 적용된 대부분의 view controller 는 pageSheet 스타일로 설정된다고해여. 물론 일부 system view controller 에는 다른 스타일이 적용될 수 있긴 하지만요 (더 많은 modal presentation 스타일을 알고 싶다면 UIModalPresentationStyle 페이지를 참고해주십셔).

여기까지 modalPresentationStyle 에 대해 간단히 알아봤는데, 그럼 해당 값을 pageSheetformSheet 로 설정하면 어떤 일이 일어날까요??

🤔 : ??? 뭔 어떤일이 일어나,, sheet 형식으로 보이게 되는거지,,

그것도 맞지만, 해당 뷰 컨트롤러의 var sheetPresentationController: UISheetPresentationController 프로퍼티에 값이 담기게 됩니다 (모달 스타일이 fullScreen 등으로 설정되면 해당 값은 nil )

🤔 : ??? UISheetPresentationController 가 뭐길래 내가 알아야하는건데? 어따 쓰는 애임?

UISheetPresentationController sheet의 모양과 동작을 관리하는 presentation controller 입니다. (UIPresentationController 를 상속받고 있어여)

아래와 같이 sheetPresentationController 에 접근 후 UISheetPresentationController프로퍼티 세팅을 통해 동작과 모양을 정의할 수 있습니다.

class ModalViewController: UIViewController {
override func viewDidLoad() {
if let sheetPresentationController = sheetPresentationController {
sheetPresentationController.detents = [.medium(), .large()]
}
}
}

이 값들을 잘 알고 조지면 half sheet -> full sheet 를 설정할 수 있고

modal 상단에 grabber 가 보이게 할 수도 있어여

다.. 다 좋은데….!! 중요한건 iOS 15 이상부터 사용 가넝이라는 점 ㅋㅋ..ㅋㅎㅋㅎ.ㅋ.. 미니멈 타겟 낮은 프로젝트는 언제나 그랬듯 한땀 한땀 가내수공업 하십셔 (물론 제 얘기임 ◠‿◠)

참고로 SwiftUI 에서도 위에서 말한 half Sheet 나 drag indicator 를 지원하는 등의 instance method 가 나오긴했지만,,

func presentationDetents(Set) -> some View

func presentationDragIndicator(Visibility) -> some View

얘네도 iOS 16+ Beta 부터 가능하다는 점..ㅋㅋㅎ.ㅋ.ㅎㅋ.ㅎ…

튼 눈물은 뒤로하고,, UISheetPresentationController 의 property 에 대해 알아보러 갑시다. 용어라도 알아두면 뭐 나중에 UIKit 이든 스유든 쓸 일이 있지 않겠어여? 그럼 ㄱㄱ

높이 지정

detents

우선 detent 의 뜻은 “멈춤쇠”라고 하네여?

어느날 운명처럼 영어 잘하게 해주세요 아멘

요 프로퍼티를 통해 sheet에 다양한 크기 구성을 추가할 수 있습니다.

var detents: [UISheetPresentationController.Detent] { get set }

이 안에 들어갈 수 있는 Detent 는 현재 두가지 유형이 있는데요,

  • large()전체 높이 로 system detent 를 생성
  • medium()— 스크린의 절반에 가까운 높이의 system detent 생성. compact height 에서는 비활성화 됨.

detentsdefault valuelarge() 하나만을 갖는 array 입니다. 그래서 아무것도 설정을 안했을때 전체 화면 길이의 sheet 가 뜬거겠죠? detents 에 할당되는 array 에는 최소한 한개 이상의 element 가 있어야합니다.

없으면 어떻게 되냐구여?

sheetPresentationController.detents = []

터지는거져 뭐

해당 값을 세팅할때는 짧은 길이부터 높은 길이로 지정을 합니다.

sheetPresentationController.detents = [.medium, .large()]

근데 높은 길이부터 짧은 길이로 설정해도 UI 상 어색하긴 하지만 잘 뜨긴 해요.

sheetPresentationController.detents = [.large, .medium()]

번외로 제가 이걸 테스트하면서 [.large()] / [.medium()] / [.medium(), .large()] / [.large(), .medium()] 네가지 케이스를 해봤거든여?

근데 [.large()] , [.medium(), .large()] 요 두가지 케이스에서만 detent 가 large 로 노출될 때 아래 깔려있는 뷰가 슉 하고 작아지더라고요? 흠냐리,,

뒤에 있는 뷰가 좀 작아지는 상황

selectedDetentIdentifier

가장 최근 선택된 detent 의 identifier 입니다.

전 프로퍼티 이름만 보고 “음 ~~ 현재 어떤 detent 에 위치해 있냐는거지?? 프린트 찍으면 뭐 medium 이나 large 가 나오군~~” 했거든요??

웬걸;; nil 이 나오는거예여??;; 역시 애플말은 끝까지;;

설명을 차분히 읽어보면 default 값이 nil 이라고 하네여. 이는 즉 sheet 가 detents 에 정의된 가장 작은 값으로 표시된다는걸 의미한다고 해여.

The default value is nil, which means the sheet displays at the smallest detent you specify in detents.

흠,,, 근데 아까 제가 [.large(), .medium()] 순으로 detents 를 설정했을때, 처음 보이는 detent 가 large 였잖아여?

이 상태로 selectedDetentIdentifier 를 print 해보면 nil 이 나온단 말이죠,,?

print(sheetPresentationController.selectedDetentIdentifier) // nil

아니 nil 이 나온다는건 smallest detent 를 표시한다는 걸 의미한다매,, 애플아,,??!!! 후,, 이 부분은 의문으로 남겨두기로 하고 일단 넘어가져,,

처음으로 sheet 가 뜬 상태에서 selectedDetentIdentifier 를 확인하면 nil 이 나오지만, 드래그 등으로 detent 를 조정한 후 다시 해당 값을 확인해보면 large, medium 등으로 잘 나옵니다.

gif 가 지연되어서 보이긴 하는데 님들이 생각하시는대로 뜨는 상황 맞음여

마지막으로 selectedDetentIdentifier 값 조정을 통해 detent 조절도 가능하긴한데,, 애니메이션이 매끄럽진 않져? 이 부분은 하단 애니메이션 부분에서 다시 보도록 합시다

User interaction 관리

largestUndimmedDetentIdentifier

🤔 ??? 이름이 왜 이리 길어??

자자 한글로 끊어 읽어봅시다.

dim 되지 않는 / 가장 큰 / detent / 식별자

네, sheet 하단의 뷰dim 처리하지 않가장 큰 detent 를 정의합니다.

기본값은 nil 으로, 모든 detent 에 대해 sheet 하단의 뷰dim 처리합니다.

ㅎ 그냥 gif 를 보면 훨씬 이해가 빠를텐데여,

sheet 가 올라가면서 하단의 뷰는 검정색 opacity 깔리면서 dim 처리된게 보이시나여? detent 가 medium 이든, large 든 상관없이요!

또한 sheet 의 바깥 영역에 있는 하단 뷰의 버튼을 눌러도 그에 대한 액션이 수행되는게 아니라, sheet 가 사라집니다.

하지만 largestUndimmedDetentIdentifier 을 설정한다면?

sheetPresentationController.largestUndimmedDetentIdentifier = .medium

하단의 뷰를 dim 처리하지 않는 가장 큰 detent => medium 으로 설정했습니다.

이로 인해 detent 가 medium 일때에는 하단 뷰는 dim 처리 되지 않습니다.

dim 처리 되지 않은 영역은 유저 인터렉션에 반응할 수 있고, 사용자의 nonmodal 경험이 가능해집니다.

prefersScrollingExpandsWhenScrolledToEdge

🤔 ??? 얼씨구 얘는 이름이 더 기네??

Scrolling 이 / Edge 로 / 스크롤 될 때 / (detent를) 확장하는 것을 / 선호하냐

그러니까,,!! tableView 같이 스크롤이 가능한 뷰가 있고 해볼게여. 근데 얘가 medium detent 안에 담겨있어여

이 상태에서 사용자의 스와이프 제스처가 sheet 를 더 큰 detent 로 확장시킬건지 vs. tableView 의 내용을 스크롤 시킬건지를 결정합니다.

기본값true 로, sheet가 선택한 detent identifier 보다 더 큰 detent 로 확장할 수 있는 경우, sheet 를 위로 스크롤 하면 내용을 스크롤하는 대신 detent 가 커집니다. sheet 가 최대 detent 에 도달하면 스크롤이 시작됩니다.

스크롤 제스처가 sheet 를 확장하지 않도록 하려면 이 값을 false 로 설정합니다.

애초에 scroll 되지 않는 영역에 대해서는 해당 값이 상관이 없기 때문에, grabber 나 일반 UIView 쪽을 잡고 위로 스크롤하면 detent 가 커지는 것을 볼 수 있습니다.

모양 관리

prefersGrabberVisible

딱 봐도 머.. 알겠져?

sheet 의 맨 위에 grabber 를 표시할지 여부를 결정하는 bool 값입니다.

없음 / 있음

grabber는 sheet 의 크기를 조정할 수 있음을 나타내는 시각적 Affordance (어떤 행동을 유도한다는 것) 로, sheet 의 크기를 조정할 수 있는 것이 분명하지 않거나 시트가 interactively 하게 해제되지 않을 때 유용할 수 있습니다.

기본값은 false 이며, true 로 설정되면 시스템이 표준 시스템 정의(system-defined) 위치에 grabber 를 표시합니다.

해당 값이 true 더라도, 시스템은 compact-height size class 에서 sheet 가 full screen 일때, 또는 다른 sheet 가 그 위에 표시될 때 등, 필요할 때 적절히 grabber 를 자동으로 숨깁니다.

preferredCornerRadius

sheet 모서리의 모서리 반지름 (corner radius) 값입니다.

기본값은 nil 이며, 이 property 는 시트가 sheet stack 의 앞쪽에 있을 때만 적용됩니다.

근데 default value 가 nil 인데 기본적으로 어느정도 radius 가 들어가 있는게 신기하네여?

prefersEdgeAttachedInCompactHeight

compact-height size class 에서 sheet 화면 아래쪽 가장자리에 붙는지 여부를 결정하는 bool 값입니다.

compact-height 라 함은.. 웬만한 iPhone 가로 모드잖아여?

참고 — https://developer.apple.com/design/human-interface-guidelines/foundations/layout/

기본값은 false 로, sheet는 compact-height 에서 full screen 으로 설정됩니다.

compact-height size class 에서 대체 모양(alternate appearance)을 사용하려면 이 값을 true로 설정하여 sheet가 아래쪽 가장자리의 화면에만 부착 시킵니다.

widthFollowsPreferredContentSizeWhenEdgeAttached

sheet 의 너비가 뷰 컨트롤러의 preferred content size 와 일치하는지 여부를 결정하는 bool 값입니다.

기본값은 false 로, 시트의 width 는 container 의 safe area width 와 같습니다.

뷰 컨트롤러의 preferredContentSize를 사용하여 시트의 너비를 결정하려면 이 값을 true 로 설정합니다.

만약 sheet 가 compact-width, regular-height size class 에 있거나, prefersEdgeAttachedInCompactHeightfalse 인 경우, 이 속성은 영향을 미치지 않습니다.

얘는,, 테스트해보기 귀찮으니까 스킵!

delegate 관리

UISheetPresentationControllerDelegate

func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_:)

함수를 통해 detent 를 변경한 후 respond 할 수 있는 기회를 얻을 수 있습니다.

머 대충 이런식으로여?

또한 해당 delegate 는 UIAdaptivePresentationControllerDelegate 를 상속받기 때문에, 이곳에서 정의된 delegate 함수들도 사용 가능합니다.

Sheet 변화에 대한 애니메이션 효과

animateChanges(_:)

아까 아래코드처럼 detent 를 변경했을때 layout 변화가 뚝뚝 끊겼던걸 기억하시나여?

sheetPresentationController.selectedDetentIdentifier = .medium

우리가 원하는 스무스~한 애니메이션을 적용하기 위해서는 animateChanges(_:) 함수를 사용할 수 있습니다.

해당 함수의 block 안에서 sheet 의 property 를 변경하면, 해당 property 변화에 대해 애니메이션 효과를 적용합니다.

같은 코드를 animateChanges 안에 넣어주면?

sheetPresentationController.animateChanges {
sheetPresentationController.selectedDetentIdentifier = .medium
}

굿~

마무리

UISheetPresentationController 문서에 있는 거의 모든 내용을 다뤘지만 딱 두개 다루지 않았습니다. sourceViewinvalidateDetents() 인데여,,, 정말 이렇게 길게 썼는데도 아직도 남아있다는게 놀랍져? ㅋ,,ㅋ,ㅋ,

sourceView 를 다루지 않은 이유는 설명만 보면 대충 감이 오는데 어떻게 쓰는지 모르겠어서, invalidateDetents 는 iOS 16 Beta 라서,,?

invalidateDetents 는 그렇다치고, sourceView 는 더 알아보면 되지 않냐고여?

맞는 말이지만,,◠‿◠ 제 방의 전구가 나가버려서 눈이 너무 아프고,, 덥고,, 지치고,,, 힘들고,, 그렇기 때문에 샤따 내립니다…! 필요할때 찾아쓰죠 뭐…. 다른 분들 좋은 레퍼 찾으시면 댓글로 남겨주세여 (냅다 떠넘기기)

그럼 전 기절…!

출처

--

--

No responses yet