시리즈: SwiftUI Representable
- SwiftUI: Representable을 이용해서 UIViewController 띄우기
- SwiftUI: 하드웨어 키보드 입력 받기 (Representable 사용)
- SwiftUI: 웹 뷰(WKWebView) 추가하기 및 자바스크립트 실행 (Representable 사용)
소개
SwiftUI 환경에서 UIKit을 이용해 만든 UIViewController를 띄우는 방법에 대한 소개입니다.
예를 들어, MPMediaPickerController (import MediaPlayer 필요)는 UIViewController를 상속받은 뷰 컨트롤러인데 UIKit 환경에서는 self.present(viewController, ...)로 바로 띄울 수 있습니다. SwiftUI에서도 이 작업이 가능하지만 복잡한 과정이 필요합니다.
방법
1: UIViewControllerRepresentable을 준수하는 구조체 생성
아래와 같이 UIViewControllerRepresentable를 준수(conform)하는 구조체(struct)를 생성합니다.
UIViewControllerRepresentable는 UIKit의 뷰 컨트롤러를 나타내는 View입니다.
일반적으로 [컨트롤러 이름] + View 라는 이름으로 구조체 이름을 작성하지만 여기서는 편의를 위해 임의로 MPMediaPickerControllerRP라는 이름으로 작성했습니다.
struct MPMediaPickerControllerRP: UIViewControllerRepresentable {
// ... //
}
2: typealias로 나타내고자 하는 UIViewController 지정
MPMediaPickerControllerRP 구조체 안에 typealias를 추가합니다.
typealias UIViewControllerType = MPMediaPickerController
3: makeUIViewController와 updateUIViewController 추가
- makeUIViewController
UIViewController를 생성하고 초기화를 수행하는 함수입니다.
- updateUIViewController
UIViewController의 업데이트가 필요할 때 호출됩니다.- 이 위치에서
UIViewController에 필요한 데이터 또는 정보를 갱신해야 합니다.
이 예제에서는 makeUIViewController만 사용합니다.
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가 나타나도록 하기
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)
sheet란 아래와 같이 화면이 완전히 겹쳐지지 않고 일부분만 겹친 형태의 팝업을 말합니다.

@State변수인isOpenMusicPickerView가true가 되면 뷰 컨트롤러가 팝업됩니다.
Delegate 사용하기
MPMediaPickerController에는 다음과 같은 delegate 함수가 있습니다.
func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
// ... //
}
앞의 섹션에서 뷰 컨트롤러를 띄우는 것 까지는 성공했는데, delegate는 어떻게 처리해야 할까요?
Coordinator라는 것을 이용하여 처리할 수 있습니다.
구체적인 방법은 다음과 같습니다.
사전 작업
노래의 메타데이터를 담은 구조체 MediaMetadata를 생성합니다.
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 연결
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() 함수 추가
// Representable에서 delegate 사용
func makeCoordinator() -> Coordinator {
Coordinator(picker, metadataCallback: metadataCallback)
}
- 여기서 반환된
Coordinator인스턴스를 통해 외부에서도delegate를 사용할 수 있게 됩니다.
5: MPMediaPickerControllerRP()에 metadataCallback 부분 (트레일링 클로저) 추가하기
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)를 이용해 해야 할 작업을 처리합니다.
전체 코드 (ContentView.swift)
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()
}
}






0개의 댓글