Avitzur
Avitzur

Reputation: 161

Infinite loop in SwiftUI layout

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. Curiously, running in the macOS Playgrounds App, no infinite loop occurs.

Any advice on diagnosing what is causing the infinite loop and how to avoid it?

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(maxHeight: position)
            Rectangle().fill(Color.yellow).frame(height: 8)
            .gesture(DragGesture().onChanged { position = max(0, position + $0.translation.height) })
            Color.blue
        }.frame(width: 500, height:500)
    }
}

Upvotes: 0

Views: 1158

Answers (2)

Avitzur
Avitzur

Reputation: 161

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) }
            )
    }
}

Upvotes: 1

Yrb
Yrb

Reputation: 9665

You don't have an infinite loop. The runtime error is pretty clear. You are producing a negative frame height which isn't allowed. Ironically, after implementing a "fix", I can't get your code to break again. I suggest clamping your variable so that this can't accidentally happen. The other thing this does is guarantee that your max position is the max height possible of the view you are changing. Obviously, I wouldn't hard code these numbers in use.

struct ContentView: View {
    @State var position: Double = 50
    var body: some View {
        VStack(spacing: 0) {
            Text(position.description)
            Color.red.frame(maxHeight: position)
            Rectangle().fill(Color.yellow).frame(height: 8)
                .gesture(DragGesture().onChanged { position = (position + $0.translation.height).clamped(to: 0...250) })
            Color.blue
        }.frame(width: 500, height:500)
    }
}

extension Comparable {
    func clamped(to limits: ClosedRange<Self>) -> Self {
        return min(max(self, limits.lowerBound), limits.upperBound)
    }
}

Upvotes: 1

Related Questions