From f3653c561717b4f0412e2e8c12e4f75c3f1f1f23 Mon Sep 17 00:00:00 2001 From: cheykrym Date: Wed, 10 Dec 2025 02:59:17 +0300 Subject: [PATCH] photo view edit --- yobble/Resources/Localizable.xcstrings | 3 + .../Views/Tab/Settings/EditProfileView.swift | 155 +++++++++++++----- 2 files changed, 120 insertions(+), 38 deletions(-) diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 7eed751..6b46b46 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -76,6 +76,9 @@ } } } + }, + "1 из 1" : { + }, "2FA включена" : { "comment" : "Заголовок уведомления об успешной активации 2FA" diff --git a/yobble/Views/Tab/Settings/EditProfileView.swift b/yobble/Views/Tab/Settings/EditProfileView.swift index 1e6ec66..7173997 100644 --- a/yobble/Views/Tab/Settings/EditProfileView.swift +++ b/yobble/Views/Tab/Settings/EditProfileView.swift @@ -408,48 +408,68 @@ struct AvatarViewerView: View { let onDownload: () -> Void let onDelete: () -> Void + @State private var scale: CGFloat = 1.0 + @State private var baseScale: CGFloat = 1.0 + @State private var panOffset: CGSize = .zero + @State private var storedPanOffset: CGSize = .zero + @State private var dismissOffset: CGSize = .zero + + private var currentOffset: CGSize { + scale > 1.05 ? panOffset : dismissOffset + } + var body: some View { - if #available(iOS 16.0, *) { - NavigationStack { - ZStack { - Color.black.ignoresSafeArea() - avatarContent - } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button(action: onClose) { - Image(systemName: "xmark") - } - .tint(.white) - } - ToolbarItem(placement: .navigationBarTrailing) { - Menu { - Button(action: onDownload) { - Label(NSLocalizedString("Скачать", comment: "Avatar download"), systemImage: "square.and.arrow.down") - } - Button(role: .destructive, action: onDelete) { - Label(NSLocalizedString("Удалить фото", comment: "Avatar delete"), systemImage: "trash") - } - } label: { - Image(systemName: "ellipsis.circle") - .imageScale(.large) - } - .tint(.white) - } - } + ZStack { + Color.black.ignoresSafeArea() + + zoomableContent + + topOverlay } } + + private var topOverlay: some View { + VStack(spacing: 8) { + HStack { + Button(action: onClose) { + Image(systemName: "xmark") + .imageScale(.large) + } + .tint(.white) + + Spacer() + + Text("1 из 1") + .font(.subheadline.weight(.semibold)) + .foregroundColor(.white.opacity(0.8)) + + Spacer() + + Menu { + Button(action: onDownload) { + Label(NSLocalizedString("Скачать", comment: "Avatar download"), systemImage: "square.and.arrow.down") + } + Button(role: .destructive, action: onDelete) { + Label(NSLocalizedString("Удалить фото", comment: "Avatar delete"), systemImage: "trash") + } + } label: { + Image(systemName: "ellipsis.circle") + .imageScale(.large) + } + .tint(.white) + } + .padding(.horizontal) + .padding(.top, 24) + + Spacer() + } } @ViewBuilder - private var avatarContent: some View { + private var zoomableContent: some View { switch state.source { case .local(let image): - Image(uiImage: image) - .resizable() - .scaledToFit() - .padding() + zoomableImage(Image(uiImage: image)) case .remote(let url, _, _): AsyncImage(url: url) { phase in switch phase { @@ -457,10 +477,7 @@ struct AvatarViewerView: View { ProgressView() .progressViewStyle(.circular) case .success(let image): - image - .resizable() - .scaledToFit() - .padding() + zoomableImage(image) case .failure: Image(systemName: "person.crop.circle.badge.exclam") .resizable() @@ -473,6 +490,68 @@ struct AvatarViewerView: View { } } } + + private func zoomableImage(_ image: Image) -> some View { + image + .resizable() + .scaledToFit() + .offset(currentOffset) + .scaleEffect(scale, anchor: .center) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .gesture(dragGesture) + .simultaneousGesture(magnificationGesture) + } + + private var dragGesture: some Gesture { + DragGesture() + .onChanged { value in + if scale > 1.05 { + dismissOffset = .zero + panOffset = CGSize( + width: storedPanOffset.width + value.translation.width, + height: storedPanOffset.height + value.translation.height + ) + } else { + dismissOffset = value.translation + } + } + .onEnded { value in + if scale > 1.05 { + storedPanOffset = CGSize( + width: storedPanOffset.width + value.translation.width, + height: storedPanOffset.height + value.translation.height + ) + } else { + if abs(value.translation.height) > 120 { + onClose() + } else { + withAnimation(.spring()) { + dismissOffset = .zero + } + } + } + } + } + + private var magnificationGesture: some Gesture { + MagnificationGesture() + .onChanged { value in + let newScale = baseScale * value + scale = min(max(newScale, 1), 4) + } + .onEnded { _ in + baseScale = scale + if baseScale <= 1.02 { + baseScale = 1 + withAnimation(.spring()) { + scale = 1 + storedPanOffset = .zero + panOffset = .zero + dismissOffset = .zero + } + } + } + } } struct ActivityView: UIViewControllerRepresentable {