Reputation: 161
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
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
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