diff --git a/SodaLive/Sources/ImagePicker/ImagePicker.swift b/SodaLive/Sources/ImagePicker/ImagePicker.swift index ef2743d..5319697 100644 --- a/SodaLive/Sources/ImagePicker/ImagePicker.swift +++ b/SodaLive/Sources/ImagePicker/ImagePicker.swift @@ -83,7 +83,6 @@ private enum CropHandle: CaseIterable, Identifiable { struct ImageCropEditorView: View { - let image: UIImage let aspectPolicy: ImageCropAspectPolicy let onCancel: () -> Void let onComplete: (UIImage) -> Void @@ -103,11 +102,10 @@ struct ImageCropEditorView: View { onCancel: @escaping () -> Void, onComplete: @escaping (UIImage) -> Void ) { - self.image = image self.aspectPolicy = aspectPolicy self.onCancel = onCancel self.onComplete = onComplete - self.normalizedImage = image.normalizedForCrop() + self.normalizedImage = image } var body: some View { @@ -146,7 +144,7 @@ struct ImageCropEditorView: View { ZStack { Color.black - Image(uiImage: image) + Image(uiImage: normalizedImage) .resizable() .scaledToFit() .scaleEffect(scale) @@ -390,7 +388,12 @@ struct ImageCropEditorView: View { y: cropYInScaled * yRatio, width: cropSize.width * xRatio, height: cropSize.height * yRatio - ).integral + ) + + cropRect.origin.x = floor(cropRect.origin.x) + cropRect.origin.y = floor(cropRect.origin.y) + cropRect.size.width = floor(cropRect.size.width) + cropRect.size.height = floor(cropRect.size.height) cropRect.origin.x = cropRect.origin.x.clamped(min: 0, max: max(0, normalizedImage.size.width - 1)) cropRect.origin.y = cropRect.origin.y.clamped(min: 0, max: max(0, normalizedImage.size.height - 1)) @@ -416,14 +419,29 @@ struct ImageCropEditorView: View { } extension UIImage { - func normalizedForCrop() -> UIImage { - if imageOrientation == .up { + func normalizedForCrop(maxDimension: CGFloat = 2048) -> UIImage { + guard size.width > 0, size.height > 0 else { return self } - let renderer = UIGraphicsImageRenderer(size: size) + let largestSide = max(size.width, size.height) + let scaleRatio: CGFloat + + if maxDimension > 0, largestSide > maxDimension { + scaleRatio = maxDimension / largestSide + } else { + scaleRatio = 1 + } + + let targetWidth = max(1, floor(size.width * scaleRatio)) + let targetHeight = max(1, floor(size.height * scaleRatio)) + let targetSize = CGSize(width: targetWidth, height: targetHeight) + let format = UIGraphicsImageRendererFormat.default() + format.scale = 1 + let renderer = UIGraphicsImageRenderer(size: targetSize, format: format) + return renderer.image { _ in - draw(in: CGRect(origin: .zero, size: size)) + draw(in: CGRect(origin: .zero, size: targetSize)) } } @@ -438,7 +456,9 @@ extension UIImage { } let scaleRatio = maxDimension / largestSide - let targetSize = CGSize(width: size.width * scaleRatio, height: size.height * scaleRatio) + let targetWidth = max(1, floor(size.width * scaleRatio)) + let targetHeight = max(1, floor(size.height * scaleRatio)) + let targetSize = CGSize(width: targetWidth, height: targetHeight) let format = UIGraphicsImageRendererFormat.default() format.scale = 1 let renderer = UIGraphicsImageRenderer(size: targetSize, format: format) diff --git a/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift b/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift index 0b62c3a..1ce290a 100644 --- a/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift +++ b/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift @@ -17,6 +17,10 @@ struct LiveRoomViewV2: View { @State private var textHeight: CGFloat = .zero @State private var menuTextHeight: CGFloat = .zero + @State private var selectedPickedImage: UIImage? + @State private var cropSourceImage: UIImage? + @State private var isShowImageCropper = false + @State private var isImageLoading = false // 롱프레스 하트 물 채우기 상태 @State private var isLongPressingHeart: Bool = false @@ -764,6 +768,10 @@ struct LiveRoomViewV2: View { if viewModel.isV2VLoading { LoadingView() } + + if isImageLoading { + LoadingView() + } } .overlay(alignment: .center) { ZStack { @@ -840,10 +848,47 @@ struct LiveRoomViewV2: View { .sheet(isPresented: $viewModel.isShowPhotoPicker) { ImagePicker( isShowing: $viewModel.isShowPhotoPicker, - selectedImage: $viewModel.coverImage, + selectedImage: $selectedPickedImage, sourceType: .photoLibrary ) } + .onChange(of: selectedPickedImage, perform: { newImage in + guard let newImage else { + return + } + + isImageLoading = true + + DispatchQueue.global(qos: .userInitiated).async { + let normalizedImage = newImage.normalizedForCrop() + DispatchQueue.main.async { + isImageLoading = false + selectedPickedImage = nil + cropSourceImage = normalizedImage + isShowImageCropper = true + } + } + }) + .onDisappear { + isImageLoading = false + } + .sheet(isPresented: $isShowImageCropper, onDismiss: { + cropSourceImage = nil + }) { + if let cropSourceImage { + ImageCropEditorView( + image: cropSourceImage, + aspectPolicy: .free, + onCancel: { + isShowImageCropper = false + }, + onComplete: { croppedImage in + viewModel.coverImage = croppedImage + isShowImageCropper = false + } + ) + } + } .sheet(isPresented: $viewModel.isShowEditRoomInfoDialog) { if let liveRoomInfo = viewModel.liveRoomInfo { LiveRoomInfoEditDialog(