[UIButton] UIButton의 모든 것! (UIButtonConfiguration, ConfigurationUpdateHandler, UIAction)

2024. 7. 24. 18:04UIKit, SwiftUI, H.I.G

1️⃣ HIG (Human Interface Guidelines) 살펴보기 : Button이란?

Button은 앱에서 사용자가 어떤 특정한 동작이나 작업을 시작하는 방법을 제공하는 가장 기본 중에 기본이 되는 Component다.

가장 기본이 되는 Component이다 보니 HIG에서는 버튼을 사람들이 사용하기 쉬우며, 버튼의 목적이 직관적으로 드러나야 한다고 설명하고 있다. (최소 44 x 44 이상의 히트 영역을 권장 + 텍스트나 심볼을 (동시 또는 필요에 따라) 사용할 수 있다.)
일반적으로 버튼은 세 가지 속성(Style + Content + Role)을 결합해서 사용자에게 그 기능을 명확하게 전달하게 되는데, 자세한 내용은 아래와 같다.

Apple Human Inteface Guidelines buttons

1. Style : 버튼 사이즈, 색상, 모양(shape)을 기반으로 하는 시각적 요소 

  • 눈에 띄는 배경이 있는 버튼으로 시각적 요소를 강조하라. (use a button that has a visible background)
  • 뷰당 눈에 띄는 버튼의 수를 두 개 이하로 유지하는 것을 생각해라. (Consider keeping the number of visually prominent buttons to one or two per view.)
  • 여러 선택을 제공하기 위한 버튼은 사이즈로 구분하기보다 스타일로 구분해라. (Use style — not size — to visually distinguish the preferred choice among multiple options.)

2. Content : 텍스트나 아이콘과 같이 버튼의 목적을 전달하는 요소

  • 버튼 아이콘은 버튼의 액션과 연관되어 있게 할 것. (Using an icon when a button performs a familiar action that people associate with the icon.)
  • 아이콘보다 짧은 라벨을 사용하는 것이 더 명확하다고 생각이 들면, 아이콘 대신 텍스트를 사용할 것. (Using text when a short label communicates more clearly than an icon.)


3. Role : 시스템에 버튼의 기능적인 의미를 전달하는 요소

  • Normal : 의미 없음
  • Primary (button people are most likely to choose.) : 기본적으로 사용하는 버튼 (default), 사용자가 가장 선택 가능성이 높은 버튼에 primary 역할을 할당한다.
  • Cancel : 현재 작업을 취소하는 버튼
  • Destructive (Don’t assign the primary role) : 데이터를 지울 수도 있는 동작을 수행하는 버튼


이 외에도 iOS나 iPadOS의 추가적인 고려사항으로도 아래와 같은 것들이 있다.

  • 버튼의 label 아래에 subtitle을 포함하는 것은 유용한 세부 정보(usefult details)인 경우에만 포함시킬 것. -> 버튼의 기능을 설명하는 용도로 사용하지 말 것. (장바구니의 항목 수를 업데이트하는 텍스트의 경우가 유용한 세부 정보에 해당한다고 설명한다.)
  • 버튼 클릭 후 작업이 즉시 완료되지 않는 경우에는 애니메이션(= 아래와 같은 로딩 화면)이나 label의 변화(공유 -> 공유중)를 통한 피드백을 사용자에게 적절히 제공할 것. 

iOS Flatform Consideration 02. instantly complete되지 않는 작업의 경우 사용자에게 Feedback을 전달할 수 있어야 한다.

 

2️⃣ 기존 UIButton 사용 방법과 UIButtonConfiguration의 등장

그동안 가장 기본적이고 흔하게 사용한 버튼 생성 코드는 아래와 같을 것이다.
setImage, setTitle 등의 메서드로 버튼에 들어갈 이미지나 텍스트를 추가하고, addTarget 메서드에 action 함수를 연결해서 버튼 이벤트를 인식하며 사용했다.

private let exampleButton: UIButton = {
    let button = UIButton()
    button.setImage(UIImage(systemName: "play.fill"), for: .normal)
    button.tintColor = .white
    button.setTitle("Button", for: .normal)
    button.backgroundColor = .systemBlue
    button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
    return button
}()

@objc
func buttonTapped() {
    print("Button Clicked")
}

그러던 중, WWDC21에서 Meet the UIKit button System이라는 세션에서 iOS 15 버전부터 적용되는 새로운 UIButton 생성 방식, UIButtonConfiguration을 소개했다.

SwiftUI에 비해 부족했던 UIKit에서도 아래에서 보이는 것처럼 더 다양한 버튼의 스타일을 지원하고 (Make buttons more stylish),
버튼의 동적 업데이트 지원 (Dynamic type supported by default),
개발자가 원하는 버튼 스타일을 지정하는데 있어 더 다양한 사용자 정의 함수와 일관적인 경험 등을 제공하기 위해 새로 도입되었다.

그럼 지금부터 UIButtonConfiguration을 어떻게 사용해서 버튼을 만들 수 있는지 차근차근 알아가보자.

UIButtonConfiguration 01. UIButtonConfiguration이 지원하는 다양한 스타일과 사이즈

 

3️⃣ UIButtonConfiguration 사용법

💡 단, 기존 버튼 생성 방식과 Configuration을 사용한 생성 방식을 같이 사용했을 때, 기존 생성 방식의 우선순위가 더 높았다.


1) Configuration 객체 생성과 스타일 지정

위에서 봤던 Configuration의 다양한 스타일을 처음 Configuration 객체를 선언할 때 지정하고, UIButton의 configuration 파라미터로 객체를 넣어주기만 하면 된다.
기본 스타일은 대표적으로 plain, gray, tinted, filled와 같이 지정할 수 있으며,
이제 버튼 디자인을 하는 데 있어서는 configuration 객체가 지원하고 있는 여러 메서드들을 활용해서 해주게 될 것이다.

// Configuration 객체 생성
// .plain() .gray() .tinted() .filled() 스타일을 지정할 수 있다.
var config = UIButton.Configuration.plain()

// Configuration 객체를 사용해서 버튼 디자인
config.title = "Title"

// UIButton과 Configuration 객체 연동
let button = UIButton(configuration: config)

UIButtonConfiguration 02. UIButtonConfiguration Styles


2) Configuration Title, Image 사용

title과 subtitle로 버튼에 들어갈 text를 지정할 수 있다.
subtitle을 사용할 때는 위의 H.I.G에서 설명했던 것처럼 버튼의 용도를 설명하는 것이 아니라 유용한 세부 정보를 나타낼 때만 사용해야 한다.

config.title = "Title"
config.subtitle = "Subtitle"

attributedTitle, attributedSubtitle 속성은 NSAttributedString을 사용해 텍스트에 대한 커스텀 스타일을 지정하는 방식이다.
주로 버튼 텍스트의 폰트나 사이즈를 지정하거나, 텍스트 특정 부분에 대해서만 다른 속성을 주고 싶은 경우에 사용할 수 있다.

let attributedString = NSAttributedString(string: "Title", attributes: [.foregroundColor: UIColor.red])
config.attributedTitle = attributedString

titleAlignment를 사용해서 .center .leading. trailing의 정렬 상태를 지정할 수 있다. (이 부분은 title과 subtitle을 별도로 지정할 수 없었다.)

config.titleAlignment = .trailing

image로 버튼에 들어갈 이미지를 지정할 수도 있다.

config.image = UIImage(systemName: "play.fill")

imagePlacement 속성으로 버튼에서 이미지가 위치하는 공간을 지정할 수도 있다.
.top .bottom .leading .trailing 4가지의 위치를 지정 가능하다!

config.titleAlignment = .trailing


3) Metrics Adjustments : 버튼 내부의 간격

아래 보이는 그림처럼 버튼 내부의 콘텐츠를 둘러싸고 있는 여백을 지정하는 contentInsets,
Title과 Subtitle 사이의 간격을 지정하는 titlePadding, Image와 Title 사이의 간격을 지정하는 imagePadding으로 구성되어 있다.

contentInsets는 NSDirectionalEdgeInsets라는 객체를 이용해 top, leading, bottom, trailing의 값을 각각 지정하면 되고
그 외에 titlePadding이나 imagePadding 값은 CGFloat 타입으로 지정하면 되겠다.

config.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)
config.titlePadding = 10
config.imagePadding = 20

UIButtonConfiguration 03. Metrics Adjustments


4) Configuration 색상

Configuration에서 색상을 지정하는 방법으로는 기본적으로 baseBackgroundColorbaseForegroundColor 두 가지가 있다.
baseBackgroundColor는 버튼 배경의 기본 색상을 지정하고, baseForegroundColor는 버튼의 텍스트와 이미지의 기본 색상을 지정하는 속성이다.

이때 baseBackgroundColor 속성은 버튼의 스타일에 영향을 받게 된다는 점을 주의할 필요가 있다.
색상을 채우는 gray나 filled 스타일은 지정한 색상이 그대로 표출되지만, plain은 아무것도 없는 색상으로, tinted는 연하고 잔잔한 색상으로 표출되는 차이점을 아래에서 확인해 보자.

config.baseBackgroundColor = .yellow
config.baseForegroundColor = .blue

UIButtonConfiguration 04. Button Style별 적용되는 baseBackgroundColor의 차이

위의 두 가지 색상 말고도 background라는 별도의 배경 속성에서 다시 두 가지 색상을 추가할 수도 있다.
배경 색상을 지정하는 background.backgroundColor 속성과 모서리 색상을 지정하는 background.strokeColor 속성이다.

이때 지정하는 background는 버튼의 스타일에 따라 영향을 받지 않는다는 점이 특징이다.
즉, 위에서 plain, tinted, filled 스타일에 따라 적용되는 yellow 색상이 달랐다고 한다면, background 속성으로 직접 색상을 지정했을 때는 구체적인 색상을 정해줬기 때문에 영향을 받지 않게 된다는 것이다. (아래 05 그림 비교 참조)

또한, 구체적인 요소까지 들어가 지정한 backgroundColor는 config.baseBackgroundColor보다 우선순위를 가지게 되는 것도 확인할 수 있었다!

config.background.backgroundColor = .green
config.background.strokeColor = .red

UIButtonConfiguration 05. baseBackgroundColor와 background.backgroundColor의 적용 차이


5) Configuration 레이아웃

buttonSize 속성으로 버튼 사이즈를 지정할 수 있다.
UIButton.Configuration.Size 속성에는 large, medium, small, mini 총 4가지 사이즈를 지원하고 있다.

// .mini .small .medium .large 사이즈를 지원
config.buttonSize = .large

UIButtonConfiguration 06. UIButton.Configuration.Size 4가지 속성 비교

cornerStyle로 버튼의 모서리 스타일을 지정할 수 있다.
마찬가지로 small, medium, large, capsule 총 4가지의 모서리 스타일을 지원하고 있으니 설정하면 되겠다. (물론 직접 layer에 들어가서 cornerRadius를 지정해도 상관없다!)

config.cornerStyle = .capsule

UIButtonConfiguration 07. UIButton.Configuration.CornerStyle 4가지 속성 비교

지금까지 설명한 속성 외에도 showsActivityIndicator나 activityIndicatorColor, activityIndicatorStyle과 같은 메서드들로 Indicator를 표현할 수도 있고, 
버튼의 상태에 따라 시각적인 요소를 자동으로 반영할 수 있게 만드는 automaticallyUpdateForSelection 같은 속성도 있으니 추가로 아래 공식 문서에서 궁금하다면 더 알아보길 바란다.

 

UIButton.Configuration | Apple Developer Documentation

A configuration that specifies the appearance and behavior of a button and its contents.

developer.apple.com

 

4️⃣ ConfigurationUpdateHandler 사용해서 UIButton의 상태 처리 깔끔하게 하기

다시 Configuration이 도입된 이유를 되돌아보면,
3번 제목에서는 다양한 버튼 스타일을 지원하는 Configuration의 특징을 알아봤다고 한다면, 지금부터는 UIButton의 동적 업데이트 지원 (Dynamic type supported by default) 특징을 알아보고자 한다.

💡 ConfigurationUpdateHandlerButton의 상태(state)에 따라 구성(Configuration)을 변경시키는 클로저이다.

🤔 버튼의 상태에 따라 구성을 변경시킨다는 게 무슨 말이지?
기존에 button.setTitle("어쩌구", for: .normal) / button.setTitle("어쩌구", for: .highlighted)로 나눠서 각각 작성해줬던 코드를 생각해보자.
버튼이 normal 상태일 때와 highlighted 상태일 때 각각 타이틀을 설정하고 이미지를 설정하고 쭉쭉 나열하다보니, 상태별로 모아서 보기도 어렵고 코드도 너무 길어지고 등등의 문제점이 있었다는 것이다.
즉, 한 클로저 안에 상태에 따른 변화 사항을 모아둠으로써 버튼의 동적인 변화를 효율적으로 만들겠다는 의미다.


ConfigurationUpdateHandler를 어떻게 사용하는지 코드로 살펴보자.

아래 코드의 핵심은 버튼의 isHighlighted 상태 여부에 따라 삼항 연산자를 이용해 다른 이미지의 Configuration을 적용하는 것이다.
또한, subtitle의 내용 역시 지속적으로 업데이트해주기 위해 ConfigurationHandler에 추가해 준 부분도 확인할 수 있다.

addToCartButton.configurationUpdateHandler = { [unowned self] button in
    var config = button.configuration
    config?.image = button.isHighlighted
        ? UIImage(systemName: "cart.fill.badge.plus")
        : UIImage(systemName: "cart.badge.plus")
    config?.subtitle = self.itemQuantityDescription
    button.configuration = config
}

상태에 영향을 받지 않았던 subTitle은 아래 보이는 것처럼 itemQuantityDescription이라는 별도의 String 타입의 프로퍼티 값 변화에 영향을 받게 된다.

프로퍼티의 값이 변하면 didSet 구문이 수행되는데, 이때 setNeedsUpdateConfiguration()이라는 메서드가 호출된다.
해당 메서드는 위에서 정의했던 configurationUpdateHandler 클로저를 호출하는 역할로, Button에게 "너 Configuration을 지금 새로 업데이트할 필요가 있어"라고 알린다고 이해하면 되겠다.

private var itemQuantityDescription: String? {
    didSet {
        addToCartButton.setNeedsUpdateConfiguration()
    }
}

이처럼 ConfigurationUpdateHandler는 selected나 highlighted 된 상태에서 별도의 Configuration을 만들거나,
또는 특정 상황에서 Button의 수정된 Configuration을 적용해야 하는 경우에 Dynamic 하게 사용할 수 있다는 특징을 갖고 있다.

ConfigurationUpdateHandler 적용 화면!

 

5️⃣ UIAction 사용해서 버튼 액션 연동하기

위에까지는 Configuration에 관한 내용이었고, 지금부터는 iOS 14 이상부터 사용가능한 UIAction이라는 action 연동 방식을 설명해보겠다.

💡 UIAction클로저를 사용해 Control이 필요한 UIKit 요소들(UIButton 뿐만 아니라, UISwitch, SegmentedControl 등) 에게 이벤트가 발생했을 때 실행될 동작(Action)을 정의하는 데 사용되는 클래스이다.

-> 무엇보다 Objective-C 기반의 @objc 함수를 이제 더 이상 정의하지 않고, 직관적인 클로저를 이용해 동작을 정의할 수 있다는 점이 가장 큰 장점! (물론, 기존 addTarget 메서드와 중복으로 사용하는 것도 가능하다.)

기본적인 UIAction의 사용 방법은 두 가지이다.
하나는 addTarget과 마찬가지로 addAction을 정의해서 UIAction을 추가해 주는 방식,
또 다른 하나는 UIButton의 primaryAction 파라미터로 UIAction 클로저를 받아 바로 객체 선언과 함께 정의하는 방식.

let buttonTapped = UIAction { _ in
    print("UIAction: Button Tapped!")
}

addToCartButton.addAction(buttonTapped, for: .touchUpInside)
private let addToCartButton = UIButton(
    primaryAction: UIAction(handler: { _ in
        print("UIAction: Button Tapped!")
    }
))

 


UIAction은 객체 초기화 시에 받을 수 있는 파라미터도 많고, (handler 구문만 필수로 구현해야 했던 것!)
handler로 넘어와서 받을 수 있는 데이터(sebder, state, identifier, attribures, description 등등..)도 매우 다양하다 보니 필요에 따라 찾아보면 유연하게 코드를 작성할 수 있을 것으로 보인다!

 

UIAction | Apple Developer Documentation

A menu element that performs its action in a closure.

developer.apple.com