Danny의 iOS 컨닝페이퍼
article thumbnail

[TIL #8] 23 / 03 / 30

오늘은 iOS 15에서 새로 도입된, UISheetPresentationController에 대해 간단히 알아보겠습니다.

 

참고 자료

UISheetpresentationcontroller

WWDC2021

WWDC는 꼭 한번 시청해 보세요.

 

 

UISheetPresentationController

UISheetPresentationController를 사용하기 위해서는 modalPresentationStyle의 설정 중,

오직 pageSheet, formSheet를 사용할 때, 구현이 가능합니다.

fullScreen과 같은 다른 이외의 값들은 nil 값을 갖게 돼서, 사용이 불가능합니다.

 

UISheetPresentationController의 기능을 간단히 설명하면,

Sheet의 높이 및 애니메이션 등 다양한 커스터 마이징을 가능하도록 도와줍니다.

 

 

시작

만약, 이와 같이 ModalVC를 띄어주게 된다면, 기본값인 pageSheet 모양으로 화면이 나타나게 될 거예요.

present 한 화면이 pageSheet이므로 UISheetPresentationController를 바로 적용할 수 있겠네요.

참고, modalPresentationStyle의 기본값은 automatic(보통 pageSheet)입니다.

class ModalViewController: UIViewController {}
func showModalVC() {
    let vc = ModalViewController()
    self.present(vc, animated: true)
}

 

이제 각 속성들을 차근차근 살펴보며, 기능들을 커스터 마이징을 해 봅시다.

 

 

detents

간단히 설명하면 Sheet이 멈추는 높이를 정해줄 수 있습니다. (멈춤쇠)

배열로 되어있어 여러 값을 넣어줄 수 있습니다.

 

detents 배열에 사용되는 값은 large, medium가 존재하고

이걸 통해 원하는 Sheet의 높이를 지정해 줄 수 있습니다.

참고로 반드시 1개 이상의 값은 필요하다고 하네요.

 

large - 전체 크기로 확장

medium - 절반 크기로 확장

func showModalVC() {
    let vc = ModalViewController()
    
    if let sheet = vc.sheetPresentationController {
        sheet.detents = [.medium(), .large()]
    }
    
    self.present(vc, animated: true)
}

 

 

 

selectedDetentIdentifier

다음으로 selectedDetentIdentifier는 가장 최근에 선택한 높이 값(Medium, Large)을 나타내준다고 하네요.

(기본값은 nil입니다)

 

공식문서 - Selecteddetentidentifier 를 보면 읽기 및 쓰기(get, set)가 가능한걸 보니

속성에 값을 넣어 사용해도 되겠네요.

 

이런 식으로 속성을 변경시켜 원하는 크기로 만들어 줄 수 있습니다.

ModalVC에서 두 개의 버튼을 만들어 동작하게 만들었습니다.

@objc func buttonHandler(_ sender: UIButton) {
    guard let sheet = self.sheetPresentationController else { return }
    
    switch sender.currentTitle {
    case "Large":
        sheet.selectedDetentIdentifier = .large
    case "Medium":
        sheet.selectedDetentIdentifier = .medium
    default: break
    }    
}

 

그런데 여기 제가 GIF로 안 올려서 그런데...

Large 및 Medium 버튼을 클릭하면, 그냥 애니메이션 효과도 없이 순식간에 위아래로 순식간에 이동하더라고요.

 

찾아보니, 이 문제를 해결하기 위한 메서드도 존재합니다.

바로, animateChanges입니다.

 

 

animateChanges

Sheet의 이동을 부드럽게 도와주는 메서드입니다.

 

사용방법은 그냥 animateChanges 클로저 내부에서 Sheet의 속성을 변경시켜 주면,

부드럽게 올라가는 애니메이션 효과가 추가 돼요!

@objc func buttonHandler(_ sender: UIButton) {
    guard let sheet = self.sheetPresentationController else { return }
    
    switch sender.currentTitle {
    case "Large":
        sheet.animateChanges {
            sheet.selectedDetentIdentifier = .large
        }
    case "Medium":
        sheet.animateChanges {
            sheet.selectedDetentIdentifier = .medium
        }
    default: break
    }
}

 

 

largestUndimmedDetentIdentifier

여기서, dim은 "흐리한"이란 뜻을 갖고 있습니다.

 

자... 대충 해석해 보면

"가장 큰 높이 식별자에 대해 흐리게 안 함?" 이런 느낌인가요? 패스...

 

아무튼 우린 detent(멈춤쇠) 식별자는 두 종류(Large, medium)가 있는 걸 알고 있습니다.

이걸 통해서 뭘 하는 것 같아 보이네요.

 

 일단 dim(흐려짐)효과를 모르니, 먼저 사진으로 비교를 해봅시다. ㅎ

이런식으로 present를 하게 되면, 안쪽 화면이 흐릿하게 변합니다. 이게 바로 dim효과입니다.

 

돌아와서 기능을 자세히 설명해보면,

largestUndimmedDetentIdentifier의 값이 지정한 detent 값 보다 클 때, dim(흐려지는) 처리를 하는 속성입니다.

 

조금 더 간단히 말하자면,

값이 클 때, 뷰 계층에서 상위뷰가 흐릿해지며, 클릭과 같은 상호작용이 불가능해집니다.

(상위뷰 == Sheet 아래 깔린 뷰컨, 이미지가 있는 뷰컨)

 

만약 상위뷰와 상호작용을 하려 한다면, Modal 뷰컨은 닫히게(pop) 될 거예요.

(예: present 후, '이미지 변환' 같은 버튼 클릭하면 버튼은 동작을 안 하고 Modal 뷰컨이 닫히게 돼요!)

 

그럼 언제 사용하는 거야?

아마도 제 생각엔 상위 뷰와 상호작용을 하고 싶을 때, 이 속성을 사용하면 될 것 같습니다.

 

그래서 사용은 어떻게 하는 거야?

설명하기가 생각보다 까다로워서 코드와 함께 설명해보겠습니다.

 

일단 코드에서, Sheet의 detent(멈춤 높이)는 medium 입니다.

그런데, 아래와 같이 largestUndimmedDetentIdentifier의 값을 medium으로 설정해 주고

서로 값을 비교 해보면, 두 값은 크지 않고 동일하므로 dim(흐려지는) 처리를 해주지 않게 되겠죠!?

func showModalVC() {
    let vc = ModalViewController()
    
    if let sheet = vc.sheetPresentationController {
        sheet.detents = [.medium()]

        sheet.largestUndimmedDetentIdentifier = .medium
    }
    
    self.present(vc, animated: true)
}

 

다시 간단히 말해, largestUndimmedDetentIdentifier 값과 detent의 크기를 비교해서

값이 같거나 작으면 dim(흐려지는)처리를 하지 않는다!

 

'largestUndimmedDetentIdentifier <= detent' ----- dim(흐림)처리 방지

 

즉, 상위 뷰(아래에 깔린 뷰)는 흐려지지 않고, 또한 상호 작용까지 가능해집니다!

 

한번 테스트로 "이미지 변환" 버튼을 눌러보겠습니다. 그림이 뒤집히네요.

상위뷰와도 상호작용이 되는걸 확인했습니다!!!

 

그럼 반대로, largestUndimmedDetentIdentifier 값을 large로 설정한다면?

 

값이 Sheet의 detent는 medium보다 크기 때문에, 당연히 상위 뷰는 흐려지고 상호작용도 안 되겠죠.

위의 사진에서 nil(왼쪽)과 같이 동작하게 될 겁니다.

 

'largestUndimmedDetentIdentifier > detent' ----- dim(흐림)처리

 

 

prefersGrabberVisible

잡아서 끌 수 있는 Grabber를 나타내줍니다.

기본값은 false입니다.

func showModalVC() {
    let vc = ModalViewController()
    
    if let sheet = vc.sheetPresentationController {
        sheet.detents = [.medium()]

        sheet.prefersGrabberVisible = true
    }
    
    self.present(vc, animated: true)
}

 

이건 아래 설명할 기능에서 꼭 필요한 기능입니다.

(스크롤 확장 기능을 제한해 놨을 때, Grabber를 잡아서 확장이 가능하게 만들어 줍니다)

 

 

prefersScrollingExpandsWhenScrolledToEdge

이건 바로 예시를 들어 설명드리겠습니다.

(아래 gif를 먼저 참고하면, 이해가 더 쉽습니다.)

 

ModalVC의 detents를 medium에서 large까지 확장이 가능하게 만들어주고

테이블뷰를 같이 사용한다고 가정을 해봅시다.

 

여기서 'detent의 확장'과 '테이블뷰' 두 가지 모두 스크롤 기능이 존재하는데,

작동 우선순위는 detent가 먼저 medium에서 large사이즈로 확장한 후, 테이블뷰의 스크롤 기능이 수행됩니다.

 

즉, '테이블뷰 스크롤 기능' 보다 'detent의 확장'이 먼저 일어납니다.

 

여기서 문제가 발생하게 되는데요.

제가 원하는 기능은 detent를 medium 높이에서도 테이블뷰의 스크롤 기능을 수행하는 동시에

사용자가 원한다면 detent를 large까지 확장시키는 기능까지 만들고 싶은데...

무조건 Sheet의 확장이 먼저 일어나게 되네요 ㅠ.ㅠ

 

이 문제를 해결하기 위해 prefersScrollingExpandsWhenScrolledToEdge 속성이 사용됩니다.

직역해 보면 '스크롤 시 스크롤 확장을 원하냐?" 이런 느낌인 것 같은데요.

 

간단히 설명하면 스크롤을 할 때, Sheet의 확장 여부를 Bool 값으로 나타내는 겁니다.

일단, 기본값은 true이며 false로 설정하면 '스크롤 시 Sheet의 확장'을 방지합니다.

 

다시 말해 만약, 'prefersScrollingExpandsWhenScrolledToEdge = false'로 설정한다면

스크롤 시, Sheet의 확장을 방지하여, medium 크기에서도 테이블뷰의 스크롤 기능을 사용하게 되죠!!!

 

대신 false로 설정 시, 완전히 Sheet의 스크롤 확장 기능을 막아 버리기 때문에,

large로 크기로 확장하기 위해선, 무조건 다른 확장 수단(Grabber)을 추가해줘야 합니다!

 

스크롤 확장을 방지하고 medium과 large를 동시에 사용할 때는

반드시 Grabber를 추가해 줘서 Grabber를 잡아서 확장을 가능하게 만들어 줍시다.

func showModalVC() {
    let vc = ModalViewController()
    
    if let sheet = vc.sheetPresentationController {
        sheet.detents = [.medium(), .large()]

        sheet.prefersGrabberVisible = true
        sheet.prefersScrollingExpandsWhenScrolledToEdge = false
    }
    
    self.present(vc, animated: true)
}

 

좌: true, 우: false

 

 

마무리

이외에도 모서리를 반경을 둥글게 하는 preferredCornerRadius 및 부착 여부? 등

다른 기능들도 더 있지만.. 별로 사용을 하지 않을 것 같아, 여까지 작성하겠습니다.

 

글 쓰다가 다 날려 먹어서... 너무 대충 쓴 느낌이 있지만,

나중에 까먹을 때를 대비해 이렇게 작성합니다.

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!