-
VoiceOver를 통한 이벤트 전송iOS 2024. 1. 4. 19:30
안녕하세요. 그린입니다 🍏
이번 포스팅은 보이스오버 기능을 사용 시 보이스오버 이벤트를 전송하는 방법에 대해 학습해보겠습니다 🙋🏻
그전에 보이스오버에 대해 개요부터 어떻게 기본적으로 사용되는지 이전 포스팅에서 세밀하게 다뤄봤으니 그걸 먼저 보고 오시는걸 추천합니다!
이번 학습은 이전 포스팅의 연장으로 특정 부분에 대해 살펴봅니다ㅎㅎ
자 그럼 시작해볼까요~?
VoiceOver를 통한 이벤트 전송?
먼저 보이스오버를 통한 이벤트 전송이라는 타이틀이 이해가 되지 않을 수 있습니다.
어떤 상황에서 필요한지 말이죠.
그래서 아래와 같은 예시 코드가 있습니다.
import SwiftUI struct ContentView: View { var body: some View { Button( action: { // 액션 로직 }, label: { Text("Click") } ) .accessibilityLabel("보이스오버를 위한 클릭") } }
아주 단순히 Click이라는 텍스트를 가진 버튼이 하나 있고 보이스오버 라벨로 "보이스오버를 위한 클릭"이라고 붙였습니다.
그럼 보이스오버 기능을 켜고 해당 버튼에 한번 탭되거나 이동되어 포커싱이 되면 "보이스오버를 위한 클릭 버튼"이라고 읽겠죠?
여기까지는 다 알고 계신 내용일거에요!
근데, 버튼을 이중탭하여 실제 버튼을 동작시키면 구현해둔 action 로직을 수행하겠지만, 사실 시각장애인분들은 어떤 액션이 행해졌는지 알기가 어렵습니다.
물론 아래와 같이 Hint를 두어 이 버튼이 어떤것을 하는 버튼인지 눌렀을때 어떤 액션을 취하는지 적용할 수 있긴 합니다.
import SwiftUI struct ContentView: View { @State var clicked: Bool = false var body: some View { if clicked { Text("Title") } Button( action: { clicked.toggle() }, label: { Text("Click") } ) .accessibilityLabel("보이스오버를 위한 클릭") .accessibilityHint("이중 탭 시 Title 텍스트의 노출을 켜고 끕니다.") } }
이렇게 말이죠.
그럼 버튼에 포커싱이되면 순차적으로 "보이스오버를 위한 클릭 버튼"을 말하고 곧이어 "이중 탭 시 Title 텍스트의 노출을 켜고 끕니다."를 말해주게 됩니다.
그럼 이 버튼을 탭했을때 어떤 액션이 일어나는지 명확히 알 수 있죠.
그럼 Hint로만 적절히 잘 쓰면 될것 같은데 왜 이벤트 전송이라는것을 오늘 알아보려 했을까요?
위에서 살펴봤듯이, accessibilityHint는 사용자에게 컨트롤이 수행하는 작업에 대한 추가적인 설명을 제공하죠.
즉 새롭게 추가된 요소에 대한 설명만 제공할 수 있지만 동적인 상황에서는 사용하지 못합니다.
즉 예를들어, 한 버튼을 탭하면 다른 뷰를 레이아웃에 띄우는 상황을 가정해볼께요.
그럼 보이스오버 사용자 입장에서는 해당 버튼을 포커싱하면 "이중 탭 시 다른 뷰를 띄웁니다."를 Hint로 들은 후,
이중 탭한 후 실제 다른 뷰가 뜨게 되면 "다른 뷰가 노출되었습니다."의 메시지를 전달받는것이 가장 이상적일겁니다.
다른 뷰가 뜨는것 자체가 동적이기에 Hint에서 이 작업을 할 수 없습니다.
아직 뜨지도 않았기에 미리 해당 버튼에 포커싱되었을때 뜰 수 있습니다 정도로만 Hint로 설명할 수 있지, 노출이 완료되었다고는 못하니까요..!
그렇기에 앱의 상태가 동적으로 변경되는 상황에서는 사용자에게 그 변화를 명확히 알려주기 위해서는 오늘 배울 이벤트 전송을 통해 구현해줘야 합니다 😃
자 그럼 보이스오버를 통한 이벤트 전송이 어떤 느낌이고 왜 필요한지 살펴봤으니 본격적으로 알아보시죠!
UIAccessibility
우선 UIAccessibility부터 접근해봐야 합니다.
가장 기초이자 첫 시작이에요.
UIAccessibility 비공식 프로토콜은 앱의 사용자 인터페이스 요소에 대한 접근성 정보를 제공합니다.
VoiceOver와 같은 보조 앱들은 이 정보를 장애를 가진 사용자들에게 전달하여 앱을 사용하는 데 도움을 주죠.
표준 UIKit 컨트롤과 뷰는 UIAccessibility 메서드를 구현하고 기본적으로 보조 앱에 접근 가능합니다.
즉, 앱이 UIButton, UISegmentedControl, UITableView와 같은 표준 컨트롤과 뷰만 사용한다면,
기본 값이 불완전한 경우에만 앱 특정 세부 정보를 제공해야 합니다.
우리가 이전 포스팅에서 살펴봤던 label을 붙이거나 하는 작업등이죠!
이를 Interface Builder에서 이러한 값을 설정하거나 이 비공식 프로토콜의 속성을 설정하여 수행할 수 있습니다.
사용자 인터페이스 객체를 대표하는 UIAccessibilityElement 클래스 또한 UIAccessibility 비공식 프로토콜을 구현합니다.
만약 완전히 사용자 정의 UIView 서브클래스를 생성한다면, 이를 대표하기 위해 UIAccessibilityElement의 인스턴스를 생성해야 할 수도 있습니다.
이 경우, 접근성 요소의 속성을 올바르게 설정하고 반환하기 위해 모든 UIAccessibility 속성을 지원해야 합니다.
즉 한마디로 정리해보면 이렇습니다 🙋🏻
UIAccessiblility는 앱의 사용자 인터펯이스에 접근성 정보를 제공하는 프로토콜로, UIKit과 뷰는 이를 기본적으로 구현하지만 커스텀한 UIView는 UIAccessibilityElement 인스턴스를 생성해 접근성 요소를 나타냅니다.
결국 우리가 지금까지 쓰던 accessibilityLabel, accessibilityHint 등과 같은 접근성 관련 작업들이 모두 이 UIAccessibility에 소개되고 포함되어 있는것이죠!
공식문서만 봐도 위 말하는것에 대해 찾을 수 있습니다.
그럼 이제 다시 돌고 돌아 오늘 알아볼것을 본격적으로 살펴볼께요!
바로, UIAccessibility에서 Handling notifications 부분입니다.
Handling notifications
섹션 타이틀만 봐도 노티피케이션을 다루는것이니 이벤트 전송을 해줄것 같은 느낌이 마구 들지 않나요? 🥹
여기서 중요하게 볼 두가지가 있습니다.
Notification 구조체와 post 메서드입니다.
하나씩 살펴보시죠!
UIAccessibility.Notification
앱이 보낼 수 있는 접근성 알림 유형입니다.
struct Notification
아주 단순하죠.
이 타입에는 static 변수들이 있습니다.
아래 나열되는 해당 static 변수들이 이제 앞으로 나올 post 메서드에서 사용되게 되고, 해당 변수들은 각자에 맞는 상황에 따라 접근성 알림을 내보내는 방법이 다릅니다.
1️⃣ announcement 👉🏻 보조 앱에 공지 사항을 전달해야 할 때 앱이 게시하는 알림
2️⃣ layoutChanged 👉🏻 화면 레이아웃이 변경될 때 앱이 게시하는 알림
3️⃣ screenChanged 👉🏻 화면의 대부분을 차지하는 새 뷰가 나타날 떄 앱이 게시하는 알림
4️⃣ pageScrolled 👉🏻 스크롤 작업이 완료되면 앱이 게시하는 알림
5️⃣ pauseAssitiveTechnology 👉🏻 보조 앱의 작업을 일시적으로 일시 중지하는 알림
6️⃣ resumeAssistiveTechnology 👉🏻 보조 앱의 작동을 일시적으로 재개하는 알림
즉 이 다양한 변수들을 상황에 맞게 골라 이벤트 전송을 해줘야하죠.
그럼 이 다양한 이벤트 전송을 가능하도록 하는 post 메서드에 대해 알아보겠습니다!
post(notification: UIAccessibility.Notification, argument: Any?)
보조 앱에 알림을 게시하는 메서드입니다.
static func post( notification: UIAccessibility.Notification, argument: Any? )
이렇게 UIAccessibility의 static 메서드로 정의되어 있어 쉽게 사용할 수 있습니다.
두개의 매개변수를 받는데요.
notification은 위에서 살펴본 Notification 타입의 게시할 알림을 받습니다.
argument는 Any? 타입으로 알림에 지정된 인수이죠.
딱히 명시되어 있지 않으면 패스합니다.
자 그럼 개념들을 알아봤으니 실제 코드에서 어떻게 사용되는지 한번 볼까요?
실제 사용해보기
바로 코드로 보시죠!
import SwiftUI struct ContentView: View { @State var clicked: Bool = false var body: some View { if clicked { Text("Title") } Button( action: { clicked.toggle() 🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻 UIAccessibility.post( notification: .announcement, argument: "Title 텍스트의 노출 여부가 변경되었습니다." ) }, label: { Text("Click") } ) .accessibilityLabel("보이스오버를 위한 클릭") .accessibilityHint("이중 탭 시 Title 텍스트의 노출을 켜고 끕니다.") } }
이렇게 사용할 수 있어요.
버튼의 action에 실제 clicked가 변한 후 변했다는걸 보이스오버로 알려주기 위해 post 메서드를 이용합니다.
notification 값으로는 화면 전환등이 아닌 단순히 공지를 해주는것이기에 announcemnet를 사용해요.
그리고 어떤 정보를 전달할지 자유롭게 구성해주게 됩니다.
그럼 이제 보이스오버 기능을 사용한다면 전체적으로 아래와 같은 흐름을 가질거에요!
1️⃣ 버튼에 포커싱이 되면 "보이스오버를 위한 클릭" + "이중 탭 시 Title 텍스트의 노출을 켜고 끕니다." 음성 안내
2️⃣ 버튼 이중 탭 시 Text 노출 여부가 변경되며 "Title 텍스트의 노출 여부가 변경되었습니다." 음성 안내
이렇게 조금 더 친절한 보이스오버를 제공할 수 있습니다!
layoutChanged로 변경한다면 해당 상황에서는 보이스오버 음성을 안내하지 않습니다.
왜냐면 해당 버튼 동작으로 레이아웃이 변경되거나 하는 부분이 없으니까요!
그렇기에 상황에 맞는 적절한 Notification 값을 선택해야합니다.
하나 더 🙋🏻
위 코드로 동작 시 post로 이벤트 전송한 음성 안내가 나타나지 않는데요.
이 경우는 동시에 일어나기에 해당 post의 안내가 묻히고 "보이스오버를 위한 클릭" 포커싱 음성 안내가 나타납니다.
그 이유는 화면 리더와 앱 간의 상호작용 타이밍에 대한 문제로 보입니다.
화면 리더는 버튼이 클릭되면 자동으로 해당 버튼의 accessibilityLabel을 읽어줍니다.
그렇기에 "Title 텍스트의 노출 여부가 변경되었습니다."가 실행되기 전에 이미 "보이스오버를 위한 클릭" 안내가 나타나죠.
이걸 어떻게 해결해볼까요?
import SwiftUI struct ContentView: View { @State var clicked: Bool = false var body: some View { if clicked { Text("Title") } Button( action: { clicked.toggle() DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { UIAccessibility.post( notification: .announcement, argument: "Title 텍스트의 노출 여부가 변경되었습니다." ) } }, label: { Text("Click") } ) .accessibilityLabel("보이스오버를 위한 클릭") .accessibilityHint("이중 탭 시 Title 텍스트의 노출을 켜고 끕니다.") } }
DispatchQueue를 사용해 적절히 현재 작업을 잠시 지연시켜주면서 메인 스레드의 다음 루프에서 해당 작업을 수행하도록 하면 됩니다.
혹시 더 좋은 방법이 있다면 공유 부탁드려요 ㅎㅎ 🙏🏻
마무리
자 이렇게 보이스오버 사용 시 이벤트 전송을 통해 조금 더 친절한 앱 접근성을 가져가는 방법에 대해 알아봤습니다 🙋🏻
재밌지 않나요..!?
사실 보이스오버를 키고 사용하지 않기에 사용하기도 어렵고 적응이 잘안되지만 하나씩이라도 알아보면서 최대한 적용을 시키는것이 바람직해보입니다.
레퍼런스
'iOS' 카테고리의 다른 글
UIKit에서 ViewController간 데이터 전달 (95) 2024.01.22 Firebase App Distribution으로 앱 배포하기 (82) 2024.01.11 Get started with privacy manifests (feat. WWDC 2023) (84) 2023.12.28 STT를 이용해 음성을 텍스트로 변환해보자 🗣️ (95) 2023.12.26 UITest에서 accessibilityIdentifier 활용하기 (78) 2023.12.18