[iOS] 앨범에서 비디오를 앱으로 가져올 때 생길 수 있는 문제 상황

taekki
daily-monster
Published in
9 min readMar 15, 2024
Photo by Jakob Owens on Unsplash

안녕하세요, 금요괴물 태끼입니다. 오늘은 iOS 개발 시 앨범에서 비디오를 앱으로 가져오면서 생겼던 문제 상황에 대해 공유를 해보고자 합니다. 어려운 내용은 아니니 가볍게 들어주세요!

우선 비디오를 앱으로 가져오기 위해 다음 글에서는 PhotoKit APITransferable 프로토콜을 사용합니다. Transferable 프로토콜에 대한 이야기는 Meet Transferable — WWDC22 세션에서 더 자세하게 만나보실 수 있습니다.

visionOS 버전 1.1에서 실행해보겠습니다. 다른 플랫폼에서도 수행가능합니다. 우리가 원하는 UI 구성은 다음과 같습니다. 비디오를 가져오면 재생할 비디오 플레이어와 앨범을 열 ‘Select a Video’ 버튼이죠.

/// ...
@State private var photosPickerItem: PhotosPickerItem?
/// ...
/// 1. UI
VStack {
VideoPlayer(player: player)

PhotosPicker(
"Select a video",
selection: $photosPickerItem,
matching: .videos
)
}

앨범에서 선택한 항목을 바인딩할 photosPickerItem 변수를 선언하고, PhotosPicker 뷰와 연결해줍니다. 프로그래밍 방식으로 앨범을 열었다 닫았다가 할 수도 있는데 그 부분이 궁금하시면 Documents를 좀 더 찾아보시는 것을 추천드립니다. 우선 UI 구성은 이게 다입니다.

그리고 앨범에서 비디오를 선택하면 비디오 플레이어에 비디오가 담기게 되는 흐름인데요.

/// 2. Load Video
private func loadVideo(_ item: PhotosPickerItem) async throws -> URL? {
guard let video = try await item.loadTransferable(type: VideoTransferable.self) else {
return nil
}
return video.url
}

앨범에 가져온 아이템, PhotosPickerItem 유형의 데이터를 앱에서 사용하기 위해서는 loadTransferable() 메서드를 사용해야 합니다. 그런데 파라미터를 잘 살펴보면, Transferable이라는 프로토콜을 준수해야 하는 것을 알 수 있습니다.

String, Image, … 등 많은 표준 타입이 Transferable을 준수하고 있지만, 우리가 원하는 비디오라는 타입은 없기 때문에 우리가 직접 데이터 타입을 만들어주어야 합니다.

struct VideoTransferable: Transferable {
let url: URL

static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .movie) { exporting in
return SentTransferredFile(exporting.url)
} importing: { received in
let origin = received.file
let filename = origin.lastPathComponent
let copied = URL.documentsDirectory.appendingPathComponent(filename)
let filePath = copied.path()

/// If file exists, delete a file.
if FileManager.default.fileExists(atPath: filePath) {
try FileManager.default.removeItem(atPath: filePath)
}

try FileManager.default.copyItem(at: origin, to: copied)
return VideoTransferable(url: copied)
}
}
}

아래와 같이 커스텀 타입을 만들어주고 Transferable 프로토콜을 채택해주면 선언적으로 전달 가능한 타입을 만들어줄 수 있습니다. 정말 간단합니다. 이렇게 되면 앱 내에서의 데이터 전달, 앱과 앱 사이의 데이터 전달이 가능해집니다.

importing 클로저 부분만 살펴보자면 원본 소스의 주소로 접근해서 앱의 Sandbox 내 파일 시스템에 비디오 소스를 복사하는 것을 확인할 수 있습니다. 이후에 우리는 복사해 온 소스의 경로에 접근하여 비디오를 사용하게 되는 것이죠.

이제 Transferable을 준수하는 데이터 유형도 만들었으니, 비디오 플레이어에 비디오만 넣어주면 끝입니다.

/// 3. Observing
.onChange(of: photosPickerItem) { oldValue, newValue in
if let newValue {
Task {
do {
if let url = try await loadVideo(newValue) {
let playerItem = AVPlayerItem(url: url)
player.replaceCurrentItem(with: playerItem)
}
} catch {
errorMessage = error.localizedDescription
showAlert.toggle()
}
}
}
}

아까 선언해둔 photosPickerItem을 관찰하고 있다가 새로운 아이템이 선택되면 그 아이템의 URL로 접근해서 비디오를 로드해옵니다. (내부적으로는 위에서 선언해 둔 데이터 타입에 따라 앨범의 아이템 소스를 파일 시스템으로 복사해오고, 그 파일 시스템의 소스 경로로 접근하고 있는 형태입니다.)

/// 4. Alert
.alert(errorMessage ?? "", isPresented: $showAlert, actions: {})

그런 뒤에 제대로 비디오를 가져오지 못하면 alert를 띄우는 것으로 간단한 예제를 마무리 할 수 있습니다.

위의 흐름에는 문제가 없는 것 같습니다. 그러나 특정 아이템을 가져오는 상황에서 문제 상황이 발생했었습니다. 최초에는 잘 가져오다가 두 번째부터는 문제가 생겨서 못 가져오더라구요.

첫 번째 비디오를 처음에는 잘 불러오다가 두 번째부터 불러오려고 하면 불러오지 못하는 문제가 발생했습니다.

로그를 살펴보니 로딩하는데 문제가 있네요. URL을 정상적으로 decode 해오지 못하는 것 같습니다.

영상이 저장되어있는 경로를 찾아가보니까 파일 이름에 공백이 있음을 확인할 수 있습니다. 공백이 있는 경우에 에러가 발생할 수 있습니다. 그렇기에 공백을 특정 문자(%20, _ 등)으로 치환을 해주면 문제가 해결이 되었습니다.

그러나 이것이 50% 답안입니다. 눈썰미가 좋으신 분들은 위의 사진에서 같은 영상이 2개 저장되어 있는 것을 보실 수 있으실 겁니다. 앨범에서 파일을 불러올 때마다 영상이 쌓이는 것이 맞을까요? 단순히 앨범에서 가져와 우리 서버로 업로드하는 과정에서 사용한 비디오를 앱 내에 용량을 차지하게 두는 것이 맞을까요?

아닐 것입니다. 그래서 파일의 사용이 끝났다면 제거해주는 작업이 필요합니다.

private func removeAllFilesInDocumentsDirectory() {
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!

do {
// Documents 디렉터리 내의 모든 항목을 가져옵니다.
let fileURLs = try fileManager.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil)

// 각 파일에 대해 반복하며 제거합니다.
for fileURL in fileURLs {
try fileManager.removeItem(at: fileURL)
}
print("모든 파일이 성공적으로 제거되었습니다.")
} catch {
// 오류 처리
print("파일 제거 중 오류 발생: \(error)")
}
}

저는 위와 같은 코드로 Documents 디렉터리를 조회해서 영상을 지워주는 방식을 택했습니다. 그러나 이것이 최선의 답은 아닙니다. 삭제하면 안되는 영상이 있을 수도 있기에 상황에 따라 적절한 방식을 택하는 것을 추천드립니다.

이렇게 오늘은 앨범에서 비디오를 가져오는 경우에 발생할 수 있는 문제 상황을 공유해보았습니다. 결국에는 URL이 문제였네요 ㅎㅎ

오늘도 글 읽어주셔서 감사합니다 :)

예제 코드의 원본은 아래에 첨부하겠습니다.

https://gist.github.com/Taehyeon-Kim/ced4492187d12a4b38440606d0c0c9aa

--

--