Skip to content

Use SwiftUI with UIKit #48

@chaneeii

Description

@chaneeii

Introduction

이번주는 오랜만에 WWCC22 영상을 정리했습니다.
이미 영상을 다 봤는데, 이전에 놔스닥이 정리한 부분이지만 다시한번 정리를 대신 쫌더 깔끔하게 해보겠습니다!

이번 영상에서는 UIKit 에서 SwiftUI 를 사용하는 방법을 배웠고 내용을 한번 정리해보겠습니다

1. UIHostingController

UIHostingController는 SwiftUI 뷰 계층 구조를 포함한 UIViewController입니다.
이 호스팅 컨트롤러는 UIKit의 ViewController 를 사용하는 곳 어느 곳에서나 사용할 수 있죠!
즉, view 속성에 UIView 가 들어있고 그안에 SwiftUI 콘텐츠가 들어갑니다

image

UIHostingController 의 기본 사용법 (예제로 알아보자)

UIHostingController는 UIKit 뷰 컨트롤러의 모든 API를 활용할 수 있다

HeartRateView를 SwiftUI 뷰로 HeartRateView를 만들고, root 뷰로 하는 UIHostingController를 만들고 표시합니다

  1. UIHostingController 를 present 하는 예시
// Presenting a UIHostingController
let heartRateView = HeartRateView() // SwiftUI View 
let hostingController = UIHostingController(rootView: heartRateView)

// Present the hosting controller modally
self.present(hostingController, animated: true)
  1. UIHostingController 를 Embeded 하는 예시

호스팅 컨트롤러를 하위뷰컨트롤러로 만들면 호스팅 컨트롤러 뷰의 위치와 크기를 조절할 수 있다.

// Embedding a UIHostingController

let heartRateView = HeartRateView() // a SwiftUI view
let hostingController = UIHostingController(rootView: heartRateView)

// Add the hosting controller as a child view controller
self.addChild(hostingController)
self.view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)

// Now position & size the hosting controller’s view as desired…

✨[NEW] UIHostingController 의 Sizing options

image

UIHostingController 는 내부 스유 컨텐츠가 바뀌면 뷰를 resizing 해야할 수 도 있는데,
iOS 16부터는 UIHostingController에서 뷰 컨트롤러가 권장하는 크기와 뷰 고유의 크기에 대해 자동 업데이트 기능을 사용할 수 있다.

  • .prefferedContentSize
  • .instrinsicContentSize

예시

// UIHostingController를 popover로 present 하는 예시 

// ✅ 1. 우선 HeartRateView를 만들고 호스팅 컨트롤러를 생성합니다 
let heartRateView = HeartRateView() // a SwiftUI view
let hostingController = UIHostingController(rootView: heartRateView)

// ✅ 2. 새로운 sizingOptions API (preferredContentSiz) 를 이용해 호스팅 컨트롤러에 권장사이즈로 자동 업데이트 한다
hostingController.sizingOptions = .preferredContentSize

// ✅ 3. 그다음 modalPresentationStyle을 팝오버뷰로 설정합니다 
hostingController.modalPresentationStyle = .popover
self.present(hostingController, animated: true)

=> 새 sizingOptions API를 이용하면 팝오버 뷰의 크기가 언제나 SwiftUI 콘텐츠에 알맞게 설정된다.

2. Binding Data

이제 UIKit 의 앱에서 SwiftUI 로 어떻게 데이터를 가져오고, 데이터가 바뀔때마다 SwiftUI 의 뷰를 업데이트 하는 법을 알아보겠다.

기존에 있던 모델 레이어에는 레이어가 소유하고 관리하는 데이터 모델 객체가 있어요 앱에는 여러 개의 뷰 컨트롤러도 있다.
SwiftUI를 쓰고 싶다면 뷰 컨트롤러 중 하나에 SwiftUI 뷰를 포함한 호스팅 컨트롤러가 필요하다.
기존 모델 레이어에 들어 있는 데이터를 이 SwiftUI 뷰에 추가한다고 했을때, UIKit와 SwiftUI의 경계를 넘어 데이터를 연결하는 법을 알아보자

image

Data in SwiftUI Views

SwiftUI 에서는 Data flow Primitives 를 여러개 제공하는데,

❌ SwiftUI 내부에서 데이터를 다루는 법 : @State / @StateObject

@State@StateObject 속성 래퍼는 SwiftUI 뷰에서 만들고 소유하는 데이터를 저장할 때 사용하므로 SwiftUI 외부에서 소유한 데이터를 다루는 이 파트에서 이 속성 래퍼들을 사용하지 않겠다.
image

✅ SwiftUI 외부에서 데이터를 다루는 법 1 : Passed Arguments (직접 값 전달)

SwiftUI 외부의 데이터를 다루는 한 가지 방법 중 하나는 뷰를 초기화할 때 직접 값을 전달하는 방식이다.
SwiftUI가 소유하거나 관리하지 않는 원시 데이터만 표시하기 때문에, 데이터가 바뀌면 UIHostingController를 수동으로 업데이트해야 한다.

image

UIKit에서 SwiftUI로 데이터를 가져오는 간단한 방법이다.
하지만 데이터가 바뀔 때마다 호스팅 컨트롤러의 루트 뷰를 수동으로 업데이트해야 한다.

// Passing data to SwiftUI with manual UIHostingController updates

struct HeartRateView: View {
    var beatsPerMinute: Int

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

class HeartRateViewController: UIViewController {
    let hostingController: UIHostingController< HeartRateView >
 // ✅ HeartRateView : 값타입 =>  값을 저장하면 별도의 복사본이 생성되고 UI를 업데이트할 수 없다.

    var beatsPerMinute: Int {  // ✅ HeartRateViewController가 HeartRateView에 들어가는 데이터를 소유
        didSet { update() } // ✅  분당 심박 수 값이 바뀌면 뷰를 업데이트하는 메서드를 호출한다.
    }

    func update() {
        hostingController.rootView = HeartRateView(beatsPerMinute: beatsPerMinute)
    }
}

✅ SwiftUI 외부에서 데이터를 다루는 법 2 : Passed Arguments (직접 값 전달)

image

지금까지 manually 하게 업데이트한 것들을 스유의 @ObservedObject와 @EnvironmentObject 속성 래퍼로 자동적으로 업데이트 하게 할 수 있다.
ObservableObject 프로토콜을 따르는 외부 모델 객체를 참조할 수 있고 이를 사용하면 바뀐 데이터를 SwiftUI가 자동으로 업데이트한다.

이번 영상에서는 @ObservedObject 속성 래퍼에 집중한다.

image

@ObservedObject를 만드는 법을 알아보자

1. 앱의 기존 부분이 소유하고 있던 모델 객체를 가져와 ObservableObject 프로토콜을 준수하게 한다
2. 모델을 SwiftUI 뷰에서 @ObservedObject 속성으로 저장한다
3. SwiftUI에 ObservableObject를 연결한다.

-> 이러면 속성이 변경될 때 뷰를 업데이트할 수 있다.

image

image

image

ObservableObject의 게시된 속성이 변경되면 HeartRateView가 자동으로 업데이트돼서 새 값이 표시된다.
ObservableObject를 사용하면 데이터가 바뀔 때마다 호스팅 컨트롤러를 수동으로 업데이트할 필요가 없다.

#3. SwiftUI in cells

Review ) Cell configurations

image

UIHostingConfiguration을 보기전에 UIKit의 Cell configurations 을 복습해봅시다.
이것은 UIKit에서 셀의 내용과 스타일 및 동작을 모던하게 해주는데, 자세한건 WWDC20 영상 참고!

  • UIView나 UIViewController와 달리 Cell Configurations은 가벼운 struct이고 만들기쉽니다.
  • Cell Configurations은 셀의 모습에 대한 description 뿐이라 셀에 apply 되어야 효과를 볼 수 있습니다.
  • Cell Configurations 셀 구성은 Composable하고 UICollectionView와 UITableView 셀에 전부 적용 가능하다.

✨[NEW] UIHostingConfiguration

image

iOS 16의 새로운 기능인 UIHostingConfiguration은 기존의 SwiftUI를 UIKit 의 컬렉션 뷰와 테이블 뷰 셀에서 사용하게 해줍니다
UIHostingConfiguration은 SwiftUI로 커스텀셀을 쉽게 만들게 해줍니다. (‼️ 추가로 뷰나 뷰 컨트롤러를 포함할 필요가 없다)

다음과 같이 사용합니다.

cell.contentConfiguration = UIHostingConfiguration {
  // Start writing SwiftUI here!
}

UIHostingConfiguration은 SwiftUI View Builder로 초기화한 콘텐츠 구성이다.
✨ 즉, SwiftUI 코드를 작성해서 내부에 바로 뷰를 만들 수 있다 ✨

예시를 보죠! EASY
image

그리고 스유에서 뷰 짜듯이 쭉쭊 짜주면 됩니다.
image

요렇게 새로운 스유API로 차트도 쉽게 넣을 수 있어요
image

✨ UIHostingConfiguration의 특별한 기능 (1) Content margins

기본적으로 루트 단계의 SwiftUI 콘텐츠는 셀 가장자리에서부터 UIKit의 셀 레이아웃 inset을 기준으로 삽입된다.
이러면 셀의 내용이 인접한 셀 및 탐색 바 같은 다른 UI 요소와 알맞게 들어간다.
(=> 요약: 셀안에 알잘딱깔센하게 들어감)

cell.contentConfiguration = UIHostingConfiguration {
    HeartRateBPMView()
}

스크린샷 2022-09-21 오후 11 59 07

그리고 마진을 줄 수도 있다.

cell.contentConfiguration = UIHostingConfiguration {
    HeartRateBPMView()
}
.margins(.horizontal, 16)

스크린샷 2022-09-21 오후 11 58 32

✨ UIHostingConfiguration의 특별한 기능 (2) Cell Backgrounds

셀의 배경색을 주는 법

cell.contentConfiguration = UIHostingConfiguration {
   HeartTitleView()
} 
.background(.pink)

스크린샷 2022-09-21 오후 11 58 23

🌊 Background 와 Content에 대한 몇가지 설명 추가
image

  • 배경은 셀의 뒤쪽에 자리를 잡는다 = 셀의 콘텐츠 뷰에서 SwiftUI 콘텐츠 아래 깔림
  • 콘텐츠는 일반적으로 셀의 가장자리에서부터 (inset) 삽입되는데 배경은 가장자리에서부터(edge to edge) 가장자리까지 쭉 펼쳐져 있다
  • 크기를 스스로 조절하는 셀의 경우엔 셀의 콘텐츠만 크기에 영향을 미칩니다

✨ UIHostingConfiguration의 특별한 기능 (3) List seperators

  • 리스트에서 seperators는 기본적으로 UIHostingConfiguration의 SwiftUI 텍스트와 자동 정렬 된다.
  • `.alginmentGuide' modifier 를 사용하여 커스텀할 수 있다. (이미지를 지나서부터 시작 등)

스크린샷 2022-09-22 오전 12 01 26

✨ UIHostingConfiguration의 특별한 기능 (4) List Swipe Actions

cell.contentConfiguration = UIHostingConfiguration {
    MedicalConditionView()
        .swipeActions(edge: .trailing) {  } // ✅ swipe actions
}

-⚠️ indexpath 는 셀이 보이는 동안 바뀌어서 잘못된 항목에 스와이프 동작이 수행될 수 있어 안정된 identifier 를 써야한다

스크린샷 2022-09-22 오전 12 03 22

configurationUpdateHandler : UIKit 의 셀상태와 상관없이 스유뷰를 따로 바꾸고 싶다면?

UIHostingConfiguration을 셀에서 사용할 때
탭 처리, 강조 표시, 선택 같은 셀 상호 작용은 컬렉션 뷰나 테이블 뷰에서 여전히 처리된다.

그럼, UIKit의 셀 상태와 상관없이 SwiftUI 뷰를 따로 바꾸고 싶다면?????
셀의 configurationUpdateHandler 안에 HostingConfigurations 을 만들면 SwiftUI 코드에서 제공하는 상태를 사용할 수 있다.

configurationUpdateHandler는 _셀의 상태가 변할 때마다 다시 실행_되고
_새로운 상태에 대한 UIHostingConfiguration을 만들어 셀에 적용_한다.

image

4. Data Flow for Cells

왼쪽에서 오른쪽으로 바꾸는 것에 대해 다룰껍니다!

이제 SwiftUI 셀로 채워진 UIKit 테이블뷰랑 컬렉션뷰에서 DataFlow 가 어떻게 되는지 한번 알아봅시다!
예시에서는 Diffable Data Source 를 사용하는 컬렉션 뷰를 씁니다

Diffable Data Source

여기서 잠깐! 한가지 알고 넘어가자

https://velog.io/@ellyheetov/UI-Diffable-Data-Source

DataSource란 TableView를 나타낼 데이터를 구성하고 UI를 업데이트 하는 역할을 한다.
기본적으로, Diffable Data Source와 DataSource의 역할은 같다.
그러나, Diffable Data Sources를 사용하면 tableView나 collectionView를 단순하게 업데이트가 가능하다. 이전 테이블과 달라진 부분을 자동으로 알아차리고, 새로운 부분만 다시 그리기 때문이다.

Displaying data in cells : 셀에 데이터를 나타내봅시다.

image
앱에는 컬렉션 뷰로 표시할 내용인 MedicalCondition 모델 객체의 컬렉션이 있다.
이 컬렉션의 각 항목에 대해 컬렉션 뷰의 셀을 생성해서 건강 상태를 표시하려고 한다.

그러려면 컬렉션 뷰에 연결된 Diffable Data Source 을 만들고 Diffable Data Source 의 스냅샷을 더해야 한다.
여기엔 데이터 컬렉션에 있는 MedicalCondition 모델 객체의 Identifier를 포함해야 한다

image

  • ✨ Diffable Data Source 의 스냅샷이 포함하고 있는 게 MedicalCondition 객체 자체가 아니라 MedicalCondition 각각의 고유 identifier이여야만한다.
    • Diffable Data Source 에서 각 항목이 identifier를 추적하고 나중에 새 snapshot을 적용할때 변경사항을 올바르게 찾을 수 있다.
    • identifier를 포함한 스냅샷을 Diffable Data Source 원본에 적용하면 자동으로 컬렉션 뷰에 업데이트 된다 -> 그리고 항목마다 새로운 셀이 생성된다.
    • 각 셀은 UIHostingConfiguration의 SwiftUI 뷰를 사용해 하나의 MedicalCondition을 표시하도록 구성

이제 SwiftUI로 만든 셀이 표시됐으니 데이터가 변했을 때 UI 업데이트를 처리해봅시다.

Handling changes. to data collection : 데이터 컬렉션의 변화처리하기

1. 데이터 컬렉션 자체가 변하는 유형

ex) 항목이 삽입, 순서가 바뀜, 항목 삭제의 경우

  • 이런 변경 사항은 가변 데이터 원본의 새 스냅샷을 적용해 처리
    • Diffable Data Source 은 이전과 새 스냅샷을 비교하고 컬렉션 뷰에 필요한 업데이트를 수행해서 셀을 삽입하거나 이동, 삭제할 겁니다
  • 데이터 컬렉션 자체의 변화는 셀 내부에는 영향을 미치지 않는다.
    • UIKit 으로 만든 셀이나 SwiftUI 로 만든 셀이나 똑같이 처리

2. 개별 모델 객체의 속성이 바뀌는 경우

  • 셀의 뷰를 업데이트해야한다
  • Diffable Data Source는 스냅샷에 항목 식별자만 포함해서 기존 항목의 속성이 언제 변경되는지 모든다
    • UIKit을 사용할 때는 Diffable Data Source에 스냅샷의 항목을 재구성하거나 다시 로딩하면서 수동으로 변경 사항을 알려야 했는데, 셀에서 SwiftUI를 사용하면 그럴 필요가 없다

SwiftUI 에선?

  • SwiftUI 뷰의 ObservableObject 속성에 관찰 가능한 모델을 저장하면서 모델의 게시된 속성이 바뀌면 자동으로 SwiftUI를 작동시켜 뷰를 새로 업데이트 한다. 즉, 셀 내부의 SwiftUI와 모델이 직접적으로 연결되어. 변경 사항이 생기면 셀 안의 SwiftUI가 바로 업데이트합니다 (가변 데이터 소스나 UICollectionView를 거치지 않는다)

image

셀의 데이터가 바뀌면 셀이 새로운 내용에 맞게 커지거나 작아진다.

하지만 SwiftUI 셀의 내용이 UIKit를 거치지 않고 바로 업데이트가 된다면 컬렉션 뷰가 어떻게 셀의 크기를 재조정할까요?
에 대해 새로운 API 로 이야기 해봐요!

✨ [NEW] Self-resizing cells

image

UIHostingConfiguration은 UIKit의 새 기능을 활용한다.
기본 기능이므로 UIHostingConfiguration을 사용하면서 SwiftUI 콘텐츠가 바뀌었을 때 필요한 경우 해당 셀 크기가 자동으로 재조정됩니다
이 기능에 대해 자세히 알고 싶으시면 WWDC 2022의 영상을 참고!

양방향 바인딩 : Handling changes to properties of objects

이번에는 ObservableObject 로 양방향 바인딩을 해줍시다.
지금까지는 데이터가 변화하면 그거 맞춰서 셀에 보여줬는데, 이제는 셀에서 데이터 변화되면 데이터에 내용을 반영하는 거죠!

image

요러면 간단하게 텍스트 필드에 타이핑을 하면 이 바인딩을 통해 SwiftUI가 변경된 내용을 ObservableObject에 직접 반영합니다
image

정리

아래와 같은 두가지 방법으로 이제 uikit 플젝에서 스유를 쓸 수 있다.

🚀 UIHostingController를 사용할 때는 앱에 뷰와 함께 뷰 컨트롤러를 항상 추가해야 하는 거 잊지말기

툴바나 키보드 단축키 및 UIViewControllerRepresentable을 쓰는 뷰와 같은 SwiftUI 기능은 UIKit의 뷰 컨트롤러 계층 구조에 연결되어야 제대로 통합될 수 있습니다. 그러니까 호스팅 컨트롤러의 뷰를 호스팅 컨트롤러 자체에서 분리하지 마세요

image

요약

  • UIHostingController를 사용해 앱에 SwiftUI를 추가해 보세요
  • UIHostingConfiguration을 사용해서 컬렉션 뷰와 테이블 뷰에 맞춤 셀을 만들어 보세요
  • ObservableObject를 활용하면 여러분의 데이터와 UI는 항상 동기화됩니다

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions