안녕하세요.
UIKit에서 SwiftUI, 그리고 SwiftUI에서 UIKit로 이벤트를 전달하는 방법에 대해 알아볼게요.
전체 프로젝트는 여기에 업로드해두었으니 참고해주세요!
먼저, SwiftUI에서 UIKit으로 이벤트를 전달하는 코드를 직접 구현해보고 그 반대도 가볍게 알아볼게요ㅎㅎ
이벤트를 전달하는 방법에는 크게(그리고 가장 유명한..) 3가지 방법이 있을 것 같아요.
- Combine, ObservableObject
- Closure
- Notification
3가지 방법을 각각 사용해서, 아래 같은 화면(SwiftUI Button을 클릭해서 count를 증가시키고 UIKit Label의 text에는 count 값이 반영되는)을 만들어보는 연습을 해볼게요.
# 1. SwifftUI -> UIKit으로 이벤트 전달
## 1.1 Combine, ObservableObject
우선 ObservableObeject를 사용해볼게요.
아래처럼 EventMessenger라는 ObservableObject 클래스와 Published 프로퍼티를 정의해줍니다.
class EventMessenger: ObservableObject {
@Published var tapCount: Int = 0
}
EventMessenger 객체를 ViewController에서 생성하고, SwiftUI 뷰는 EventMessenger를 전달받을 수 있도록 EnvironmentObject로 선언해줄게요.
class ViewController: UIViewController {
private var notifier: EventMessenger = EventMessenger()
...
}
struct TapView: View {
@EnvironmentObject var notifier: EventMessenger
var body: some View {
VStack {
Text("This is a SwiftUI Control.")
Button("Tap Me") {
notifier.tapCount += 1
}
.buttonStyle(.borderedProminent)
}
.padding()
.background(Color.gray.opacity(0.3))
.cornerRadius(8)
}
}
그다음 ViewController에서 EventMessenger 객체를 구독하고, SwiftUI 뷰에 주입시켜주면 됩니다.
class ViewController: UIViewController {
private var subs: [AnyCancellable] = []
private var notifier: EventMessenger = EventMessenger()
private let countLabel: UILabel = {
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
notifier.$tapCount.sink { [weak self] count in // 구독
self?.countLabel.text = "This is a UIKit Control.\nYou tapped \(count) times"
}
.store(in: &subs)
view.addSubview(countLabel)
NSLayoutConstraint.activate([
countLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
countLabel.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
])
// 주입
let tapController = UIHostingController(rootView: TapView().environmentObject(notifier))
tapController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tapController.view)
self.addChild(tapController)
tapController.didMove(toParent: self)
NSLayoutConstraint.activate([
tapController.view.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
tapController.view.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: 200),
])
}
}
👍 👍 👍
# 1.2 Closure
클로저로 이벤트를 전달해줄 수도 있어요.
우선 SwiftUI 뷰를 생성할 때 클로저를 전달받도록 하고, 버튼 클릭 액션 때 클로저를 동작시키면 되겠죠?
struct TapView: View {
private var handler: () -> ()
internal init(handler: @escaping () -> ()) {
self.handler = handler
}
@EnvironmentObject var notifier: EventMessenger
var body: some View {
VStack {
Text("This is a SwiftUI Control.")
Button("Tap Me") {
handler()
}
.buttonStyle(.borderedProminent)
}
.padding()
.background(Color.gray.opacity(0.3))
.cornerRadius(8)
}
}
ViewController에서는 Count 값을 저장할 프로퍼티를 만들고, SwiftUI 뷰를 생성 시 count와 Label Text를 수정해주는 로직이 들어간 클로저를 넘겨주면 됩니다.
class ViewController: UIViewController {
private var tapCount: Int = 0
...
override func viewDidLoad() {
...
let tapView = TapView() { [weak self] in
self?.tapCount += 1
self?.countLabel.text = "This is a UIKit Control.\nYou tapped \(self?.tapCount ?? 0) times"
}
let tapController = UIHostingController(rootView: tapView)
...
}
}
👍 👍 👍
# 1.3 Notification
Notification을 사용하는 방법도 있어요.
우선 Notification Name을 하나 정의해주고요.
extension NSNotification.Name {
static let tapViewTapped = NSNotification.Name("tapViewTapped")
}
SwiftUI 뷰에서 버튼 클릭 액션 때 NotificationCenter로 이벤트를 보냅니다.
struct TapView: View {
@EnvironmentObject var notifier: EventMessenger
var body: some View {
VStack {
Text("This is a SwiftUI Control.")
Button("Tap Me") {
NotificationCenter.default.post(name: .tapViewTapped, object: nil)
}
.buttonStyle(.borderedProminent)
}
.padding()
.background(Color.gray.opacity(0.3))
.cornerRadius(8)
}
}
ViewController에서는 NotificationCenter가 이벤트를 받았을 때의 동작을 정의해주면 끝이에요.
class ViewController: UIViewController {
private var subs: [AnyCancellable] = []
private var tapCount: Int = 0
...
override func viewDidLoad() {
...
NotificationCenter.default.publisher(for: .tapViewTapped).sink { [weak self] tapEvent in
self?.tapCount += 1
self?.countLabel.text = "This is a UIKit Control.\nYou tapped \(self?.tapCount ?? 0) times"
}
.store(in: &subs)
}
}
👍 👍 👍
# 2. UIKit -> SwifftUI으로 이벤트 전달
지금까지는 SwiftUI -> UIKit으로 이벤트를 전달하는 방법에 대해 알아봤다면,
반대로 UIKit -> SwiftUI로 이벤트를 전달하는 방법에 대해 알아볼게요.
사실 별거 없고...
Combine을 이용한 방법을 예시로 들면,
ViewController에서 버튼 클릭 액션 때 EventMessenger의 tapCount를 증가시켜주면 끝이에요ㅎㅎ..
(UIKit 버튼 클릭 시 SwiftUI 라벨이 변경된다는 의미죠...허헣..)
class ViewController: UIViewController {
private var subs: [AnyCancellable] = []
private var notifier: EventMessenger = EventMessenger()
...
override func viewDidLoad() {
...
notifier.$tapCount.sink { [weak self] count in
self?.countLabel.text = "This is a UIKit Control.\nYou tapped \(count) times"
}
.store(in: &subs)
...
let uikitTapButton = UIButton(configuration: .borderedProminent(), primaryAction: UIAction(){ [weak self] _ in
self?.notifier.tapCount += 1 ✅
})
}
}
다들 아시겠지만 뭐가 가장 좋다는 없고.. 상황에 맞는 적절한 방법을 쓰는 게 중요한 것 같습니다. ^^
## 참고
- https://www.swiftjectivec.com/events-from-swiftui-to-uikit-and-vice-versa/
이번 글은 여기서 마무리.
'iOS' 카테고리의 다른 글
Apple Common Scheme List (0) | 2022.08.04 |
---|---|
Private Pod 배포 방법 (0) | 2022.06.01 |
Logger, OSLogPrivacy (0) | 2022.05.02 |
[오픈소스] Inject (0) | 2022.04.12 |
Notification에 Action 버튼 추가하기 (0) | 2022.04.05 |