[WWDC-2021] Meet the UIKit button system

2021. 10. 6. 21:55iOS/WWDC

이 글은 WWDC 2021 영상을 정리하여 작성한 글입니다.

원본 영상은 링크를 참고하여 주세요.

Meet the UIKit button system

오늘은 iOS 15에서 새롭게 추가된 버튼 시스템에 대한 세션을 정리해보도록 하겠습니다. 

iOS 15의 UIKit에서는 버튼의 네 가지 스타일을 제공하고 있습니다.

이미 제공되고 있는 Plain 스타일과 함께, Gray, Tinted, Filled 스타일을 제공합니다. 그리고 동적으로 버튼의 스타일을 구성하는 것과 여러 줄의 텍스트를 입력할 수 있는 부분이 버튼 시스템에 내장되어 있습니다.

이러한 향상된 시스템의 시작점인 UIButtonConfiguration을 만나보도록 하겠습니다.

우리는 이 새롭게 제공된 filled 스타일을 버튼에 적용하여 간단하게 모서리가 둥근 버튼을 생성할 수 있습니다. 사용 방법은 스토리보드의 inspector로 지정할 수 있지만, 새롭게 버튼에 추가된 'configuration'을 통해 설정할 수 있습니다.

configuration 속성에 'filled' 속성을 적용시켜 주면 간단하게 구현해 줄 수가 있는 것이죠.

이러한 스타일 적인 부분 외에도 많은 기능들이 UIButtonConfiguration 안에 담겨 저 있습니다. 다음을 보시죠.

다음의 화면에서는 'Add to Cart'라는 버튼이 있습니다. 아주 기본적인 스타일로 구현된 버튼인데, 여기에 다양한 기능들을 통해 사용자 경험을 업그레이드해줄 수 있습니다.

우선 이번에는 버튼에 'tinted' 스타일을 적용하고 제목을 설정합니다. 

그리고 버튼에 이미지를 추가할 것 입니다. 기본 버튼에서는 버튼 왼쪽에만 이미지를 추가할 수 있어서 이미지를 오른쪽으로 이동시키기 위해선 버튼에 대한 커스텀이 이루어져야 했습니다. 그러나 이 configuration을 통해 이러한 이미지의 배치를 간단하게 수정할 수 있습니다.

세션에서는 아직 업그레이드 하고 싶은 부분이 많아 보이는데요..!! 다음으로 원하는 것은 상품에 대한 추가가 있을 때 부제로 주문에 대해 확인해 보는 것입니다.

두 번째로는 버튼을 눌렀을 때 윤곽선 이미지에서 채워진 버전으로 전환하는 것입니다.

이렇게 동적인 핸들링이 필요할 때는 configurationUpdateHandler라는 것을 이용해 볼 수 있습니다. 이것은 버튼을 업데이트해야 하는 경우 위 핸들러가 호출되어 변경할 수 있습니다. 예시를 살펴봅시다.

위에서 필요로 한 기능은 일반적으로 버튼의 configuration을 변경하는 것이므로 기존 버튼에서의 configuration을 가져오는 것으로부터 시작할 수 있습니다.

addToCartButton.configurationUpdateHandler = {
    [unowned self] button in
    /// ...
    var config = button.configuration
    button.configuration = config
}

먼저 버튼에 대한 이미지에 대해 설정을 해보도록 할 텐데요.

버튼을 눌렀을 때는 UIButton의 속성 값인 'isHighlighted' 속성이 변경됩니다. 그리고 속성 값이 변경됨에 따라 configurationUpdateHandler가 자동으로 호출됩니다. 그리고 이 핸들러 내에서 버튼의 'isHighlighted' 속성은 true이므로 이를 이용하여 버튼의 이미지를 변경하는 로직을 구현할 수 있습니다.

자 이제 버튼의 부제를 동적으로 업데이트해주도록 합시다.

// Customize image and subtitle with a configurationUpdateHandler

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
}

위와 같이 핸들러 내에서 configuration의 subtitle에 원하는 내용을 지정하도록 해주었습니다. 그러나 여기서 'itemQuantityDescription'은 버튼의 속성이 아닌 것을 알 수 있는데요. 이때는 핸들러가 자동으로 호출되지 않습니다. 그러면 어떻게 동적으로 업데이트를 할 수 있을까요?

답은 configurationUpdateHandler가 호출되도록 하는 메서드를 통해 해결할 수 있습니다. 아래와 같이 소비자가 액션을 통해 구입할 물건의 개수를 변경하고, 이를 관찰하여 itemQuantityDescription을 변경하도록 한다면, itemQuantityDescription을 관찰하여 값이 변경될 때마다 버튼의 setNeedsUpdateConfiguration()을 호출하도록 프로퍼티 옵저버를 구현하여 해결할 수 있습니다. 그리고 이 메서드는 configurationUpdateHandler가 호출되도록 트리거합니다.

// Update addToCartButton when itemQuantityDescription changes

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

Activity indicator

그리고 이제부터 버튼에서 activity indicator가 보이도록 할 수 있습니다. 간단한 속성만 바꾸는 것으로 말이죠

그리고 버튼의 레이아웃을 변경하는 것이 매우 간단하여, 버튼에서 컨텐츠가 배치되는 방식을 쉽게 제어할 수 있습니다.

그리고 이러한 버튼 내의 컨텐츠에 대하여 정렬되는 방법을 제어할 수 있습니다.

그리하여 간단히 filled 버튼으로부터 여러 가지 속성을 통해 제어를 하게 되면, 별도의 노력 없이 각 상태에 따른 버튼 디자인이 생성됩니다. 그리고 이러한 자동화가 제공되어도 여전히 각 상태에 대한 커스터마이징도 여전히 쉽습니다. 아래 'Check Out' 버튼을 살펴봅시다.

다음과 같이 크고 채워진 상태인 버튼이 있습니다. 그리고 버튼을 누르면 'Check Out' 프로세스가 진행될 것입니다. 이는 소비자가 버튼을 통해 확인할 수 있습니다.

그래서 다음과 같이 핸들러에서 'showActivityIndicator' 값을 수정해 주었습니다. 물론 'isCartBusy' 속성은 외부 속성이므로 버튼을 통해 'setNeedsUpdateConfiguration'를 호출하도록 할 것입니다. 그리고 버튼의 배경색을 디자인에 맞게 지정해 주었습니다.

Toggle buttons

지금까지 버튼의 디자인에 대해 사용자가 편리하게 지정할 수 있는 방법에 대해 알아보았습니다. 이제부터는 버튼에 추가된 다양한 기능들에 대해 알아보겠습니다.

버튼은 일반적으로 'push' 버튼이지만, 때로는 버튼의 추가 동작이 필요할 수 있습니다. 

첫 번째로 살펴볼 동작은 toggle입니다.

이 동작은 버튼의 선택한 상태를 유지하도록 하여 버튼을 누를 때마다 자동으로 켜고 끕니다. 그리고 이러한 방식은 코드를 통해 변경할 수도 있습니다. 그리고 이러한 상태에 대해 기본적으로 제공되는 스타일 이외에도 UIButtonConfiguration을 활용하여 켜지고 꺼지는 상태에 대한 디자인을 커스텀할 수 있습니다.

토글 버튼에 대한 예시로 캘린더 앱을 들 수 있습니다.

위와 같은 토글 형식의 버튼을 구현하기 위해 버튼에서 선택한 상태를 읽거나 설정할 수 있는 새로운 속성이 생겼습니다. 예시를 살펴봅시다.

예시에서는 상품에 대한 리스트를 볼 수 있는 화면이 존재합니다. 그리고 토글 버튼을 통해 재고가 있는 제품에 대하여 필터링할 수가 있습니다. 그럼 코드는 어떨까요??

// Toggle button

// UIAction setup
let stockToggleAction = UIAction(title: "In Stock Only") { _ in
    toggleStock()
}

// The button
let button = UIButton(primaryAction: stockToggleAction)

button.changesSelectionAsPrimaryAction = true

// Initial state
button.isSelected = showingOnlyInStock()

아주 간단하게 구현되어 있습니다. 버튼에 대한 액션을 구성하고, changesSelectionAsPrimaryAction을 true로 설정해 주면 버튼을 누를 때 상태는 저장됩니다.

Pop-up buttons

버튼은 이제 두 가지 상태를 왔다 갔다 할 수 있습니다. 그러나 더 많은 옵션으로 전환되는 것을 원할 경우 팝업 버튼을 사용할 수 있습니다. 이 버튼은 pull-down 버튼과 유사하다고 볼 수 있습니다. 이 버튼은 누르면 메뉴가 나타나고 메뉴 중 하나만 선택되도록 할 수 있습니다.

이러한 경우 버튼에 메뉴가 할당되고, 이를 기본 작업으로 설정하려면 showMenuAsPrimaryAction 속성을 true로 설정합니다. 

pop-up 버튼의 예시 전화앱, 발신 회선을 선택할 수 있는 팝업버튼이 있다.

이제 예시로 작성하던 앱에서 이러한 버튼을 사용해 봅시다.

다음과 같이 구매할 상품에 대해 팝업 버튼을 통해 색상을 지정하고 지정된 색상의 예시를 불러오도록 해줍니다. 코드를 살펴봅시다.

// Pop-up button

let colorClosure = { (action: UIAction) in
    updateColor(action.title)
}

let button = UIButton(primaryAction: nil)

button.menu = UIMenu(children: [
    UIAction(title: "Bondi Blue", handler: colorClosure),
    UIAction(title: "Flower Power", state: .on, handler: colorClosure)
])

button.showsMenuAsPrimaryAction = true

button.changesSelectionAsPrimaryAction = true

// Update to the currently set one
updateColor(button.menu?.selectedElements.first?.title)

// Update the selection
(button.menu?.children[selectedColorIndex()] as? UIAction)?.state = .on

다음과 같이 버튼의 menu 속성으로 전환할 여러 상태에 대해 지정하고, changesSelectionAsPrimaryAction를 true로 설정하여 상태를 저장하도록 한 뒤에 showsMenuAsPrimaryAction을 true로 설정하여 버튼 선택 시 팝업이 뜨도록 설정하면 모든 구현이 끝납니다. 이에 따라 초기값과 상태 변경에 따른 로직만 추가를 해주면 됩니다. 그리고 이는 스토리보드 상에서도 설정이 가능합니다.

Mac Catalyst

이러한 버튼들을 사용할 수가 있는 점의 가장 좋은 부분은 Mac Catalyst에서도 자동으로 작동한다는 것입니다. 일반적으로 PC 사용자는 이러한 인터렉션을 기대하는 부분이 많은데 iOS에서 구현하는 방식 그대로 Mac 버전으로 자동 업데이트가 가능합니다.

이는 버튼의 속성으로 간단하게 자동으로 환경에 맞는 스타일 업데이트가 가능하도록 해줍니다.

button.preferredBehavioralStyle = .automatic

UIMenu

앞서 팝업 버튼을 위해 사용했단 UIMenu라는 컴포넌트가 존재했습니다. 이 메뉴들에서도 하위 메뉴들을 설정할 수 있습니다.

만약 무언가를 정렬하는 기준에 대해 선택하도록 하는 팝업 버튼이 존재한다면, 여러 가지 정렬 기준 중 날짜에 대한 기준이 있을 것이고 이 날짜 중에서도 일, 월, 년을 기준으로 각각 정렬할 수 있는 방식이 존재할 수 있을 것입니다. 따라서 위에서 팝업 버튼의 메뉴를 구성하는 방식에서 추가적으로 하위 메뉴들을 생성할 수 있는 방식도 존재합니다.

세션에 따로 예시가 없어서 허접하게 직접 만들어 보았는데요..ㅎ

이러한 메뉴를 설정하는 방식도 매우 간단하기 때문에 코드만 첨부하도록 하겠습니다.

// Single selection menu

// The sort menu
let sortMenu = UIMenu(title: "Sort By", options: .singleSelection, children: [
	UIAction(title: "Title", handler: sortClosure),
	UIAction(title: "Date", handler: sortClosure),
	UIAction(title: "Size", handler: sortClosure)
])

// The top menu
let topMenu = UIMenu(children: [
	UIAction(title: "Refresh", handler: sortClosure),
	UIAction(title: "Account", handler: sortClosure),
	sortMenu
])

signInButton.menu = topMenu

자 여기까지 이번에 새롭게 추가된 버튼 시스템에 대해 알아보았습니다. 기존에 버튼에서 귀찮게 구현했던 것들을 잡아준 느낌이라 매우 유용할 것 같다는 생각이 드는데요. iOS 15를 타겟으로 하는 앱을 만든다면 꼭 써보고 싶네요 ~~!!