如何提升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() } } }
问题根源
- 频繁触发视图重绘:缩放时直接修改
CardComponentInfo的属性,而该对象被@Published监听,每一次拖拽变化都会触发整个视图树的重绘,CPU负载过高。 - 临时状态与持久化模型混淆:缩放过程中的临时偏移直接写入持久化模型,没有做批量更新,导致多次不必要的状态发布。
- 手势冲突与冗余计算:主视图拖拽手势和缩放控件手势存在潜在冲突,且位置、尺寸计算依赖多个
@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
相关产品推荐
相关产品推荐

