iOS

SwiftUI <-> UIKit 이벤트 전달 방법

Phililip
728x90

안녕하세요.

 

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/

 

Send Events from SwiftUI to UIKit and Vice Versa

So, how do you notify UIKit that something happened in SwiftUI? And how about the other way?

www.swiftjectivec.com

 


 

이번 글은 여기서 마무리.

 

 

 

 

반응형

'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