본문 바로가기

코딩의 유익함/스위프트(Swift)

SwiftUI - 커스텀 버튼 스타일 만들기(ButtonStyle, PrimitiveButtonStyle)

그림) 커스텀 버튼 스타일

SwiftUI에서 제공하는 버튼(Button) 스타일은 2가지가 있습니다. 바로 BorderlessButtonStyle과 PlainButtonStyle인데요. 그럼 이 2가지 외의 스타일은 만들 수 없는 것일까요? 아닙니다.

 

SwiftUI에서는 친절(?)하게 이 2가지 외에 우리가 직접 버튼 스타일을 만들 수 있도록 하고 있습니다. 그럼 어떻게 만들어야 하는지 살펴보기 전에 기존 스타일들을 어떻게 적용했었는지 살펴볼게요.

 

그림) 커스텀 버튼 스타일(1) 빨간 박스를 보시면, buttonStyle 수식어를 적용하면서 위에서 말씀드렸던 2가지의 스타일을 인자로 전달하고 있습니다. 즉 우리가 원하는 버튼 스타일을 만들게 되면 아래와 같이 사용하게 됩니다.

 

그렇다면 buttonStyle 수식어를 사용해서 우리의 커스텀 스타일을 적용해야 한다는 것인데요. 그렇다면 buttonStyle 수식어가 어떤 것을 요구하는지, 인자로 어떤 값을 보내야 하는 것인지 살펴보아야 합니다.

 

그림) 커스텀 버튼 스타일(1)

1.  buttonStyle 수식어

 

public func buttonStyle<S>(_ style: S) -> some View where S : ButtonStyle
public func buttonStyle<S>(_ style: S) -> some View where S : PrimitiveButtonStyle

수식어 buttonStyle의 정의를 찾아보면 위와 같이 2가지로 정의되어 있는 것을 볼 수 있는데요. 반환형은 some View이고, 인자는 ButtonStyle이나, PrimitiveButtonStyle 프로토콜을 준수해야 합니다.

 

2. ButtonStyle 프로토콜

 

그림) 커스텀 버튼 스타일(2)

위 그림에서 ButtonStyle 프로토콜의 정의입니다. 이를 준수하기 위해서는 함수 makeBody 하나만 구현을 해주면 되네요. 그런데 생긴 모양이 너무 어려워 보이지만, 하나씩 살펴보겠습니다.

 

먼저 반환형을 살펴보면 Self.Body인데, 그 위에 associatedtype으로 정의가 되어 있네요. 어떤 타입인지는 정해지지 않았지만 View 프로토콜을 준수하는 타입으로 정해야 한다는 것입니다.

 

쉽게 이해하려면 그냥 뷰를 반환한다고 생각해도 될 것 같습니다. 이제 configuration 매개변수를 살펴보면, 타입을 Self.Configuration으로 지정하고 있는데요. 이 타입은 밑에 보면 typealias로 지정하고 있네요.

 

configuration은 ButtonStyleConfiguration 타입인 것인데요. 그렇다면 이 타입의 정의도 살펴봐야겠네요.

 

그림) 커스텀 버튼 스타일(3)

Label 프로퍼티는 View 프로토콜을 채택하여 body 프로퍼티를 구현해야 합니다. 그런데 Body의 타입이 Never이네요. 즉 반환 값이 없다는 의미이므로 구현하지 않아도 무방한 상황이네요.(View 프로토콜 참조)

 

label 프로퍼티는 위에서 정의한 Label을 타입으로 하고 있네요. 결과적으로 뷰 프로토콜을 구현하고 있는 것이나 마찬가지이기 때문에, 이 프로퍼티를 통해서 우리는 사용할 커스텀 스타일의 버튼을 만들 수 있습니다.

 

isPressed는 Bool 타입입니다. 단어 그대로 버튼이 눌렸는지, 아닌지를 판단하는 프로퍼티이며, 눌렸을 때는 true를, 아닐 때는 false를 반환하기 때문에 다양한 기능으로 사용할 수 있습니다.

 

다시  그림) 커스텀 버튼 스타일(2)을 보겠습니다. makeBody를 구현하려고 하는데. Configuration을 타입을 알고 나니 매우 익숙한 구조네요. Configuration타입의 lable 프로퍼티를 이용하여 수식어를 적용하면 되겠군요.

 

그림) 커스텀 버튼 스타일(4)

ContentView의 body에 버튼을 하나 만들고. buttonStyle에 직접 만든 MyButtonStyle을 전달하였는데요. 바로 아래에 구현된 모습은 configuration.label을 대상으로 수식어를 적용한 것이 프리뷰에 그대로 드러났네요.

 

sacleEffect에서는 위에서 확인했던 isPressed를 활용하고 있는데요. 이 값이 true, 즉 눌렸다면 0.5의 크기를, false 라면 본래의 크기를 유지하거나 돌아갑니다. 이렇게 눌렀을 때의 효과를 줄 수 있는 것이지요.

 

그런데 아쉽게도 이것이 전부입니다. 버튼을 눌렀을 때의 효과만 줄 수 있지요. 그 외 다른 효과를 주고 싶다면 PrimitiveButtonStyle 프로토콜을 채택해야 합니다. 지금부터 살펴보겠습니다.

 

3. PrimitiveButtonStyle 프로토콜

 

이 프로토콜의 구조 역시 위에서 살펴본 ButtonStyle과 같습니다. 다른 점이 하나 있다면 configuration 타입이 PrimitiveButtonStyleConfiguration이라는 것입니다. 이것을 잠시 살펴보겠습니다.

 

그림) 커스텀 버튼 스타일(5)

PrimitiveButtonStyleConfiguration 프로토콜 역시 ButtonStyleConfiguration 프로토콜과 매우 유사합니다. 다른 점이 있다면 isPressed 프로퍼티가 존재하지 않고 대신 trigger()이라는 함수가 있네요.

 

이 trigger 함수는 버튼 액션 효과의 수행 조건이나 시점을 지정해줄 수 있습니다. isPressed처럼 단순하게 누르는 것 외에 다양한 모션이나 효과를 넣고 싶을 때 사용합니다.

 

수행조건과 시점은 "0.7초간 누르고 있다면"으로 설정하고 효과는 버튼의 모양이 변경되도록 설정해보겠습니다. 즉 버튼을 0.7초 이상 떼지 않고 누르고 있을 시 버튼의 모양을 변경시키는 것입니다.

 

수행조건과 시점을 지정한 내용을 코드로 나타내면 아래의 그림과 같으며, 옆에 PreView에 나타난 버튼을 누르면 버튼이 원래 크기의 0.7만큼 작아지다가 버튼을 떼면 다시 커지는 것을 확인할 수 있습니다.

그림) 커스텀 버튼 스타일(6)

위 그림에 빨간 박스의 코드가 핵심이기 때문에 순서대로 살펴보겠습니다. 먼저 isPressed 프로퍼티가 없기 때문에 하나 만들어주어야 합니다. 눌렀을 때 true, 누르지 않았을 때 false의 값을 가질 수 있도록 Bool 타입으로 합니다.

 

@GestureState는 제스처가 동작하는 순간에 변화했다가 다시 초기 값으로 돌아오는 속성인데요. @State와 유사한 속성으로 현재 상태(눌렀는지)를 알기 위함과 동시에, 필요한 함수의 인자 값으로 들어가기 위해 설정합니다.

 

updating 함수는 Gesture 프로토콜에 존재하는 함수입니다. LongPressGesture가 Gesture를 구현하고 있으며, 생성 시 minimumDuration은 최소 얼마의 시간 동안 제스처를 실행할 것인지를 지정합니다.

 

updating의 첫 번째 인자로는 GestureState속성의 변수 즉 위에 isPressed를 전달하고, 두 번째 인자로는 body(클로저)인데, 그 모습은 3개의 인자를 가지며, Void를 반환하는 모습을 하고 있습니다. 

 

위에서 클로저의 인자의 형태를 다 설명하기는 아직 저의 이해가 조금 부족하네요. 간단하게 첫 번째는 value, 두 번째는 state, 세 번째는 _로 설정하고 리턴 값은 Void이므로 지정하지 않아도 됩니다.

 

첫 번째 인자 value는 버튼을 누르는지 여부를 가지고 있으며, 이 값을 state에 넘겨줍니다. state에 저장되면 isPressed에 저장되도록 구현되어 있습니다. 그 반환 값으로 다시 onEnded를 호출합니다.

 

이 함수 역시 인자로 클로저(하나의 인자를 가지며 반환 값은 Void)를 받는데요. 결과적으로 trigger() 함수를 호출하도록 구현했습니다. 결국  longPress에 이 모든 행위를 담았다고 할 수 있습니다.

 

configuration.label은 우리가 지정하는 수식어의 대상이 되는 버튼(뷰)이 되는데요. 여기에 scaleEffect를 지정합니다. 버튼이 눌렸으면 0.7의 크기로, 눌리지 않았다면 본래의 크기로 합니다.

 

마지막으로 longPress를 gesture 수식어의 인자로 전달해주면 모든 등록이 끝났습니다. 이제 아래의 그림처럼 적용하면 됩니다. time은 지정이 가능한데, 2를 넣으면 눌렀을 시 2초간 0.7의 크기를 유지한다는 뜻입니다.

 

그림) 커스텀 버튼 스타일(7)

공부하면서 작성한 내용이라, 틀린 부분이 있다면 양해 부탁드립니다. 해당 내용은 "스윗한 SwiftUI"를 참고하면서 작성한 내용입니다.