본문 바로가기
🍎 iOS/WWDC

[WWDC 2022] 기존 UIKit 앱에 SwiftUI 적용하기 (Use SwiftUI with UIKit)

by Fomagran 💻 2022. 7. 9.
728x90
반응형

안녕하세요 Foma 입니다!

 

예전부터 WWDC 내용을 정리하고 싶었는데 드디어 오늘 WWDC에 대해 글을 작성하게 되네요.

 

WWDC 2022에서 가장 관심을 끌었던 세션은 UIKit 앱에 SwiftUI를 적용하는 세션이었는데요.

 

해당 세션을 직접 구현해 보고 정리해 보도록 하겠습니다.

 

바로 시작할게요~

 

(Xcode 14 Beta로 진행되기 때문에 혹시 미리 경험하고 싶은 분들은 여기 에서 다운로드 받아서 진행해 주세요~)


Preview

 


UIHostingController

 

UIHostingController는 SwiftUI 뷰를 포함한 UIViewController입니다.

 

 

UIHostingController를 이용하여 UIViewController에서 SwiftUI 뷰를 팝업 형식으로 띄워보도록 하겠습니다.

 

먼저 아래와 같이 SwiftUI 뷰를 만들어 줍니다.

 

import SwiftUI

struct SayHelloView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

 

그리곤 ViewController로 이동해 줍니다.

 

먼저 UIHostingController에 위에서 만들어준 SayHelloView를 rootView로 설정해 줍니다.

 

그리곤 컨텐트 사이즈 옵션과 원하는 모달 형식을 정하고 present로 hostingController를 띄워줍니다.

 

 viewDidLoad에 openSwiftUIView 메서드를 1초 뒤에 띄우도록 하겠습니다.

 

import UIKit
import SwiftUI

@available(iOS 16.0, *)
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.openSwiftUIView()
        }
    }
    
    private func openSwiftUIView() {
        let hostingController = UIHostingController(rootView: SayHelloView())
        hostingController.sizingOptions = .preferredContentSize
        hostingController.modalPresentationStyle = .popover
        self.present(hostingController, animated: true)
    }
}

 

이렇게 하면 아래와 같이 SwiftUI로 된 SayHelloView가 팝업으로 띄워지게 됩니다.

 


Data in SwiftUI Views

 

이젠 SwiftUI 뷰에 데이터를 전달하는 방법에 대해서 다뤄 보도록 하겠습니다.

 

먼저 가장 간단한 방법으로 구현해 보겠습니다.

 

아래와 같이 HeartRateView에 BPM을 저장할 변수 beatsPerMinute을 만들어 주고 body의 Text에 해당 BPM을 띄우도록 합니다.

 

struct HeartRateView: View {
    var beatsPerMinute: Int
    
    var body: some View {
        Text("\(beatsPerMinute) BPM")
    }
}

 

그리곤 아래와 같이 HeartRateViewController에 똑같이 beatsPerMinute 변수를 만들어 주고 didSet 으로 변화할 때 마다 update 메서드를 실행하도록 합니다.

 

update 메서드는 바뀐 beatsPerMinute을 HeartRateView에 전달하여 초기화 하도록 합니다.

 

import UIKit
import SwiftUI

@available(iOS 16.0, *)
class HeartRateViewController: UIViewController {
    let hostingController: UIHostingController<HeartRateView> = UIHostingController(rootView: HeartRateView(beatsPerMinute: 0))
    
    var beatsPerMinute: Int = 0 {
        didSet {
            update()
        }
    }
    
    override func viewDidAppear(_ animated: Bool) {
        self.openSwiftUIView()
    }
    
    func update() {
        hostingController.rootView = HeartRateView(beatsPerMinute: beatsPerMinute)
    }
    
 	private func openSwiftUIView() {
        hostingController.sizingOptions = .preferredContentSize
        hostingController.modalPresentationStyle = .popover
        self.present(hostingController, animated: true)
    }
}

 

이렇게 구현하면 아래와 같이 ViewController에서 지정한 데이터가 HeartRateView에 전달되어 띄워지게 됩니다.

 

 

 

하지만 이러한 방법은 직접 HostingController의 rootView를 데이터가 바뀔 때마다 업데이트 해줘야 하기 때문에 최선의 방법은 아닙니다.


Bridging data to SwiftUI using ObservableObject

 

변화를 감지하여 자동으로 변경할 수 있도록 하는 프로퍼티 래퍼를 사용하는 것입니다.

 

 

아래와 같이 ObservableObject로 된 데이터 모델을 만들고 SwiftUI 뷰에 ObservedObject로 된 데이터 변수를 만들어 줍니다.

 

 

아래와 같이 ObservableObject로 된 HeartData 모델을 만들어 줍니다.

 

@Published로 beatsPerMinute 변수를 만들어 주어 원하는 뷰에 데이터를 전달할 수 있도록 만들어 줍니다.

 

import Foundation

class HeartData:ObservableObject {
    @Published var beatsPerMinute: Int
    
    init(beatsPerMinute: Int) {
        self.beatsPerMinute = beatsPerMinute
    }
}

 

HeartRateView엔 @ObservedObject로 된 데이터를 만들어 주어 위 HeartData에서 전달된 데이터를 관찰할 수 있도록 만들어 줍니다.

 

import SwiftUI

struct HeartRateView: View {
    @ObservedObject var data: HeartData
    
    var body: some View {
        Text("\(data.beatsPerMinute) BPM")
    }
}

 

ViewController엔 해당 데이터를 만들어 HostingController의 rootView에 전달해 줍니다.

 

import UIKit
import SwiftUI

@available(iOS 16.0, *)
class ViewController: UIViewController {
 
    override func viewDidAppear(_ animated: Bool) {
        self.openSwiftUIView()
    }
    
     private func openSwiftUIView() {
         let data:HeartData = HeartData(beatsPerMinute: 10)
               let hostingController: UIHostingController<HeartRateView> = UIHostingController(rootView: HeartRateView(data: data))
        hostingController.sizingOptions = .preferredContentSize
        hostingController.modalPresentationStyle = .popover
        self.present(hostingController, animated: true)
    }
}

 

아래와 같이 데이터가 전달됩니다.

 

이렇게 만들어 주면 전달한 데이터의 값에 따라 SwiftUI 뷰가 보여지는 것을 볼 수 있습니다.

 


UIHostingConfiguration

 

UIHostingConfiguration은 SwiftUI 뷰로 UITableViewCell 또는 UICollectionViewCell을 구현할 수 있도록 하는 struct입니다.

 

셀을 보여주는데 굉장히 가볍게 보여줄 수 있으며 UITableView와 UICollectionView에서 모두 사용할 수 있다는 장점이 있습니다.

 


Controller

 

HeartRateViewController를 만들어 주고 Storyboard에서 TableView를 세팅해 줍니다.

 

 

아래와 같이 TableViewCell에 .contentConfiguration 메서드를 사용하여 UIHostingConfiguration을 구성해 줍니다.

 

안의 내용은 SwiftUI 뷰를 만들 때와 같이 구성해 주면 됩니다.

 

import UIKit
import SwiftUI

@available(iOS 16.0, *)
class HeartRateViewController: UIViewController {
    
    @IBOutlet weak var table: UITableView!
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

@available(iOS 16.0, *)
extension HeartRateViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return heartDataSamples.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
            cell.contentConfiguration = UIHostingConfiguration {
                HStack {
                    VStack(alignment: .leading) {
                        HeartRateTitleView()
                        Spacer()
                        HStack(alignment: .bottom) {
                            HeartRateBPMView(heartData:heartDataSamples[indexPath.row])
                            Spacer()
                            HeartRateChartView(heartData:heartDataSamples[indexPath.row])
                        }
                    }
                }
            }
        
        return cell
    }
}

Data

 

HeartData에 chartData와 id를 추가해 주도록 하겠습니다.

 

class HeartData: Identifiable,ObservableObject {
    let id = UUID()
    @Published var chartData: [HeartChartData]
    @Published var bpm: Int
    
    init(chartData: [HeartChartData] , bpm: Int) {
        self.chartData = chartData
        self.bpm = bpm
    }
}

 

HeartChartData도 추가해 줍니다.

 

import Foundation

class HeartChartData: Identifiable,ObservableObject {
    let id = UUID()
    @Published var time: Int
    @Published var beatsPerMinute: Int
    
    init(time: Int, beatsPerMinute: Int) {
        self.time = time
        self.beatsPerMinute = beatsPerMinute
    }
}

 

위 두 개의 데이터를 이용하여 heartDataSamples를 만들어 줍니다.

 

import Foundation

var heartDataSamples: [HeartData] = [
    HeartData(chartData: [HeartChartData(time: 10, beatsPerMinute:5),
                          HeartChartData(time: 20, beatsPerMinute:1),
                          HeartChartData(time: 30, beatsPerMinute:2),
                          HeartChartData(time: 40, beatsPerMinute:3),
                          HeartChartData(time: 50, beatsPerMinute:4),
                          HeartChartData(time: 60, beatsPerMinute:1),], bpm: 90),
    HeartData(chartData: [HeartChartData(time: 10, beatsPerMinute:1),
                          HeartChartData(time: 20, beatsPerMinute:2),
                          HeartChartData(time: 30, beatsPerMinute:3),
                          HeartChartData(time: 40, beatsPerMinute:4),
                          HeartChartData(time: 50, beatsPerMinute:5),
                          HeartChartData(time: 60, beatsPerMinute:6),], bpm: 100),
    HeartData(chartData: [HeartChartData(time: 10, beatsPerMinute:3),
                          HeartChartData(time: 20, beatsPerMinute:5),
                          HeartChartData(time: 30, beatsPerMinute:7),
                          HeartChartData(time: 40, beatsPerMinute:1),
                          HeartChartData(time: 50, beatsPerMinute:8),
                          HeartChartData(time: 60, beatsPerMinute:9),], bpm: 150),
    HeartData(chartData: [HeartChartData(time: 10, beatsPerMinute:2),
                          HeartChartData(time: 20, beatsPerMinute:3),
                          HeartChartData(time: 30, beatsPerMinute:5),
                          HeartChartData(time: 40, beatsPerMinute:7),
                          HeartChartData(time: 50, beatsPerMinute:2),
                          HeartChartData(time: 60, beatsPerMinute:1),], bpm: 120),
    HeartData(chartData: [HeartChartData(time: 10, beatsPerMinute:6),
                          HeartChartData(time: 20, beatsPerMinute:8),
                          HeartChartData(time: 30, beatsPerMinute:9),
                          HeartChartData(time: 40, beatsPerMinute:0),
                          HeartChartData(time: 50, beatsPerMinute:3),
                          HeartChartData(time: 60, beatsPerMinute:2),], bpm: 80),
    HeartData(chartData: [HeartChartData(time: 10, beatsPerMinute:6),
                          HeartChartData(time: 20, beatsPerMinute:7),
                          HeartChartData(time: 30, beatsPerMinute:8),
                          HeartChartData(time: 40, beatsPerMinute:3),
                          HeartChartData(time: 50, beatsPerMinute:1),
                          HeartChartData(time: 60, beatsPerMinute:8),], bpm: 110),
    HeartData(chartData: [HeartChartData(time: 10, beatsPerMinute:9),
                          HeartChartData(time: 20, beatsPerMinute:4),
                          HeartChartData(time: 30, beatsPerMinute:2),
                          HeartChartData(time: 40, beatsPerMinute:1),
                          HeartChartData(time: 50, beatsPerMinute:7),
                          HeartChartData(time: 60, beatsPerMinute:8),], bpm: 70),
]

View

 

HeartRateTitleView

 

해당 뷰는 셀의 위쪽에 Heart 이미지와 현재 날짜를 보여주는 뷰입니다.

 

import SwiftUI

@available(iOS 16.0, *)
struct HeartRateTitleView: View {
    var body: some View {
        HStack {
            Label("Heart Rate", systemImage: "heart.fill")
                .foregroundColor(.pink)
                .font(.system(.subheadline, weight: .bold))
            Spacer()
            Text(Date(),style: .time)
                .foregroundStyle(.secondary)
                .font(.footnote)
        }
    }
}

 

HeartRateBPMView

 

해당 View는 셀의 바텀쪽에 BPM을 띄우는 뷰입니다.

 

import SwiftUI

@available(iOS 16.0, *)
struct HeartRateBPMView: View {
    @ObservedObject var heartData: HeartData
    
    var body: some View {
        HStack(alignment: .firstTextBaseline) {
            Text("\(heartData.bpm)")
                .font(.system(.title,weight: .semibold))
            
            Text("BPM")
                .foregroundColor(.secondary)
                .font(.system(.subheadline,weight:.bold))
        }
    }
}

 

HeartRateChartView

 

해당 View는 이번에 WWDC 2022에서 발표된 Chart를 이용하여 HeartData를 띄워주는 뷰입니다.

 

import SwiftUI
import Charts

@available(iOS 16.0, *)
struct HeartRateChartView: View {
    @ObservedObject var heartData: HeartData
    
    var body: some View {
        Chart(heartData.chartData) { sample in
            LineMark(x: .value("Time", sample.time), y: .value("BPM", sample.beatsPerMinute))
                .symbol(Circle().strokeBorder(lineWidth: 2))
                .foregroundStyle(.pink)
        }
    }
}

실행 화면

 

위와 같이 구현해 주면 아래와 같이 TableViewCell에 SwiftUI로 된 뷰가 데이터의 값에 맞게 보여지는 것을 볼 수 있습니다.

 


List Actions

 

테이블뷰 셀을 Swipe하거나 Tap을 하는 액션 등을 구현해 줄 수도 있는데요.

 

swipeActions

 

먼저 swipe부터 원하는 View에 swipeActions으로 스와이프 했을 때의 뷰를 만들어 줍니다.

 

저는 끝에 trash 아이콘 버튼을 넣고 해당 버튼을 누르면 delete가 출력되도록 만들겠습니다.

 

    private func deleteHandler() {
        print("delete")
    }
    
    ...

            cell.contentConfiguration = UIHostingConfiguration {
                HStack {
                    VStack(alignment: .leading) {
                        HeartRateTitleView()
                        Spacer()
                        HStack(alignment: .bottom) {
                            HeartRateBPMView(heartData:heartDataSamples[indexPath.row])
                            Spacer()
                            HeartRateChartView(heartData:heartDataSamples[indexPath.row])
                        }
                    }
                }
                .swipeActions(edge: .trailing) {
                    Button(role: .destructive, action: self.deleteHandler) {
                        Label("Delete", systemImage: "trash")
                    }
                }
            }

 

아래와 같이 스와이프를 하면 trash 모양 버튼이 끝 쪽에 나타나고,

 

 

해당 버튼을 누르면 delete가 출력되는 것을 볼 수 있습니다.

 

onTapGesture

 

onTapGesture를 활용하여 HeartRateTitleView를 누르면 BPM이 +1 되도록 구현해 보겠습니다.

 

HeartRateTitleView에 onTapGesture를 달고 해당 indexPath.row번째 bpm을 +1 해줍니다.

 

            cell.contentConfiguration = UIHostingConfiguration {
                HStack {
                    VStack(alignment: .leading) {
                        HeartRateTitleView()
                            .onTapGesture {
                                heartDataSamples[indexPath.row].bpm += 1
                            }
                            
                            ...

 

HeartRateTitleView 부분을 누르면 BPM이 올라가는 모습을 볼 수 있습니다.

 

ConfigurationUpdateHandler

 

onTapGesture 말고도 Cell의 상태를 확인하여 구현하는 방법도 있는데요.

 

아래와 같이 contentConfiguration을 configurationUpdateHandler로 한번 감싸줍니다.

 

그 다음 state를 통해서 셀이 선택 되었는지 여부를 확인하고 SwiftUI 뷰를 추가할 수도 있는데요.

 

셀이 선택되면 오른쪽의 끝에 체크 표시를 해보도록 하겠습니다.

 

        cell.configurationUpdateHandler = { cell, state in
            cell.contentConfiguration = UIHostingConfiguration {
              		...
                    
                    if state.isSelected {
                        VStack {
                            Spacer()
                            Image(systemName: "checkmark")
                            Spacer()
                        }
                    }
                }
	...

 

아래와 같이 셀이 선택되면 오른쪽 끝에 체크 표시가 추가되는 것을 볼 수 있습니다.

 


HeartRateDetailView

 

마지막으로 셀을 선택했을 때 HeartRate의 디테일 뷰를 팝업 형식으로 띄워보도록 하겠습니다.

 

HeartRateDetailView를 만들어 줍니다.

 

import SwiftUI

@available(iOS 16.0, *)
struct HeartRateDetailView: View {
    @ObservedObject var data: HeartData
    
    var body: some View {
            HStack {
                VStack(alignment: .leading) {
                    HeartRateTitleView()
                    HStack(alignment: .bottom) {
                        HeartRateBPMView(heartData: data)
                        Spacer()
                        HeartRateChartView(heartData: data)
                            .frame(height:200)
                    }
                }
            }
    }
}

 

UIHostingController에 rootView로 HeartRateDetailView를 설정한 뒤 팝업 형식으로 띄워보도록 하겠습니다.

 

    private func openDetailView(heartData: HeartData) {
        let hostingController: UIHostingController<HeartRateDetailView> = UIHostingController(rootView: HeartRateDetailView(data: heartData))
        hostingController.sizingOptions = .preferredContentSize
        hostingController.modalPresentationStyle = .popover
        self.present(hostingController, animated: true)
    }

 

HStack에 onTapGesture를 달고 openDetailView에 heartDataSamples의 indexPath.row번째 데이터를 넘겨줍니다.

 

   cell.contentConfiguration = UIHostingConfiguration {
                HStack {
                    VStack(alignment: .leading) {
                        HeartRateTitleView()
                            .onTapGesture {
                                heartDataSamples[indexPath.row].bpm += 1
                            }
                        Spacer()
                        HStack(alignment: .bottom) {
                            HeartRateBPMView(heartData:heartDataSamples[indexPath.row])
                            Spacer()
                            HeartRateChartView(heartData:heartDataSamples[indexPath.row])
                        }
                    }
                }
                .swipeActions(edge: .trailing) {
                    Button(role: .destructive, action: self.deleteHandler) {
                        Label("Delete", systemImage: "trash")
                    }
                }
                .onTapGesture {
                    self.openDetailView(heartData: heartDataSamples[indexPath.row])
                }

 

이렇게 하면 셀을 선택했을 때 HeartRateDetailView가 데이터와 함께 모달 형식으로 띄워지게 됩니다.

 


Source Code

 

 

GitHub - fomagran/WWDC: Repository to study content published by WWDC

Repository to study content published by WWDC. Contribute to fomagran/WWDC development by creating an account on GitHub.

github.com


Reference

 

 

Use SwiftUI with UIKit - WWDC22 - Videos - Apple Developer

Learn how to take advantage of the power of SwiftUI in your UIKit app. Build custom UICollectionView and UITableView cells seamlessly...

developer.apple.com

 

728x90
반응형

댓글