如何使用拖动手势调整 SwiftUI 视图的大小以提高性能?

问题描述 投票:0回答:1

[![在此处输入图像描述][1]][1]

我尝试使用拖动手势来允许用户更改视图的位置并调整视图的大小。拖动来移动视图效果很好,但拖动来调整大小时的性能很差。 我不知道问题是什么。我尝试添加

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]: https://i.sstatic.net/YRy7y.gif
performance swiftui gesture drag
1个回答
3
投票

默认情况下,

DragGesture
将读取最大
local
坐标空间内的手势。对于您的
ResizingControlsView
,这将是绿色矩形内的区域。

不幸的是,在这种情况下,每次调整矩形大小时,都会调整坐标空间。

DragGesture
将注册额外的移动,因为空间已经移动,这将触发进一步移动空间的函数,导致注册更多的移动,等等 - 导致循环条件。

通过在

ResizingControlsView.swift
中替换以下代码可以很容易地解决这个问题:

...
private func dragGesture(point: ResizePoint) -> some Gesture {
    DragGesture(coordinateSpace: .global)
        .onChanged { drag in
...

此解决方案通过监视

global
坐标空间来工作,该空间不受绿色矩形位置变化的影响。但是,这意味着它返回的翻译宽度和高度将不再像以前一样考虑偏移量的变化。

为了解决这个问题,您需要在每次调用时在

updateForResize
内手动从新偏移量中减去先前的偏移量(deltaX 和 deltaY)。

var previousResizeOffset: CGSize? = 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
    
    // Adjust the values of deltaY and deltaX to mimic a local coordinate space.
    let adjDeltaY = deltaY - (previousResizeOffset?.height ?? 0)
    let adjDeltaX = deltaX - (previousResizeOffset?.width ?? 0)
    
    switch resizePoint {
    case .topLeft:
        width -= adjDeltaX
        height -= adjDeltaY
        x += adjDeltaX
        y += adjDeltaY
...

在上面的示例中,我已将之前的 deltaX 和 deltaY 偏移量保存在变量

previousResizeOffset
中,并在新的 deltaX 和 deltaY 偏移量进来时将其减去。

© www.soinside.com 2019 - 2024. All rights reserved.