You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修改现有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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.17 18:24:10