
이번 포스팅에서는 프로젝트에서 옵션을 설정할 때 사용했던 컴포넌트인 Pop up button과 Pull down button에 대해서 알아보겠습니다.
기존에는 여러 옵션 가운데 하나의 옵션만 선택 가능한(mutual-exclusive) 상황을 CollectionView를 통해 선택할 수 있도록 하였습니다. 모든 옵션들을 보여주고, 옵션에 해당하는 Cell을 선택하는 형식으로 처리했습니다.
그러나 UX를 개선하고자 몇 가지 수정을 하고 UI를 업데이트하는 과정에서 CollectionView를 사용하는 방법보다 더 나은 방법이 있는지 고민하는 과정에서 Pull-down, Pop-up button을 알게 되었습니다.
A pop-up button displays a menu of mutually exclusive options.
HIG에서는 이를 위와 같이 정의하고 있으며 다음과 같이 사용하라고 말합니다.
A pull-down button displays a menu of items or actions that directly relate to the button's purpose.
반면 Pull-down button에 대해서는 HIG에서 위와 같이 정의하고 있습니다.
ellipsis.circle 사용한 버튼)showMenuAsPrimaryActioniOS 14부터 UIButton에 menu를 설정할 수 있습니다.
그리고 UIControl의 프로퍼티인 showMenuAsPrimaryAction을 통해 버튼을 클릭했을 때 설정한 메뉴를 보여줄 수 있습니다.
let button = UIButton()
let menu = UIMenu()
button.menu = menu
button.showMenuAsPrimaryAction = true
만약 showMenuAsPrimaryAction의 값이 false라면 버튼을 꾹 누르고 있으면 메뉴가 나타납니다.
changesSelectionAsPrimaryAction해당 속성은 iOS 15부터 사용할 수 있는 UIButton의 프로퍼티로, 버튼이 Selection을 추적하여 변화할 지를 설정합니다.
let button = UIButton()
button.changesSelectionAsPrimaryAction = true
Pop up button과 Pull down button은 위 두 프로퍼티를 설정하여 구현할 수 있습니다.
Pop up button의 특성을 생각해보면 다음과 같습니다.
button.menu != nilbutton.showMenuAsPrimaryAction = truebutton.changeSelectionAsPrimaryAction = true반면 Pull down button의 특성은 다음과 같습니다.
button.menu != nilbutton.showMenuAsPrimaryAction = truebutton.changeSelectionAsPrimaryAction = falseprivate let popUpButton: UIButton = {
let button = UIButton()
button.menu = /* Menu */
button.showsMenuAsPrimaryAction = true
button.changesSelectionAsPrimaryAction = true
return button
}()
private let pullDownButton: UIButton = {
let button = UIButton()
button.menu = /* Menu */
button.showsMenuAsPrimaryAction = true
button.changesSelectionAsPrimaryAction = false
return button
}()
Pop up button을 활용할 때, 한 옵션 클릭 시 실행되는 로직에 따라 성공할 수도 있고 실패할 수도 있습니다.
예를 들어, 이번 프로젝트에서는 메뉴의 옵션 가운데 사용자와 가까운 순으로 정렬하는 옵션이 있었습니다.
이 때, 해당 메뉴 클릭이 처음이라면 사용자의 위치 권한 동의에 따라 동의를 받는 프롬프트가 나타나고, 위치 권한에 동의하지 않았다면 설정으로 이동하도록 하였습니다.
따라서 '가까운 순' 메뉴를 클릭하였을 때 우선 권한을 먼저 검사한 후 비동의 상태라면 메뉴의 선택을 이전의 옵션으로 돌려줘야 합니다. (이미 메뉴 선택을 하는 순간 버튼은 해당 옵션을 선택한 것임)
위 상황을 해결하기 위해 UIAction의 state 프로퍼티를 이용할 수 있습니다.
final class PopUpButton: UIButton {
let selectedFilterRelay = BehaviorSubject<MyOption>(value: /* initial option */)
private let options: [MyOption] = [/* options */]
init() {
// ...
let menuChildren = self.options.map { filterOption in
return UIAction(title: filterOption.title) { action in
if filterOption == /* distance option */, !self.checkLocationAuthorization() {
self.selectMenuOption(self.selectedFilterRelay.value)
return
}
self.selectedFilterRelay.accept(filterOption)
}
}
self.menu = UIMenu(children: menuChildren)
self.preferredMenuElementOrder = .fixed
self.showsMenuAsPrimaryAction = true
self.changesSelectionAsPrimaryAction = true
}
private func checkLocationAuthorization() -> Bool {
switch LocationManager.instance.status {
case .authorized:
return true
case .denied:
URLOpener.openURL(urlString: UIApplication.openSettingsURLString)
return false
case .notDetermined:
LocationManager.instance.requestPermission()
// 💡권한 프롬프트에서 위치 권한에 동의한다면 실행할 작업
LocationManager.instance.scheduledTask = { [unowned self] in
self.selectMenuOption(.distance)
self.selectedFilterRelay.accept(.distance)
}
return false
}
}
private func selectMenuOption(_ option: StoreSortOption) {
guard let previousOption = self.menu?.children.first(where: { $0.title == option.title }) as? UIAction else { return }
previousOption.state = .on
}
}
import CoreLocation
public class LocationManager: NSObject, AuthorizationRequired {
public static let instance = LocationManager()
private lazy var manager: CLLocationManager = {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self
return manager
}()
public private(set) var status: AuthorizationStatus = .notDetermined
// ...
/// A task which will be executed when the authorization status changed from notDetermined into authorized.
/// This task will be gotten across from FilteringButton class and do the follwing things
/// - Select distance option from UIMenu
/// - FilterOption relay will accept distance option
public var scheduledTask: (() -> Void)?
private override init() {}
public func requestPermission() {
guard self.status != .authorized else { return }
self.manager.requestWhenInUseAuthorization()
}
}
// MARK: - CLLocationManagerDelegate
extension LocationManager: CLLocationManagerDelegate {
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .notDetermined, .restricted:
self.status = .notDetermined
return
case .denied:
self.status = .denied
case .authorizedAlways, .authorizedWhenInUse:
self.status = .authorized
self.manager.startUpdatingLocation()
if let task = self.scheduledTask {
task()
}
@unknown default:
break
}
self.scheduledTask = nil
}
// ...
}
Apple HIG - Pop up buttons
Apple HIG - Pull down buttons
[iOS] Pull Down Button 과 Pop Up Button