WWDC를 종종 보고 있다.
의도치않게 매번 찾은 주제보다 다른 곳에 관심을 가지게 되는 것 같지만 😅
테스트 코드에서 항상 보이는 UIAction에 대해 간략하게나마 정리해보고자 한다.
UIAction 자체는 iOS 13에서 소개 되었지만, 개인적으로 사용하지 않았던만큼 이유나 의미에 대해서 알아본다면 조금은 유의미하지 않을까~ 하는 마음에서 정리해본다!
A menu element that performs its action in closures.
공식문서에서 정리된 아주 짧은 UIAction에 대한 설명이다.
UIAction을 사용하면 '메뉴'를 만들 수 있게 되는데, 탭 시 실행되는 행동들은 클로저에 담으면 된다.
아래는 공식문서에서 제공하는 예시 코드!
let refreshAction = UIAction(title: "refresh") { (action) in
print("refresh the Data")
}
let refreshMenuItem = UIMenu(title: "", options: .displayInline, children: [refreshAction])
builder.insertSibling(refreshMenuItem, beforeMenu: .close)
위를 보면 refresh라는 제목을 가진 UIAction에 탭 시 행동(print)을 만들어 두었다.
해당 행동을 UIMenu에 담아둘 수 있도록 하였는데, UIMenu의 child의 하나로 넣어두게 된 모습이다.
아래처럼 만들어진다고 이해할 수 있겠다!
위에 정리한대로 UIAction은 iOS 13에 소개되었을 때 메뉴를 생성하는데 활용되었다.
하지만 iOS 14를 거치면서 UIKit에서 제공하는 다양한 컴포넌트(UISlider, UIButton, UISwitch) 등에도 활용이 가능해지게 되었다!
좀 더 명확하게 정리를 한다면 UIControl을 채택한 View에서 활용할 수 있게 되었다.
컴포넌트를 분리하는 습관이 있어서 아래와 같이 만들어 보았다.
버튼을 생성해두고 함수에서 버튼의 액션과 constraint를 잡아주었다.
let newButton = NewButton()
func configureNew() {
newButton.addAction(for: .touchUpInside) { [weak self] action in
let alert = UIAlertController(title: "이렇게?", message: "알람이?", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .cancel))
self?.present(alert, animated: true)
}
view.addSubview(newButton)
NSLayoutConstraint.activate([
newButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
newButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
let startButton = UIButton(primaryAction: UIAction { [activity] _ in
activity.start()
}
)
startButton.setImage(UIImage(systemName: "play.circle.fill"), for: .normal)
startButton.setTitle("Start", for: .normal)
view.addSubview(startButton)
사실 이게 가장 궁금했다.
WWDC23 에서 버튼들은 UIAction을 통해 행동을 제공하고 있었는데 이유가 도대체 무엇일까? 싶은 마음이었다. 간단한 예시들이면서 동시에 viewDidLoad()에 버튼들을 선언했기에 실용성을 고려한 코드라 보기 어려웠지만 말이다...
하지만 조금씩 찾아보면서 나름(?) 정리를 할 수 있었다.
일반화하기 어렵지만, 나는 Swift를 처음 배울 때 버튼을 아래와 같이 만들었다.
let button = UIButton()
button.setImage(UIImage(systemName: "refresh", for: .normal)
button.setTitle("Refresh", for: .normal)
button.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside)
view.addSubview(button)
@objc func actionButtonTapped() {
print("버튼이 눌렸습니다.")
}
이렇게 버튼을 먼저 만들고 하나씩 값을 주거나 만드는 시점에 모든 요소들을 함께 선언했었다.
하지만 여기서 중요한 부분은 'button.addTarget'에 있다.
버튼이 눌렸을 때의 액션을 제공하는 addTarget을 사용하지 않기 위해 애플은 UIAction의 적용 범위를 넓힌 것으로 보인다.
여러 이유들이 언급되었는데 몇 가지만 정리를 해보자면...
1. Creating code TWICE
하나의 버튼을 만드는데 관련 코드가 두 개 이상이 된다.
버튼을 어떻게 생성하던간에 버튼이 눌렸을 때의 액션은 @objc func를 만들어야 한다.
사람마다 코드 정리하는 방식이 다르겠지만 프로퍼티 및 컴포넌트를 파일 상단에, 메서드를 비교적 하단에 정리를 한다면 실질적인 버튼과 버튼의 액션을 알리는 코드의 간격이 멀어지게 된다.
결국 불필요해 보인다는 지적!
2. Objective-C
Swift 이전에 사용되던 Objective-C 언어를 사용한다는 점도 언급되었다.
Objc를 대체하기 위해 탄생한 Swift에서 간단한 버튼 하나 만드는데 이전 방식을 그대로 활용한다는 점이 어긋나 보인다는 의견도 종종 보였다.
이 내용은 그 다음 이유와도 이어지는데...
3. NOT SWIFT-Like
Swift 답지 않다는 의견이 가장 지배적이다.
읽고, 사용하기 쉬워야 하는 언어인만큼 UIAction의 적용 범위를 넓혔다고도 한다.
공식 입장이나 이유를 담은 내용은 발견하지 못한만큼 대부분 블로그, 커뮤니티 및 다양한 소스에서 긁어모은 내용들입니다! 틀렸거나 실수한 내용이 있다면 알려주세욥!
오히려 @objc 패턴으로 액션을 제공하는 방식이 여러 방면에서 안전하다는 의견도 많았다.
여러모로 프로젝트에서 컴포넌트를 선언할 때도 고려해야할 부분들이 많다!
@objc 말고 다른 방법은 없는건지
바로 rxswift, combine 같은 Reactive Programming으로 넘어가야 하는 건지 궁금했는데
이렇게 하는 거였군요
addAction 으로 버튼 혹은 다른 컴포넌트가 터치되었을 때의 작동을 정의 한다면 터치가 발생한 컴포넌트에는 클로저 안에서는 알 방법이 없는거죠?
self.xxx 이렇게 접근해야 하는건가요?
@objc 방식은 파라미터에 _ uiButton: UIButton 으로 적어두면 그 버튼의 레퍼런스를 넘겨 주잖아요