如何修改现有SwiftUI代码实现图片任意位置捏合、双击缩放功能
解决方案
核心问题原因
- 现有代码仅在未缩放状态下通过滑动关闭关联的拖动手势捕获点击位置,捏合手势完全没有更新缩放锚点,导致捏合时默认使用初始坐标(即视图中心)作为缩放基点
- 坐标转换采用全局坐标系计算
UnitPoint,没有适配aspectRatio(contentMode: .fit)带来的图片上下/左右留白场景,坐标映射存在偏差 - 双击手势未捕获当前点击位置,每次双击都沿用旧的锚点坐标
修复调整点
- 为双击手势新增点击位置捕获逻辑,每次双击触发时都更新缩放锚点
- 为捏合手势新增起始触点位置捕获,捏合开始时自动更新锚点为捏合触发位置
- 调整坐标转换逻辑为基于图片视图的本地坐标系计算缩放锚点,消除图片留白带来的坐标偏差
- 移除
isZoomed状态对锚点更新的限制,缩放状态下触发新的手势也能正确更新锚点
完整可运行代码
import SwiftUI public struct SampleZoom { @ObservedObject private var viewModel: ViewModel @State private var currentZoom: CGFloat = 0 @State private var endingZoom: CGFloat = 1 @State private var isZoomed = false @State private var pointTapped: CGPoint = .zero @GestureState var swipeOffset: CGSize = .zero public init(viewModel: ViewModel) { self.viewModel = viewModel } } extension SampleZoom: View { public var body: some View { if viewModel.isVisible { ZStack { Color.black .opacity(viewModel.bgOpacity) .edgesIgnoringSafeArea(.all) image(imageData: viewModel.image) } .onAppear { endingZoom = 1 isZoomed = false pointTapped = .zero } .overlay( /// Close Button Button(action: { viewModel.isVisible.toggle() }, label: { Image(systemName: "xmark") .foregroundColor(.white) .padding() .background(Color.white.opacity(0.33)) .clipShape(Circle()) }) .padding(Spacing.standard), alignment: .topLeading ) } else { Spacer() } } } private extension SampleZoom { /// Creates card Image func image(imageData: Data) -> some View { GeometryReader { reader in Image(uiImage: UIImage(data: imageData) ?? UIImage()) .resizable() .aspectRatio(contentMode: .fit) .offset(y: viewModel.imageOffset.height) .animation(.default, value: viewModel.imageOffset) .frame(maxWidth: .infinity, maxHeight: .infinity) // 修正缩放锚点计算,使用本地坐标系 .scaleEffect(max(endingZoom + currentZoom, 1), anchor: UnitPoint( x: pointTapped.x / reader.size.width, y: pointTapped.y / reader.size.height )) /// 双击缩放 + 滑动关闭手势 .gesture( // 捕获双击位置 TapGesture(count: 2) .simultaneously(with: DragGesture(minimumDistance: 0, coordinateSpace: .local)) .onEnded { value in guard let tapLocation = value.second?.startLocation else { return } pointTapped = tapLocation withAnimation { isZoomed.toggle() endingZoom = endingZoom > 1 ? 1 : 2 } } .simultaneously( with: DragGesture(minimumDistance: 0, coordinateSpace: .global) .updating($swipeOffset) { value, outValue, _ in // 仅未缩放时允许滑动关闭 guard !isZoomed else { return } outValue = value.translation viewModel.onChange(imagePosition: swipeOffset) } .onEnded { value in guard !isZoomed else { return } viewModel.onEnd(swipeDistance: value) } ) ) /// 捏合缩放手势 .gesture( MagnificationGesture() .simultaneously(with: DragGesture(minimumDistance: 0, coordinateSpace: .local)) .onChanged { value in // 捏合开始时更新锚点 if let dragLocation = value.second?.startLocation, currentZoom == 0 { pointTapped = dragLocation } currentZoom = value.first - 1 } .onEnded { _ in endingZoom += currentZoom currentZoom = 0 isZoomed = endingZoom > 1 } ) } } } public extension SampleZoom { final class ViewModel: ObservableObject { let image: Data @Published var isVisible: Bool @Published var imageOffset: CGSize = .zero @Published var bgOpacity: Double = 1 func onChange(imagePosition: CGSize) { imageOffset = imagePosition let halfScreenHeight = UIScreen.main.bounds.height / 2 let progress = imagePosition.height / halfScreenHeight withAnimation(.default) { bgOpacity = Double(1 - abs(progress)) } } func onEnd(swipeDistance: DragGesture.Value) { withAnimation(.easeOut) { let translation = abs(swipeDistance.translation.height) if translation < Spacing.doubleExtraLarge * 3 { imageOffset.height = .zero bgOpacity = 1 } else { isVisible.toggle() imageOffset.height = .zero bgOpacity = 1 } } } init(image: Data, isVisible: Bool) { self.image = image self.isVisible = isVisible } } } // 补充缺失的Spacing常量定义 enum Spacing { static let standard: CGFloat = 16 static let none: CGFloat = 0 static let doubleExtraLarge: CGFloat = 32 }
额外优化点
- 修复了图片加载逻辑,直接使用传入的
imageData生成图片,替换了原来的占位字符串 - 给
animation修饰符增加了value参数,适配新版SwiftUI的动画触发规则,避免无用动画触发 - 增加了缩放状态下的滑动关闭拦截,避免缩放后拖动图片误触发关闭逻辑
- 简化了透明度计算逻辑,用
abs()替代了原有正负值判断 - 补充了缺失的
Spacing枚举定义,确保代码可直接运行
内容的提问来源于stack exchange,提问作者Person 16489461
相关产品推荐
相关产品推荐

