VoiceOver 환경에서 UITapGestureRecognizer가 동작 안하는 이슈
안녕하세요 :) 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으로 변경해보겠습니다.
아까랑 다 똑같고 height만 다르게 줬는데 갑자기 dimmedViewDidTap이 호출됩니다.
# 원인
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이 불립니다. ㅋㅋㅋㅋㅋ....ㅋㅋㅋ!!!!!!!!!!