diff --git a/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/Component/PhotoFramePreview.swift b/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/Component/PhotoFramePreview.swift index bda2abe6..57802094 100644 --- a/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/Component/PhotoFramePreview.swift +++ b/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/Component/PhotoFramePreview.swift @@ -9,10 +9,26 @@ import SwiftUI struct PhotoFramePreview: View { let information: PhotoInformation + private let hasPreloadedPhotoImages: Bool + @State private var photoImagesByURL: [URL: UIImage] + + init(information: PhotoInformation, photoImages: [UIImage?] = []) { + self.information = information + self.hasPreloadedPhotoImages = photoImages.isEmpty == false + _photoImagesByURL = State( + initialValue: Dictionary( + uniqueKeysWithValues: zip(information.photos, photoImages).compactMap { photo, photoImage in + guard let photoImage else { return nil } + return (photo.url, photoImage) + } + ) + ) + } var body: some View { GeometryReader { geometry in let size = geometry.size + let currentPhotoImages = information.photos.map { photoImagesByURL[$0.url] } Canvas { context, _ in let canvas = CGRect(origin: .zero, size: size) @@ -29,9 +45,8 @@ struct PhotoFramePreview: View { context.drawLayer { layer in layer.clip(to: Path(roundedRect: slot, cornerRadius: 5)) - if index < information.photos.count, - let photoData = information.photos[index].imageData, - let photo = UIImage(data: photoData) { + if index < currentPhotoImages.count, + let photo = currentPhotoImages[index] { let target = aspectFillRect(for: photo.size, into: slot) layer.draw(Image(uiImage: photo), in: target) } else { @@ -68,6 +83,21 @@ struct PhotoFramePreview: View { } } .aspectRatio(information.layout.previewAspect, contentMode: .fit) + .task(id: information.photos) { + guard !hasPreloadedPhotoImages else { return } + await synchronizePhotoImages() + } + } + + private func synchronizePhotoImages() async { + let currentPhotos = information.photos + for photo in currentPhotos where photoImagesByURL[photo.url] == nil { + let loadedPhotoImage = await PhotoImageLoader.loadImage(from: photo.url) + guard Task.isCancelled == false else { return } + guard let loadedPhotoImage else { continue } + + photoImagesByURL[photo.url] = loadedPhotoImage + } } /// Cliping 될 때 크기에 맞게 잘 잘리도록 전처리 diff --git a/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/Model/Photo.swift b/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/Model/Photo.swift index 61a027c3..dc40515b 100644 --- a/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/Model/Photo.swift +++ b/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/Model/Photo.swift @@ -13,10 +13,6 @@ struct Photo: Identifiable, Hashable { let url: URL var selectNumber: Int? - var imageData: Data? { - return try? Data(contentsOf: url) - } - static func == (lhs: Photo, rhs: Photo) -> Bool { lhs.id == rhs.id } diff --git a/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/PhotoComposer.swift b/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/PhotoComposer.swift index 0c66a27c..4352fd53 100644 --- a/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/PhotoComposer.swift +++ b/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/PhotoComposer.swift @@ -8,14 +8,25 @@ import SwiftUI struct PhotoComposer { + static func render(with information: PhotoInformation) async -> UIImage? { + let photoImages = await PhotoImageLoader.loadImages(from: information.photos) + // CI의 Xcode 16.4는 Default Actor Isolation을 `nonisolated`이 기본 설정 + // Xcode 버전이 26.* 이라면 Default Actor Isolation을 `MainActor`이 기본 설정 + // CI와 로컬 환경의 버전이 달라 임시로 CI 환경에 맞는 형태로 코드를 작성하였음 + return await renderPreview(with: information, photoImages: photoImages) + } + @MainActor - static func render(with information: PhotoInformation) -> UIImage? { + private static func renderPreview(with information: PhotoInformation, photoImages: [UIImage?]) -> UIImage? { // 출력 이미지 크기 결정 (예: 1080 x 1440 또는 레이아웃 비율에 맞춤) let targetWidth: CGFloat = 1080 let targetHeight: CGFloat = targetWidth / information.layout.previewAspect let targetSize = CGSize(width: targetWidth, height: targetHeight) - let preview = PhotoFramePreview(information: information) + let preview = PhotoFramePreview( + information: information, + photoImages: photoImages + ) .frame(width: targetSize.width, height: targetSize.height) let renderer = ImageRenderer(content: preview) diff --git a/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/PhotoImageLoader.swift b/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/PhotoImageLoader.swift new file mode 100644 index 00000000..4963470f --- /dev/null +++ b/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoComposition/PhotoImageLoader.swift @@ -0,0 +1,35 @@ +// +// PhotoImageLoader.swift +// mirroringBooth +// +// Created by 최윤진 on 2026-03-09. +// + +import UIKit + +enum PhotoImageLoader { + static func loadImages(from photos: [Photo]) async -> [UIImage?] { + var loadedPhotoImages = [UIImage?](repeating: nil, count: photos.count) + + await withTaskGroup(of: (Int, UIImage?).self) { taskGroup in + for (index, photo) in photos.enumerated() { + taskGroup.addTask { + let loadedPhotoImage = await loadImage(from: photo.url) + return (index, loadedPhotoImage) + } + } + + for await (index, loadedPhotoImage) in taskGroup { + loadedPhotoImages[index] = loadedPhotoImage + } + } + + return loadedPhotoImages + } + + static func loadImage(from photoUrl: URL) async -> UIImage? { + await Task.detached(priority: .userInitiated) { + UIImage(contentsOfFile: photoUrl.path(percentEncoded: false)) + }.value + } +} diff --git a/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoResult/ResultView.swift b/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoResult/ResultView.swift index 4fbd7cbb..b7855396 100644 --- a/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoResult/ResultView.swift +++ b/mirroringBooth/mirroringBooth/Device/Mirroring/PhotoResult/ResultView.swift @@ -150,7 +150,7 @@ struct ResultView: View { } .task { if let photoInformation = store.state.resultPhoto { - guard let image = PhotoComposer.render(with: photoInformation) else { return } + guard let image = await PhotoComposer.render(with: photoInformation) else { return } store.send(.setRenderedImage(image: image)) } }