feat(image): 이미지 선택 후 크롭 편집 흐름을 안정화한다

This commit is contained in:
Yu Sung
2026-03-17 17:05:22 +09:00
parent 039355c088
commit 606470ae04
2 changed files with 76 additions and 11 deletions

View File

@@ -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)

View File

@@ -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(