[Swift] UISheetPresentationController 뿌시기
들어가기전에
개-하!
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
으로 설정을 하면, 하단의 전체 뷰를 덮는 식으로 모달이 노출됩니다.
별도의 값을 설정하지 않으면 적용되는 modalPresentationStyle
의 default 값은 automatic
입니다.
automatic
이 적용된 대부분의 view controller 는 pageSheet
스타일로 설정된다고해여. 물론 일부 system view controller 에는 다른 스타일이 적용될 수 있긴 하지만요 (더 많은 modal presentation 스타일을 알고 싶다면 UIModalPresentationStyle 페이지를 참고해주십셔).
여기까지 modalPresentationStyle
에 대해 간단히 알아봤는데, 그럼 해당 값을 pageSheet
나 formSheet
로 설정하면 어떤 일이 일어날까요??
🤔 : ??? 뭔 어떤일이 일어나,, 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 에서는 비활성화 됨.
detents
의 default value 는 large()
하나만을 갖는 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
등으로 잘 나옵니다.
마지막으로 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 가로 모드잖아여?
기본값은 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 에 있거나, prefersEdgeAttachedInCompactHeight
가 false
인 경우, 이 속성은 영향을 미치지 않습니다.
얘는,, 테스트해보기 귀찮으니까 스킵!
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
문서에 있는 거의 모든 내용을 다뤘지만 딱 두개 다루지 않았습니다. sourceView
랑 invalidateDetents()
인데여,,, 정말 이렇게 길게 썼는데도 아직도 남아있다는게 놀랍져? ㅋ,,ㅋ,ㅋ,
sourceView
를 다루지 않은 이유는 설명만 보면 대충 감이 오는데 어떻게 쓰는지 모르겠어서, invalidateDetents
는 iOS 16 Beta 라서,,?
invalidateDetents
는 그렇다치고, sourceView
는 더 알아보면 되지 않냐고여?
맞는 말이지만,,◠‿◠ 제 방의 전구가 나가버려서 눈이 너무 아프고,, 덥고,, 지치고,,, 힘들고,, 그렇기 때문에 샤따 내립니다…! 필요할때 찾아쓰죠 뭐…. 다른 분들 좋은 레퍼 찾으시면 댓글로 남겨주세여 (냅다 떠넘기기)
그럼 전 기절…!