feat(image): 이미지 선택 후 크롭 편집 흐름을 안정화한다
This commit is contained in:
@@ -83,7 +83,6 @@ private enum CropHandle: CaseIterable, Identifiable {
|
|||||||
|
|
||||||
struct ImageCropEditorView: View {
|
struct ImageCropEditorView: View {
|
||||||
|
|
||||||
let image: UIImage
|
|
||||||
let aspectPolicy: ImageCropAspectPolicy
|
let aspectPolicy: ImageCropAspectPolicy
|
||||||
let onCancel: () -> Void
|
let onCancel: () -> Void
|
||||||
let onComplete: (UIImage) -> Void
|
let onComplete: (UIImage) -> Void
|
||||||
@@ -103,11 +102,10 @@ struct ImageCropEditorView: View {
|
|||||||
onCancel: @escaping () -> Void,
|
onCancel: @escaping () -> Void,
|
||||||
onComplete: @escaping (UIImage) -> Void
|
onComplete: @escaping (UIImage) -> Void
|
||||||
) {
|
) {
|
||||||
self.image = image
|
|
||||||
self.aspectPolicy = aspectPolicy
|
self.aspectPolicy = aspectPolicy
|
||||||
self.onCancel = onCancel
|
self.onCancel = onCancel
|
||||||
self.onComplete = onComplete
|
self.onComplete = onComplete
|
||||||
self.normalizedImage = image.normalizedForCrop()
|
self.normalizedImage = image
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -146,7 +144,7 @@ struct ImageCropEditorView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
Color.black
|
Color.black
|
||||||
|
|
||||||
Image(uiImage: image)
|
Image(uiImage: normalizedImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.scaleEffect(scale)
|
.scaleEffect(scale)
|
||||||
@@ -390,7 +388,12 @@ struct ImageCropEditorView: View {
|
|||||||
y: cropYInScaled * yRatio,
|
y: cropYInScaled * yRatio,
|
||||||
width: cropSize.width * xRatio,
|
width: cropSize.width * xRatio,
|
||||||
height: cropSize.height * yRatio
|
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.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))
|
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 {
|
extension UIImage {
|
||||||
func normalizedForCrop() -> UIImage {
|
func normalizedForCrop(maxDimension: CGFloat = 2048) -> UIImage {
|
||||||
if imageOrientation == .up {
|
guard size.width > 0, size.height > 0 else {
|
||||||
return self
|
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
|
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 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()
|
let format = UIGraphicsImageRendererFormat.default()
|
||||||
format.scale = 1
|
format.scale = 1
|
||||||
let renderer = UIGraphicsImageRenderer(size: targetSize, format: format)
|
let renderer = UIGraphicsImageRenderer(size: targetSize, format: format)
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ struct LiveRoomViewV2: View {
|
|||||||
|
|
||||||
@State private var textHeight: CGFloat = .zero
|
@State private var textHeight: CGFloat = .zero
|
||||||
@State private var menuTextHeight: 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
|
@State private var isLongPressingHeart: Bool = false
|
||||||
@@ -764,6 +768,10 @@ struct LiveRoomViewV2: View {
|
|||||||
if viewModel.isV2VLoading {
|
if viewModel.isV2VLoading {
|
||||||
LoadingView()
|
LoadingView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isImageLoading {
|
||||||
|
LoadingView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.overlay(alignment: .center) {
|
.overlay(alignment: .center) {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -840,10 +848,47 @@ struct LiveRoomViewV2: View {
|
|||||||
.sheet(isPresented: $viewModel.isShowPhotoPicker) {
|
.sheet(isPresented: $viewModel.isShowPhotoPicker) {
|
||||||
ImagePicker(
|
ImagePicker(
|
||||||
isShowing: $viewModel.isShowPhotoPicker,
|
isShowing: $viewModel.isShowPhotoPicker,
|
||||||
selectedImage: $viewModel.coverImage,
|
selectedImage: $selectedPickedImage,
|
||||||
sourceType: .photoLibrary
|
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) {
|
.sheet(isPresented: $viewModel.isShowEditRoomInfoDialog) {
|
||||||
if let liveRoomInfo = viewModel.liveRoomInfo {
|
if let liveRoomInfo = viewModel.liveRoomInfo {
|
||||||
LiveRoomInfoEditDialog(
|
LiveRoomInfoEditDialog(
|
||||||
|
|||||||
Reference in New Issue
Block a user