SwiftUI: Representable을 이용해서 UIViewController 띄우기
[rcblock id=”5440”]
소개
SwiftUI 환경에서 _UIKit_을 이용해 만든 UIViewController를 띄우는 방법에 대한 소개입니다.
예를 들어, MPMediaPickerController (import MediaPlayer 필요)는 UIViewController를 상속받은 뷰 컨트롤러인데 UIKit 환경에서는 self.present(viewController, ...)로 바로 띄울 수 있습니다. SwiftUI에서도 이 작업이 가능하지만 복잡한 과정이 필요합니다.
방법
1: UIViewControllerRepresentable을 준수하는 구조체 생성
아래와 같이 UIViewControllerRepresentable를 준수(conform)하는 구조체(struct)를 생성합니다.
UIViewControllerRepresentable는 UIKit의 뷰 컨트롤러를 나타내는 View입니다.
일반적으로 [컨트롤러 이름] + View 라는 이름으로 구조체 이름을 작성하지만 여기서는 편의를 위해 임의로 MPMediaPickerControllerRP라는 이름으로 작성했습니다.
1
2
3
4
struct MPMediaPickerControllerRP: UIViewControllerRepresentable {
// ... //
}
2: typealias로 나타내고자 하는 UIViewController 지정
MPMediaPickerControllerRP 구조체 안에 typealias를 추가합니다.
1
typealias UIViewControllerType = MPMediaPickerController
3: makeUIViewController와 updateUIViewController 추가
- makeUIViewController
UIViewController를 생성하고 초기화를 수행하는 함수입니다.
- updateUIViewController
UIViewController의 업데이트가 필요할 때 호출됩니다.- 이 위치에서
UIViewController에 필요한 데이터 또는 정보를 갱신해야 합니다.
이 예제에서는 makeUIViewController만 사용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct MPMediaPickerControllerRP: UIViewControllerRepresentable {
// ... //
let picker = MPMediaPickerController(mediaTypes: .music)
// ... //
func makeUIViewController(context: Context) -> UIViewControllerType {
picker.allowsPickingMultipleItems = false
return picker
}
func updateUIViewController(_ uiViewController: MPMediaPickerController, context: Context) {}
// ... //
}
makeUIViewController함수에서UIViewControllerType(=>MPMediaPickerController)를 리턴하도록 지정한 뒤, 해당 뷰 컨트롤러 인스턴스를 리턴합니다.
4: 버튼을 누르면 MPMediaPickerControllerRP가 나타나도록 하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct ContentView: View {
/*
@State로 선언한 프로퍼티는 값이 변경되면 뷰 계층 구조의 부분을 업데이트
@State를 자식 뷰에 전달하면 부모에서 값이 변경될 때마다 자식을 업데이트
단, 자식 뷰에서 값을 수정하려면, 부모에서 자식으로 Binidng을 전달하여 자식 뷰에서 값을 수정이 가능
*/
@State var isOpenMusicPickerView = false
var albums: [AlbumInfo] = []
var songQuery: SongQuery = SongQuery()
var body: some View {
VStack {
// ... //
Button {
isOpenMusicPickerView = true
} label: {
Text("Select a music from library...")
}.sheet(isPresented: $isOpenMusicPickerView) {
MPMediaPickerControllerRP()
}
}
// ... //
}
}
- sheet(isPresented: $isOpenMusicPickerView)
Delegate 사용하기
MPMediaPickerController에는 다음과 같은 delegate 함수가 있습니다.
1
2
3
func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
// ... //
}
앞의 섹션에서 뷰 컨트롤러를 띄우는 것 까지는 성공했는데, delegate는 어떻게 처리해야 할까요?
Coordinator라는 것을 이용하여 처리할 수 있습니다.
구체적인 방법은 다음과 같습니다.
사전 작업
노래의 메타데이터를 담은 구조체 MediaMetadata를 생성합니다.
1
2
3
4
5
6
7
8
9
import UIKit
struct MediaMetadata {
let title: String
let artist: String
let albumTitle: String
let duration: TimeInterval
let albumArtImage: UIImage?
}
1: Representable 구조체 안에 Coordinator 이너 클래스 생성
2: Coordinator 클래스 안에 사용하고자 하는 딜리게이트 함수 추가
3: 생성자에서 metadataCallback과 delegate = self 연결
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct MPMediaPickerControllerRP: UIViewControllerRepresentable {
// ... //
typealias MetadataCallback = (MediaMetadata) -> Void
var metadataCallback: MetadataCallback
// ... //
class Coordinator: NSObject, MPMediaPickerControllerDelegate {
// 데이터를 전달하는 콜백(클로저) 함수
var metadataCallback: MetadataCallback
init(_ viewController: MPMediaPickerController, metadataCallback: @escaping MetadataCallback) {
self.metadataCallback = metadataCallback
super.init()
viewController.delegate = self
}
// Delegate function
func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
let media = mediaItemCollection.items[0]
let title = media.title ?? "unknown title"
let artist = media.artist ?? "unknown artist"
let albumTitle = media.albumTitle ?? "unknown album title"
let duration = media.playbackDuration
let albumArtImage = media.artwork?.image(at: .zero)
let metadata = MediaMetadata(title: title, artist: artist, albumTitle: albumTitle, duration: duration, albumArtImage: albumArtImage)
// 데이터 밖으로 내보내기
metadataCallback(metadata)
mediaPicker.dismiss(animated: true)
}
}
}
- 대리자 함수 중
func mediaPicker(...didPickMediaItems...)를 사용할 예정이므로 해당 함수를Coordinator클래스 안에 추가합니다. - 대리자 함수 안에서 생성된 데이터를 밖에서 사용하려면 콜백(클로저) 를 사용합니다.
metadataCallback이 그 역할을 합니다.
4: Representable 구조체 안에 makeCoordinator() 함수 추가
1
2
3
4
// Representable에서 delegate 사용
func makeCoordinator() -> Coordinator {
Coordinator(picker, metadataCallback: metadataCallback)
}
- 여기서 반환된
Coordinator인스턴스를 통해 외부에서도delegate를 사용할 수 있게 됩니다.
5: MPMediaPickerControllerRP()에 metadataCallback 부분 (트레일링 클로저) 추가하기
1
2
3
4
5
6
7
8
9
10
11
12
13
Button {
isOpenMusicPickerView = true
} label: {
Text("Select a music from library...")
}.sheet(isPresented: $isOpenMusicPickerView) {
MPMediaPickerControllerRP { metadata in
// coordinator에서 밖으로 꺼낸 데이터
print("metadata outside:", metadata)
mediaTitle = metadata.title
mediaSubtitle = "\(metadata.artist) - \(metadata.albumTitle)"
albumImage = metadata.albumArtImage
}
}
- 기존의
MPMediaPickerControllerRP()대신MPMediaPickerControllerRP(metadata:)로 대체합니다.- 트레일링 클로저를 이용해 위와 같은 형태로 축약할 수 있습니다.
- 하이라이트된 부분과 같이
Coordinator외부로 전송된 데이터(metadata)를 이용해 해야 할 작업을 처리합니다.
https://giphy.com/gifs/EIAJUrg6zH5Hj5i2Rw
전체 코드 (ContentView.swift)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import SwiftUI
import MediaPlayer
struct ContentView: View {
/*
@State로 선언한 프로퍼티는 값이 변경되면 뷰 계층 구조의 부분을 업데이트
@State를 자식 뷰에 전달하면 부모에서 값이 변경될 때마다 자식을 업데이트
단, 자식 뷰에서 값을 수정하려면, 부모에서 자식으로 Binidng을 전달하여 자식 뷰에서 값을 수정이 가능
*/
@State var statusText = "Ready to play..."
@State var isOpenMusicPickerView = false
@State var mediaTitle = "Select a music..."
@State var mediaSubtitle = ""
@State var albumImage: UIImage? = UIImage(named: "Adiemus II")
var albums: [AlbumInfo] = []
var songQuery: SongQuery = SongQuery()
var body: some View {
VStack {
Spacer()
// 이미지 사이즈 조정
Image(uiImage: albumImage ?? UIImage())
.resizable()
.frame(width: 380, height: 380)
Spacer()
// 각종 버튼 (HStack)
HStack {
Spacer()
Button {
statusText = "Backward"
} label: {
Image(systemName: "backward.end.fill")
.font(.system(size: 50))
.foregroundColor(.black)
}
Spacer()
Button {
statusText = "Play"
} label: {
Image(systemName: "play.fill")
.font(.system(size: 50))
.foregroundColor(.black)
}
Spacer()
Button {
statusText = "Afterward"
} label: {
Image(systemName: "forward.end.fill")
.font(.system(size: 50))
.foregroundColor(.black)
}
Spacer()
}
// Spacer 높이 변경
Spacer().frame(height: 25)
Group {
Text(mediaTitle)
.font(.system(size: 25, weight: .bold))
Text(mediaSubtitle)
.font(.system(size: 18))
Spacer()
Text(statusText)
.foregroundColor(.gray)
}
Spacer()
Button {
isOpenMusicPickerView = true
} label: {
Text("Select a music from library...")
}.sheet(isPresented: $isOpenMusicPickerView) {
MPMediaPickerControllerRP { metadata in
// coordinator에서 밖으로 꺼낸 데이터
print("metadata outside:", metadata)
mediaTitle = metadata.title
mediaSubtitle = "\(metadata.artist) - \(metadata.albumTitle)"
albumImage = metadata.albumArtImage
}
}
}
.padding()
.onAppear {
MPMediaLibrary.requestAuthorization { status in
switch status {
case .notDetermined:
print("status: notDetermined")
case .denied:
print("status: denied")
case .restricted:
print("status: restricted")
case .authorized:
print("status: authorized")
@unknown default:
print("status: unknown default")
}
}
}
}
}
struct MPMediaPickerControllerRP: UIViewControllerRepresentable {
typealias UIViewControllerType = MPMediaPickerController
typealias MetadataCallback = (MediaMetadata) -> Void
let picker = MPMediaPickerController(mediaTypes: .music)
var metadataCallback: MetadataCallback
func makeUIViewController(context: Context) -> UIViewControllerType {
picker.allowsPickingMultipleItems = false
return picker
}
func updateUIViewController(_ uiViewController: MPMediaPickerController, context: Context) {}
// Representable에서 delegate 사용
func makeCoordinator() -> Coordinator {
Coordinator(picker, metadataCallback: metadataCallback)
}
class Coordinator: NSObject, MPMediaPickerControllerDelegate {
// 데이터를 전달하는 콜백(클로저) 함수
var metadataCallback: MetadataCallback
init(_ viewController: MPMediaPickerController, metadataCallback: @escaping MetadataCallback) {
self.metadataCallback = metadataCallback
super.init()
viewController.delegate = self
}
// Delegate function
func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
let media = mediaItemCollection.items[0]
let title = media.title ?? "unknown title"
let artist = media.artist ?? "unknown artist"
let albumTitle = media.albumTitle ?? "unknown album title"
let duration = media.playbackDuration
let albumArtImage = media.artwork?.image(at: .zero)
let metadata = MediaMetadata(title: title, artist: artist, albumTitle: albumTitle, duration: duration, albumArtImage: albumArtImage)
// 방법 1: system music player로 재생
let musicPlayer = MPMusicPlayerController.systemMusicPlayer
musicPlayer.setQueue(with: mediaItemCollection)
musicPlayer.play()
// 데이터 밖으로 내보내기
metadataCallback(metadata)
mediaPicker.dismiss(animated: true)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
