How to diagnose infinite layout loop

Due to limitations of SwiftUI VSplitView, and to preserve the appearance of the old C++/AppKit app I'm porting to SwiftUI, I rolled my own pane divider. It worked well on macOS 11, but after updating to macOS 12, it now triggers an infinite loop somewhere. Running the code below in an Xcode playground works for a short while, but if you wiggle the mouse up and down, after a few seconds it will get caught in an infinite loop:

import SwiftUI
import PlaygroundSupport
import AppKit
import CoreGraphics

PlaygroundPage.current.setLiveView(ContentView())

struct ContentView: View {
    @State var position: Double = 50

    var body: some View {
        VStack(spacing: 0) {
            Color.red
            .frame(maxHeight: position)
            PaneDivider(position: $position)
            Color.blue
        }
        .frame(width: 500, height:500)
    }
}

struct PaneDivider: View {
    @Binding var position: Double
    @GestureState private var isDragging = false // Will reset to false when dragging has ended

    var body: some View {
        Group {
            Divider()
            ZStack {
                Rectangle().fill(Color(white: 0.99))
                Circle().frame(width: 6, height: 6).foregroundColor(Color(white: 0.89))
            }.frame(height: 8)
            Divider()
        }
        .gesture(DragGesture()
            .onChanged {
                position += $0.translation.height
                if position < 0 { position = 0 }
            }
            .updating($isDragging) { (value, state, transaction) in state = true })
    }
}

Any thoughts on how to diagnose or fix that?

  • Paired it down a little smaller: `import Foundation import SwiftUI import PlaygroundSupport

    PlaygroundPage.current.setLiveView(ContentView())

    struct ContentView: View { @State var position: Double = 50

    var body: some View { VStack(spacing: 0) { Color.red.frame(width: 500, height: position) PaneDivider(position: $position) Color.blue }.frame(width: 500, height:500) }

    }

    struct PaneDivider: View { @Binding var position: Double

    var body: some View { Rectangle().fill(Color.yellow).frame(height: 8) .gesture(DragGesture() .onChanged { position += $0.translation.height if position < 0 { position = 0 } }) }

    }`

  • FB9812157

Add a Comment

Replies

Using .location in the stack coordinate space instead of .translation avoids the problem, whatever it was. The following works as desired with no infinite loop.

import Foundation
import SwiftUI
import PlaygroundSupport

PlaygroundPage.current.setLiveView(ContentView())

struct ContentView: View {
    @State var position: Double = 50

    var body: some View {
        VStack(spacing: 0) {
            Color.red.frame(width: 500, height: position)
            PaneDivider(position: $position)
            Color.blue
        }
        .frame(width: 500, height:500)
        .coordinateSpace(name: "stack")
    }
}

struct PaneDivider: View {
    @Binding var position: Double

    var body: some View {
        Color.yellow.frame(height: 8)
        .gesture(
            DragGesture(minimumDistance: 1, coordinateSpace: .named("stack"))
            .onChanged { position = max(0, $0.location.y) }
            )
    }
}