티스토리 뷰

반응형

 

안녕하세요 :) Zedd입니다.

오늘은...VoiceOver 환경에서 UITapGestureRecognizer가 동작 안하는 이슈에 대해서 분석해보도록 하겠습니다. 

 

# 문제

왼쪽 그림의 버튼을 누르면 오른쪽 사진처럼 노란색 Bottom Sheet가 뜨는 아주 간단한 UI입니다.

BottomSheet ViewController의 구조는 아래와 같습니다.

Bottom Sheet이 떴을 때 뒤를 흐리게 만들어주는 DimmedView가 있고, 메인 View인 Bottom Sheet View가 있습니다. 

오른쪽 gif처럼 dimmedView를 tap했을 때 이 Bottom Sheet이 dimiss가 됐으면 좋겠으니, 

let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dimmedViewDidTap))
self.dimmedView.addGestureRecognizer(tapGesture)

@objc
func dimmedViewDidTap() {
    self.dismiss(animated: true, completion: nil)
}

dimmedView에 UITapGestureRecognizer를 추가해줍니다.

 

# VoiceOver

보이스오버 사용자도 이 Bottom Sheet을 닫을 수 있어야합니다.

self.dimmedView.isAccessibilityElement = true
self.dimmedView.accessibilityTraits = .button
self.dimmedView.accessibilityLabel = "닫기"

dimmedView에 적절한 Label과 Traits을 추가해줍니다.

dimmedView는 화면 전체를 채우고 있기 때문에 포커스가 화면 전체로 잡히게 됩니다. 

하지만, 오른쪽 gif에서 볼 수 있듯이 보이스오버에서의 Tap Gesture인 Double Tap을 해도 Bottom Sheet이 닫히지 않습니다. 

let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dimmedViewDidTap))
self.dimmedView.addGestureRecognizer(tapGesture)

@objc
func dimmedViewDidTap() {
    self.dismiss(animated: true, completion: nil)
}

즉, dimmedViewDidTap이 호출되지 않습니다.

 

# 분석

한가지 재밌는 실험을 해보겠습니다.

self.heightConstraints.constant = 400 ✅
UIView.animate(withDuration: 0.2) {
    self.view.layoutIfNeeded()
    self.dimmedView.alpha = 0.3
}

현재 Bottom Sheet을 띄울 때 (노란색) View Height을 400으로 주고있는데요. 

이걸 100으로 변경해보겠습니다.

왼쪽 400 / 오른쪽 100

아까랑 다 똑같고 height만 다르게 줬는데 갑자기 dimmedViewDidTap이 호출됩니다.

 

# 원인

accessibilityActivationPoint

activation point = 사용자가 요소를 두번 탭할 때 VoiceOver가 활성화 하는 특정 영역. 

이 프로퍼티의 기본값은 accessibility element’s frame의 midpoint(중간지점)입니다.

 

즉...

1. 현재 dimmedView의 frame.height이 734인데, height을 반이 넘는 400을 주게 됨.

2. 보이스오버 사용자가 Double Tap을 함

3. 사실상 iOS는(?..VoiceOver는?) dimmedView가 아니라 노란색 Bottom Sheet View를 Tap하게 됨.

4. dimmedViewDidTap은 불리지 않음.

이 시나리오입니다.

 

# 해결

이 상황을 해결해봅시다.

가장 먼저 생각나는 해결방법은 

왼쪽처럼 포커스되는 프레임을 전체로 주는게 아니라 오른쪽 사진처럼 Bottom Sheet 윗부분만 포커스 되도록 하면 될 것 같습니다. 

accessibilityFrame을 이용하면 되는데요.

self.dimmedView.accessibilityFrame = CGRect(x: 0,
                                            y: self.view.safeAreaTop,
                                            width: self.view.frame.width,
                                            height: self.dimmedView.frame.height - 400)

이렇게 해주면

이렇게 accessibilityFrame이 지정됩니다.

이 방법은 간단하지만,

Dynamic Type을 지원하는 앱 == Bottom Sheet 높이가 실시간으로 바뀔 수 있는 앱은

accessibilityFrame을 매번 다시 계산해줘야 할 것 같습니다.

그래도 accessibilityActivationPoint에서 봤듯이 잡힌 Frame의 midPoint이므로...

Dynamic Type을 지원하는데, 지금 잡은 Frame의 midPoint를 안넘으면...dismiss는 잘 동작은 하겠죠!?..

(하지만 포커스 Frame이 굉장히 이상해질것이므로 매번 다시 계산하는 것을 추천 👀) 

 

두번째 방법은 이 사태의 원인이었던 accessibilityActivationPoint값을 수정하는 것입니다.

self.dimmedView.accessibilityActivationPoint = CGPoint(x: 0, y: self.view.safeAreaTop)

대충 이런식으로 해주면..

잘 닫히는 것을 볼 수 있습니다.

 

어떤 BottomSheet은 닫히고 어떤 BottomSheet은 안닫히고...

원인을 도통 모르겠었는데 이런 허무한 결말이라니,,,!! 😂

 

+ ) 실험

🙋 : 아 그럼 만약에 DimmedView에도 TapGesture가 있고, Bottom Sheet View에도 TapGesture가 있어.

let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dimmedViewDidTap))
let tapGesture2 = UITapGestureRecognizer(target: self, action: #selector(bottomSheetViewDidTap))

self.dimmedView.addGestureRecognizer(tapGesture)
self.bottomSheetView.addGestureRecognizer(tapGesture2)


@objc
func dimmedViewDidTap() {
    self.dismiss(animated: true, completion: nil)
}

@objc
func bottomSheetViewDidTap() {
   print("Zedd")
}

이렇게! 그럼

1. 내가 DimmedView의 accessibilityFrame을 화면 전체로 두고

2. Bottom Sheet View의 높이를 DimmedView의 절반이 넘는 값을 주고

3. 보이스오버 Double Tap을 하면, bottomSheetViewDidTap이 불려?

쉽게 말해서 이 상태에서 노란색 View에도 Tap Gesture를 달았을 때, DimmedView에 포커스를 준 채로 Double Tap하면 어떻게 되냐?

🧑‍💻 : 놀랍게도 bottomSheetViewDidTap이 불립니다. ㅋㅋㅋㅋㅋ....ㅋㅋㅋ!!!!!!!!!! 

 

반응형