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

如何提升SwiftUI中拖拽手势缩放视图的性能?

SwiftUI拖拽缩放性能优化问题

缩放卡顿演示

我尝试使用拖拽手势让用户既能更改视图位置,又能缩放视图。拖拽移动视图的功能运行正常,但缩放时性能表现很差,我找不到问题所在。我尝试添加drawingGroup()修饰符来提升性能,但并没有明显效果。不过我对此也不意外,因为即使是移动单个非常简单的视图,性能也很差。我认为自己可能对SwiftUI的工作原理存在误解,该问题出现在2018款Core i3 Mac Mini设备上。

原始代码

@main
struct App: App {
    
    @StateObject private var model = TestModel()
    
    let info = CardComponentInfo(type: .text, origin: .init(x: 300, y: 300), size: .init(width: 200, height: 200))
    
    var body: some Scene {
        WindowGroup("Ensemble") {
            ZStack {
                Color.white
                TestComponentView(info: info, model: model)
            }
            .frame(width: 1000, height: 1000)
        }
    }
}

struct TestComponentView: View {
    
    var info: CardComponentInfo
    @ObservedObject var model: TestModel
    
    var body: some View {
        ZStack {
            Label(info.type.title, systemImage: info.type.systemImageName)
            ResizingControlsView { point, deltaX, deltaY in
                model.resizedComponentInfo = info
                model.updateForResize(using: point, deltaX: deltaX, deltaY: deltaY)
                //model.updateForResize(point: point, deltaX: deltaX, deltaY: deltaY) // other udpateForResize may work
            } dragEnded: {
                model.resizeEnded()
            }
        }
        .frame(
            width: model.widthForCardComponent(info: info),
            height: model.heightForCardComponent(info: info)
        )
        .background(info.type.color)
        .position(
            x: model.xPositionForCardComponent(info: info),
            y: model.yPositionForCardComponent(info: info)
        )
        .gesture(
            DragGesture()
                .onChanged { gesture in
                    model.draggedComponentInfo = info
                    model.updateForDrag(deltaX: gesture.translation.width, deltaY: gesture.translation.height)
                }
                .onEnded { _ in
                    model.dragEnded()
                }
        )
    }
}

class TestModel: ObservableObject {
    
    @Published var componentInfos: [CardComponentInfo] = []
    
    @Published var draggedComponentInfo: CardComponentInfo? = nil
    @Published var dragOffset: CGSize? = nil
    
    @Published var selectedComponentInfo: CardComponentInfo? = nil
    
    @Published var selectedTypeToAdd: CardComponentViewType? = nil
    @Published var componentBeingAddedInfo: CardComponentInfo? = nil
    
    @Published var resizedComponentInfo: CardComponentInfo? = nil
    @Published var resizeOffset: CGSize? = nil
    @Published var resizePoint: ResizePoint? = nil

    func widthForCardComponent(info: CardComponentInfo) -> CGFloat {
        let widthOffset = (resizedComponentInfo?.id == info.id) ? (resizeOffset?.width ?? 0.0) : 0.0
        return info.size.width + widthOffset
    }
    
    func heightForCardComponent(info: CardComponentInfo) -> CGFloat {
        let heightOffset = (resizedComponentInfo?.id == info.id) ? (resizeOffset?.height ?? 0.0) : 0.0
        return info.size.height + heightOffset
    }
    
    func xPositionForCardComponent(info: CardComponentInfo) -> CGFloat {
        let xPositionOffset = (draggedComponentInfo?.id == info.id) ? (dragOffset?.width ?? 0.0) : 0.0
        return info.origin.x + (info.size.width / 2.0) + xPositionOffset
    }
    
    func yPositionForCardComponent(info: CardComponentInfo) -> CGFloat {
        let yPositionOffset = (draggedComponentInfo?.id == info.id) ? (dragOffset?.height ?? 0.0) : 0.0
        return info.origin.y + (info.size.height / 2.0) + yPositionOffset
    }
    
    func updateForResize(point: ResizePoint, deltaX: CGFloat, deltaY: CGFloat) {
        resizeOffset = CGSize(width: deltaX, height: deltaY)
        resizePoint = resizePoint
    }
    
    func resizeEnded() {
        guard let resizedComponentInfo, let resizePoint, let resizeOffset else { return }
        var w: CGFloat = resizedComponentInfo.size.width
        var h: CGFloat = resizedComponentInfo.size.height
        var x: CGFloat = resizedComponentInfo.origin.x
        var y: CGFloat = resizedComponentInfo.origin.y
        switch resizePoint {
        case .topLeft:
            w -= resizeOffset.width
            h -= resizeOffset.height
            x += resizeOffset.width
            y += resizeOffset.height
        case .topMiddle:
            h -= resizeOffset.height
            y += resizeOffset.height
        case .topRight:
            w += resizeOffset.width
            h -= resizeOffset.height
        case .rightMiddle:
            w += resizeOffset.width
        case .bottomRight:
            w += resizeOffset.width
            h += resizeOffset.height
        case .bottomMiddle:
            h += resizeOffset.height
        case .bottomLeft:
            w -= resizeOffset.width
            h += resizeOffset.height
            x -= resizeOffset.width
            y += resizeOffset.height
        case .leftMiddle:
            w -= resizeOffset.width
            x += resizeOffset.width
        }
        resizedComponentInfo.size = CGSize(width: w, height: h)
        resizedComponentInfo.origin = CGPoint(x: x, y: y)
        self.resizeOffset = nil
        self.resizePoint = nil
        self.resizedComponentInfo = nil
    }
    
    func updateForDrag(deltaX: CGFloat, deltaY: CGFloat) {
        dragOffset = CGSize(width: deltaX, height: deltaY)
    }
    
    func dragEnded() {
        guard let dragOffset else { return }
        draggedComponentInfo?.origin.x += dragOffset.width
        draggedComponentInfo?.origin.y += dragOffset.height
        draggedComponentInfo = nil
        self.dragOffset = nil
    }
    
    func updateForResize(using resizePoint: ResizePoint, deltaX: CGFloat, deltaY: CGFloat) {
        
        guard let resizedComponentInfo else { return }
        
        var width: CGFloat = resizedComponentInfo.size.width
        var height: CGFloat = resizedComponentInfo.size.height
        var x: CGFloat = resizedComponentInfo.origin.x
        var y: CGFloat = resizedComponentInfo.origin.y
        switch resizePoint {
        case .topLeft:
            width -= deltaX
            height -= deltaY
            x += deltaX
            y += deltaY
        case .topMiddle:
            height -= deltaY
            y += deltaY
        case .topRight:
            width += deltaX
            height -= deltaY
            y += deltaY
            print(width, height, x)
        case .rightMiddle:
            width += deltaX
        case .bottomRight:
            width += deltaX
            height += deltaY
        case .bottomMiddle:
            height += deltaY
        case .bottomLeft: //
            width -= deltaX
            height += deltaY
            x += deltaX
        case .leftMiddle:
            width -= deltaX
            x += deltaX
        }
        resizedComponentInfo.size = CGSize(width: width, height: height)
        resizedComponentInfo.origin = CGPoint(x: x, y: y)
    }
}

enum ResizePoint {
    case topLeft, topMiddle, topRight, rightMiddle, bottomRight, bottomMiddle, bottomLeft, leftMiddle
}

struct ResizingControlsView: View {
    
    let borderColor: Color = .white
    let fillColor: Color = .blue
    let diameter: CGFloat = 15.0
    let dragged: (ResizePoint, CGFloat, CGFloat) -> Void
    let dragEnded: () -> Void
    
    var body: some View {
        VStack(spacing: 0.0) {
            HStack(spacing: 0.0) {
                grabView(resizePoint: .topLeft)
                Spacer()
                grabView(resizePoint: .topMiddle)
                Spacer()
                grabView(resizePoint: .topRight)
            }
            Spacer()
            HStack(spacing: 0.0) {
                grabView(resizePoint: .leftMiddle)
                Spacer()
                grabView(resizePoint: .rightMiddle)
            }
            Spacer()
            HStack(spacing: 0.0) {
                grabView(resizePoint: .bottomLeft)
                Spacer()
                grabView(resizePoint: .bottomMiddle)
                Spacer()
                grabView(resizePoint: .bottomRight)
            }
        }
    }
    
    private func grabView(resizePoint: ResizePoint) -> some View {
        var offsetX: CGFloat = 0.0
        var offsetY: CGFloat = 0.0
        let halfDiameter = diameter / 2.0
        switch resizePoint {
        case .topLeft:
            offsetX = -halfDiameter
            offsetY = -halfDiameter
        case .topMiddle:
            offsetY = -halfDiameter
        case .topRight:
            offsetX = halfDiameter
            offsetY = -halfDiameter
        case .rightMiddle:
            offsetX = halfDiameter
        case .bottomRight:
            offsetX = +halfDiameter
            offsetY = halfDiameter
        case .bottomMiddle:
            offsetY = halfDiameter
        case .bottomLeft:
            offsetX = -halfDiameter
            offsetY = halfDiameter
        case .leftMiddle:
            offsetX = -halfDiameter
        }
        return Circle()
            .strokeBorder(borderColor, lineWidth: 3)
            .background(Circle().foregroundColor(fillColor))
            .frame(width: diameter, height: diameter)
            .offset(x: offsetX, y: offsetY)
            .gesture(dragGesture(point: resizePoint))
    }
    
    private func dragGesture(point: ResizePoint) -> some Gesture {
        DragGesture()
            .onChanged { drag in
                switch point {
                case .topLeft:
                    dragged(point, drag.translation.width, drag.translation.height)
                case .topMiddle:
                    dragged(point, 0, drag.translation.height)
                case .topRight:
                    dragged(point, drag.translation.width, drag.translation.height)
                case .rightMiddle:
                    dragged(point, drag.translation.width, 0)
                case .bottomRight:
                    dragged(point, drag.translation.width, drag.translation.height)
                case .bottomMiddle:
                    dragged(point, 0, drag.translation.height)
                case .bottomLeft:
                    dragged(point, drag.translation.width, drag.translation.height)
                case .leftMiddle:
                    dragged(point, drag.translation.width, 0)
                }
            }
            .onEnded { _ in dragEnded() }
    }
}

问题根源

  1. 频繁触发视图重绘:缩放时直接修改CardComponentInfo的属性,而该对象被@Published监听,每一次拖拽变化都会触发整个视图树的重绘,CPU负载过高。
  2. 临时状态与持久化模型混淆:缩放过程中的临时偏移直接写入持久化模型,没有做批量更新,导致多次不必要的状态发布。
  3. 手势冲突与冗余计算:主视图拖拽手势和缩放控件手势存在潜在冲突,且位置、尺寸计算依赖多个@Published属性,每次属性变化都会重复计算。

优化方案

1. 分离临时缩放状态

在TestModel中新增临时状态存储缩放过程中的偏移,直到拖拽结束再更新持久化模型,减少重绘次数:

class TestModel: ObservableObject {
    // ... 原有属性保留
    @Published var tempResizeInfo: (info: CardComponentInfo, deltaX: CGFloat, deltaY: CGFloat, point: ResizePoint)?

    func widthForCardComponent(info: CardComponentInfo) -> CGFloat {
        guard let tempInfo = tempResizeInfo, tempInfo.info.id == info.id else {
            return info.size.width
        }
        switch tempInfo.point {
        case .topLeft, .bottomLeft, .leftMiddle:
            return info.size.width - tempInfo.deltaX
        case .topRight, .bottomRight, .rightMiddle:
            return info.size.width + tempInfo.deltaX
        default:
            return info.size.width
        }
    }

    func heightForCardComponent(info: CardComponentInfo) -> CGFloat {
        guard let tempInfo = tempResizeInfo, tempInfo.info.id == info.id else {
            return info.size.height
        }
        switch tempInfo.point {
        case .topLeft, .topMiddle, .topRight:
            return info.size.height - tempInfo.deltaY
        case .bottomLeft, .bottomMiddle, .bottomRight:
            return info.size.height + tempInfo.deltaY
        default:
            return info.size.height
        }
    }

    func xPositionForCardComponent(info: CardComponentInfo) -> CGFloat {
        let baseX = info.origin.x + info.size.width / 2
        let dragOffsetX = draggedComponentInfo?.id == info.id ? (dragOffset?.width ?? 0) : 0
        
        guard let tempInfo = tempResizeInfo, tempInfo.info.id == info.id else {
            return baseX + dragOffsetX
        }
        
        switch tempInfo.point {
        case .topLeft, .bottomLeft, .leftMiddle:
            return baseX + tempInfo.deltaX / 2 + dragOffsetX
        case .topRight, .bottomRight, .rightMiddle:
            return baseX + tempInfo.deltaX / 2 + dragOffsetX
        default:
            return baseX + dragOffsetX
        }
    }

    func yPositionForCardComponent(info: CardComponentInfo) -> CGFloat {
        let baseY = info.origin.y + info.size.height / 2
        let dragOffsetY = draggedComponentInfo?.id == info.id ? (dragOffset?.height ?? 0) : 0
        
        guard let tempInfo = tempResizeInfo, tempInfo.info.id == info.id else {
            return baseY + dragOffsetY
        }
        
        switch tempInfo.point {
        case .topLeft, .topMiddle, .topRight:
            return baseY + tempInfo.deltaY / 2 + dragOffsetY
        case .bottomLeft, .bottomMiddle, .bottomRight:
            return baseY + tempInfo.deltaY / 2 + dragOffsetY
        default:
            return baseY + dragOffsetY
        }
    }

    func updateForResize(using resizePoint: ResizePoint, deltaX: CGFloat, deltaY: CGFloat) {
        tempResizeInfo = (info: resizedComponentInfo!, deltaX: deltaX, deltaY: deltaY, point: resizePoint)
    }

    func resizeEnded() {
        guard let tempInfo = tempResizeInfo else { return }
        var info = tempInfo.info
        var width = info.size.width
        var height = info.size.height
        var x = info.origin.x
        var y = info.origin.y
        
        switch tempInfo.point {
        case .topLeft:
            width -= tempInfo.deltaX
            height -= tempInfo.deltaY
            x += tempInfo.deltaX
            y += tempInfo.deltaY
        case .topMiddle:
            height -= tempInfo.deltaY
            y += tempInfo.deltaY
        case .topRight:
            width += tempInfo.deltaX
            height -= tempInfo.deltaY
            y += tempInfo.deltaY
        case .rightMiddle:
            width += tempInfo.deltaX
        case .bottomRight:
            width += tempInfo.deltaX
            height += tempInfo.deltaY
        case .bottomMiddle:
            height += tempInfo.deltaY
        case .bottomLeft:
            width -= tempInfo.deltaX
            height += tempInfo.deltaY
            x += tempInfo.deltaX
        case .leftMiddle:
            width -= tempInfo.deltaX
            x += tempInfo.deltaX
        }
        
        info.size = CGSize(width: width, height: height)
        info.origin = CGPoint(x: x, y: y)
        
        if let index = componentInfos.firstIndex(where: { $0.id == info.id }) {
            componentInfos[index] = info
        }
        
        tempResizeInfo = nil
        resizedComponentInfo = nil
    }
}

2. 优化手势响应逻辑

给主视图拖拽手势设置优先级,确保缩放手势优先响应:

struct TestComponentView: View {
    // ... 原有代码
    .gesture(
        DragGesture()
            .onChanged { gesture in
                model.draggedComponentInfo = info
                model.updateForDrag(deltaX: gesture.translation.width, deltaY: gesture.translation.height)
            }
            .on
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 10:10:13