如何使使用拖动手势调整 SwiftUI 视图的代码更加高效?

3

enter image description here

我正在尝试使用拖拽手势来允许用户改变视图的位置和大小。将视图拖动以移动它们可以正常工作,但是在调整大小时,性能非常差。我无法弄清楚问题所在。我尝试添加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

}

结构体 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() }
}
}

请将您的项目打包成一个最小化的Xcode项目,并在某个地方提供可用。 - Alnitak
@MatiasContreras 是的,我已经尝试使用drawingGroup,并没有看到性能上的改善。 - KevinF
你能提供一个完整的工作示例,以便我可以在我的端上构建和运行吗? - Matias Contreras
@KevinF 它是空的。 - Matias Contreras
@MatiasContreras 噢,对不起。代码现在应该已经在那里了。 - KevinF
显示剩余5条评论
1个回答

1

默认情况下,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偏移量到来时从中减去。

谢谢,@Shawn。这些更改对您的性能有所改善吗?当我切换到使用全局coordinateSpace并且不在updateForResize函数中进行更改时,我没有看到任何性能变化,矩形始终是我期望的形状。使用默认的coordinateSpace和全局coordinateSpace,调整大小会得到我期望的形状。当我按照您建议的更改坐标空间和updateForResizeFunction时,应用程序会占用CPU并冻结。 - KevinF
是的,我在XCode 14.2(14C18)中看到了性能改进,使用了帖子评论中提供的Github项目。如果坐标空间发生了变化,updateForResize要正常工作就不应该没有更改。模拟器视频:https://imgur.com/jaXc9sy - Shawn
这就是我使用的Xcode版本。也许我的电脑只是慢一些?我的原始帖子展示了一个gif,它使用默认坐标空间调整大小到正确的形状和位置。 - KevinF
我第一次阅读时误解了你的回答。你的解决方案确实解决了性能问题。谢谢! - KevinF
@Shawn 或 @KevinF,我什么时候设置或更新 previousResizeOffset - undefined
在"updateForResize"的末尾,我有"previousResizeOffset = .init(width: deltaX, height: deltaY)"。我的"resizeEnded"也只是将"previousResizeOffset"、"resizePoint"和"resizedComponentInfo"设置为nil。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接