아티클

SwiftUI와 접근성 4부 - 토글버튼 만들기

엔비전스 접근성 2023-10-23 09:30:13

안녕하세요, 엔비전스 입니다. 어느덧 SwiftUI와 접근성도 4부가 되었습니다 :)

지난 시간에는 UIKit과 SwiftUI의 컴포넌트를 혼합하여 UIAccessibilityTraits.tabbar를 활용하는 방법에 관해 다뤘습니다. 이번 시간에는 SwiftUI에서 어떻게 토글버튼를 만드는지 알아보도록 하겠습니다.

토글버튼은 트레이트도 없는 것 같던데, 어떻게 구현할 수 있나요?

SwiftUI에는 각 네이티브 컨트롤 컴포넌트마다 스타일을 지정할 수 있습니다. 그 스타일 안에서, 제가 원하는대로 도형(Shape 프로토콜을 채택한 구조체)과 텍스트를 배치하여 새로운 컴포넌트 모양을 만들어낼 수 있습니다.

그 중 오늘 주로 다룰 것은 ToggleStyle에 관한 것입니다. SwiftUI에는 Toggle이라는 뷰가 있으며, 이 뷰에는 레이블과 함께 isOn이라는 부울값 상태를 바인딩 속성으로 전달하게 됩니다.

그런 다음, toggleStyle 속성자에 직접 만든 스타일을 적용시키면, 원하는 모양에, 접근성은 그대로인 네이티브 토글버튼를 만들 수 있게 됩니다. 우선, ToggleStyle을 먼저 만들어보겠습니다.


import SwiftUI

struct CustomSwitchStyle:ToggleStyle {

ToggleStyle 프로토콜을 채택하는 CustomSwitchStyle을 만듭니다.

    func makeBody(configuration: Configuration) -> some View {
        HStack {
            configuration.label
            CustomSwitchShape(isOn:configuration.$isOn)
        }.onTapGesture {
            configuration.isOn.toggle()
        }
    }
}

CustomSwitchStyle과 같은 뷰의 Style 관련 프로토콜들은 대체로 makeBody라는 뷰를 반환하는 메소드를 요구합니다. 여기서 하는 것은 오로지 보여지는 모양과 레이블 배치에 관한 것입니다. 인자에는 상태정보와 레이블정보를 담고 있는 configuration이 전달됩니다.

저는 HStack안에, 레이블을 먼저 배치하고, 제 커스텀 토글버튼 모양을 배치했습니다. 모양은 아래에서 설명할 겁니다. 기다려주세요. 그리고, 누루면, configuration.isOn 부울값이 반전되도록 해놨씁니다. 참 쉽죠?

struct CustomSwitchShape: View {

    @Binding var isOn:Bool

토글버튼 모양을 구현할 View를 만듭니다. isOn 상태에 따라 토글버튼의 똑딱이 위치가 바뀌어야 하기 때문에 isOn변수에 부울 바인딩값을 받습니다.

    var body: some View {
        HStack(alignment:.center){
            GeometryReader { proxy in
                ZStack (alignment:isOn ? .trailing : .leading){

토글버튼의 모양의 위치를 제어할 HStack과 ZStack을 만듭니다. isOn의 상태에 따라, true면 .trailling으로 설정하여 똑딱이가 오른쪽으로 가게, 그렇지 않다면 .leading으로 똑딱이 단추가 왼쪽으로 가게 설정합니다.

아까 전에, CustomSwitchStyle에서 전달한 isOn은 이런 역할을 하기위해 Shape로 전달되는 겁니다.

                    Capsule()
                        .strokeBorder(.primary.opacity(0.2),lineWidth:2)
                        .frame(width:proxy.size.width,
                        height:proxy.size.height*0.85)
                        .background { Capsule().fill(Color("switch-track").shadow(.inner(color:.black.opacity(0.5), radius: 6,x:2,y:3)))}

캡슐 도형을 만듭니다. 우리에게 익숙한 둥그랗고 길쭉한 그 모양 맞습니다. SwiftUI에서 제공하는 기본 도형 중 하나로, 캡슐 모양을 만들어줍니다.

위에 ZStack이 있으므로, 먼저 선언된 Capsule이 아래에 깔립니다. 높이는 GeometryReader에서 잡힌 크기의 85퍼센트, 길이는 넓이만큼 차지하게 할 겁니다.

background에는 이중으로 캡슐도형을 만들고, 색을 칠할 겁니다. Color("switch-track")은 제 프로젝트 Assets Catalog 안에 제가 정의해둔 색상입니다. 다른 색상을 사용해도 무방합니다. 그림자도 주도록합니다.

Circle().strokeBorder(.white.opacity(0.2),lineWidth:2).foregroundStyle(.accent)
                        .frame(width:proxy.size.height,height:proxy.size.height)
                        .overlay {
                            Image(systemName:isOn ? "poweron" : "poweroff").dynamicTypeSize(.medium).foregroundStyle(Color.white)
                        }.background{
                        Circle().fill(.accent).shadow(color:.black,radius: 2)}
                    }.grayscale(isOn ? 0 : 1)
                    .transition(.slide)
                    .animation(.easeIn, value: isOn)
            }
        }.frame(width:70,height:35)
    }
}

동그란 똑딱이 단추 부분입니다. 테두리는 흰색, 두꼐는 2정도로 얇지도 굵지도 않게 줬씁니다. foregroundStyle은 .accent로 줍니다.

.accent는 기본으로 제공되는 Color의 색상입니다. 아마 버튼 텍스트 색이라고 하면 이해가 빠르실겁니다. 동그란 똑딱이 단추 안에는 더 직관적으로 상태를 알 수 있도록, 전원 온/오프 아이콘을 넣어줬습니다.

역시나 아까 캡슐 모양에 했던 것 처럼 두겹으로 배경을 추가해줬습니다. 그리고 토글버튼가 눌릴때마다, isOn이 true이면 그레이스케일 필터를 없애고, false면 그레이스케일을 주어 회색으로 똑딱이가 표시되게끔 합니다.

마지막으로, 똑딱이 버튼이 움직일 때 부드럽게 움직이게 하기 위해 transition과 animation효과까지 넣었습니다. 이제 이것을 Toggle에 적용해봅시다.

    @State var isOn:Bool = false
    var body:some View {
    //...생략
        Toggle(isOn: $isOn,label: {
            Text("Light")
        }).toggleStyle(CustomSwitchStyle()).onChange(of: isOn, perform: { v in
            self.forceTheme = currentScheme == .dark ? .light : .dark
        })
    //...생략
    }

자 이제, 앱을 실행해보면, 아래처럼 훌륭한 커스텀 토글버튼가 완성되어 있습니다.

토글버튼 애니메이션 스크린샷, VoiceOver로 토글버튼를 조작하는 모습

저는 어두운 테마와 밝은 테마를 전환하는 것으로 이 스위치를 사용했습니다. 이것으로 토글버튼은 모두 다 만들었습니다.

어때요, 참. 쉽죠?

SwiftUI에서는 누구든지, 굳이 traits같은 것에 의존하지 않아도 이렇게 쉽게 기존에 쓰던 토글버튼을 커스터마이징할 수 있습니다.

그럼에도 불구하고

Apple은 iOS 17/iPadOS17에 AccessibilityTratis와 UIAccessibilityTraits에 각각, .isToggle과 .toggle을 주가했습니다. 반가운 소식이 아닐 수 없습니다.

위와같이 ToggleStyle을 사용하는 사례는 기존에 쓰던 토글버튼가 네이티브 토글버튼일 때만 가능하다는 단점이 있습니다. 기존에 완전히 커스텀으로 만들어뒀던 컴포넌트는 VoiceOver에서 토글 버튼으로 인식하게 할 방법이 없었죠. 토글버튼은 아래와 같이 사용할 수 있습니다.

@available(iOS 17.0, *)
struct CustomSwitchiOS17<Label:View>:View {
    @ViewBuilder var label:()->Label
    @Binding var isOn:Bool
    var body:some View {
        HStack {
            label()
            CustomSwitchShape(isOn: $isOn)
        }.accessibilityElement(children: .combine).accessibilityAddTraits(.isToggle).accessibilityValue(isOn ? "켜짐" : "꺼짐").onTapGesture {
            isOn.toggle()
        }
    }
}

스위치 모양은 재탕입니다. 하핫. 재탕이든 어니든 그건 중요하지 않고, 중요한 것은, 이렇게 텍스트와 도형만으로 만든 요소도 앞으로는 전환버튼으로 만들 수 있다는 겁니다. 다만 아직은 iOS17의 최신 기능이므로, 데코레이터나 조건문이 필요한 상태임을 인지하셔야합니다.

이번 시간은 여기까지이며 다음에 더 좋은 아티클로 찾아뵙도록 노력하겠습니다. 감사합니다.

댓글 0
댓글을 작성하려면 해주세요.