Swiping to close the sidebar (#678)

* enabled drag gesture to close the sidebar view

* removed extraneous onChanged block

* Fix sidebar swipe gestures to work with ScrollView

Improvements:
- Use simultaneousGesture() instead of gesture() to work alongside ScrollView
- Add horizontal-only detection (width > height * 1.5) to prevent interfering with vertical scrolling
- Add visual feedback during drag with sidebarDragOffset updates
- Add 20pt right edge zone for easier swipe-to-open activation (iOS-native behavior)
- Lower threshold for edge swipe (-50pt instead of -100pt)

Both swipe-to-open and swipe-to-close now work reliably:
- Swipe left from anywhere (or from right edge) to open sidebar
- Swipe right on sidebar to close it
- Vertical scrolling unaffected

* Fix sidebar drag offset direction

Changed offset calculation from:
  showSidebar ? -dragOffset : width - dragOffset
To:
  showSidebar ? dragOffset : width + dragOffset

This fixes the issue where dragging right to close the sidebar would
make it fly to the left side of the screen before closing.

Now the sidebar correctly follows the finger during drag:
- When closing: moves right (toward off-screen)
- When opening: moves left (toward on-screen)

* Optimize sidebar drag performance for smooth 60fps

Performance improvements:
- Remove .animation() modifier that was conflicting with drag updates
- Use Transaction with disablesAnimations during drag for instant updates
- Throttle state updates to only fire when offset changes >2pt
- Always render sidebar (avoid conditional view creation overhead)
- Only animate on gesture end, not during drag

This eliminates the lag/jank during swipe by ensuring:
1. No animation conflicts during manual drag
2. Direct offset updates follow finger immediately
3. Stable view hierarchy (no conditional rendering)
4. Reduced state update frequency

The sidebar now feels buttery smooth at 60fps.

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
rick conroy
2025-10-06 23:24:24 +02:00
committed by GitHub
co-authored by jack
parent b5b05977fb
commit 13b58aeaa9
+130 -33
View File
@@ -129,6 +129,45 @@ struct ContentView: View {
)
}
// Right edge swipe zone for easier sidebar opening (iOS-native behavior)
if !showSidebar {
HStack {
Spacer()
Color.clear
.frame(width: 20)
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 5)
.onChanged { value in
let translationWidth = value.translation.width.isNaN ? 0 : value.translation.width
if translationWidth < 0 {
let newOffset = max(translationWidth, -300)
if abs(newOffset - sidebarDragOffset) > 2 {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
sidebarDragOffset = newOffset
}
}
}
}
.onEnded { value in
let translationWidth = value.translation.width.isNaN ? 0 : value.translation.width
let velocity = value.velocity.width.isNaN ? 0 : value.velocity.width
withAnimation(.easeOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
if translationWidth < -50 || velocity < -300 {
showSidebar = true
sidebarDragOffset = 0
} else {
sidebarDragOffset = 0
}
}
}
)
}
.allowsHitTesting(true)
}
// Sidebar overlay
HStack(spacing: 0) {
// Tap to dismiss area
@@ -141,31 +180,19 @@ struct ContentView: View {
}
}
// Only render sidebar content when it's visible or animating
if showSidebar || sidebarDragOffset != 0 {
sidebarView
#if os(macOS)
.frame(width: min(300, max(0, geometry.size.width.isNaN ? 300 : geometry.size.width) * 0.4))
#else
.frame(width: max(0, geometry.size.width.isNaN ? 300 : geometry.size.width) * 0.7)
#endif
.transition(.move(edge: .trailing))
} else {
// Empty placeholder when hidden
Color.clear
#if os(macOS)
.frame(width: min(300, max(0, geometry.size.width.isNaN ? 300 : geometry.size.width) * 0.4))
#else
.frame(width: max(0, geometry.size.width.isNaN ? 300 : geometry.size.width) * 0.7)
#endif
}
// Always render sidebar to avoid layout recalculation during drag
sidebarView
#if os(macOS)
.frame(width: min(300, max(0, geometry.size.width.isNaN ? 300 : geometry.size.width) * 0.4))
#else
.frame(width: max(0, geometry.size.width.isNaN ? 300 : geometry.size.width) * 0.7)
#endif
}
.offset(x: {
let dragOffset = sidebarDragOffset.isNaN ? 0 : sidebarDragOffset
let width = geometry.size.width.isNaN ? 0 : max(0, geometry.size.width)
return showSidebar ? -dragOffset : width - dragOffset
return showSidebar ? dragOffset : width + dragOffset
}())
.animation(.easeInOut(duration: TransportConfig.uiAnimationSidebarSeconds), value: showSidebar)
}
}
#if os(macOS)
@@ -921,9 +948,52 @@ struct ContentView: View {
.id(viewModel.allPeers.map { "\($0.peerID)-\($0.isConnected)" }.joined())
}
Spacer()
}
.background(backgroundColor)
Spacer()
}
.background(backgroundColor)
.simultaneousGesture(
DragGesture(minimumDistance: 10)
.onChanged { value in
let translationWidth = value.translation.width.isNaN ? 0 : value.translation.width
let translationHeight = value.translation.height.isNaN ? 0 : value.translation.height
// Only handle horizontal drags on the sidebar
guard abs(translationWidth) > abs(translationHeight) * 1.5 else { return }
if translationWidth > 0 {
let newOffset = min(translationWidth, 300)
if abs(newOffset - sidebarDragOffset) > 2 {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
sidebarDragOffset = newOffset
}
}
}
}
.onEnded { value in
let translationWidth = value.translation.width.isNaN ? 0 : value.translation.width
let translationHeight = value.translation.height.isNaN ? 0 : value.translation.height
let velocity = value.velocity.width.isNaN ? 0 : value.velocity.width
// Only handle if drag is predominantly horizontal
guard abs(translationWidth) > abs(translationHeight) * 1.5 else {
sidebarDragOffset = 0
return
}
withAnimation(.easeOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
if translationWidth > 100 || (translationWidth > 50 && velocity > 500) {
showSidebar = false
sidebarDragOffset = 0
} else {
sidebarDragOffset = 0
}
}
}
)
}
}
@@ -939,29 +1009,56 @@ struct ContentView: View {
}
.background(backgroundColor)
.foregroundColor(textColor)
.gesture(
DragGesture()
.simultaneousGesture(
DragGesture(minimumDistance: 10)
.onChanged { value in
let translation = value.translation.width.isNaN ? 0 : value.translation.width
if !showSidebar && translation < 0 {
sidebarDragOffset = max(translation, -300)
} else if showSidebar && translation > 0 {
sidebarDragOffset = min(-300 + translation, 0)
let translationWidth = value.translation.width.isNaN ? 0 : value.translation.width
let translationHeight = value.translation.height.isNaN ? 0 : value.translation.height
// Only handle if drag is predominantly horizontal (prevents interfering with scroll)
guard abs(translationWidth) > abs(translationHeight) * 1.5 else { return }
if !showSidebar && translationWidth < 0 {
let newOffset = max(translationWidth, -300)
if abs(newOffset - sidebarDragOffset) > 2 {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
sidebarDragOffset = newOffset
}
}
} else if showSidebar && translationWidth > 0 {
let newOffset = min(-300 + translationWidth, 0)
if abs(newOffset - sidebarDragOffset) > 2 {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
sidebarDragOffset = newOffset
}
}
}
}
.onEnded { value in
let translation = value.translation.width.isNaN ? 0 : value.translation.width
let translationWidth = value.translation.width.isNaN ? 0 : value.translation.width
let translationHeight = value.translation.height.isNaN ? 0 : value.translation.height
let velocity = value.velocity.width.isNaN ? 0 : value.velocity.width
// Only handle if drag is predominantly horizontal
guard abs(translationWidth) > abs(translationHeight) * 1.5 else {
sidebarDragOffset = 0
return
}
withAnimation(.easeOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
if !showSidebar {
if translation < -100 || (translation < -50 && velocity < -500) {
if translationWidth < -100 || (translationWidth < -50 && velocity < -500) {
showSidebar = true
sidebarDragOffset = 0
} else {
sidebarDragOffset = 0
}
} else {
if translation > 100 || (translation > 50 && velocity > 500) {
if translationWidth > 100 || (translationWidth > 50 && velocity > 500) {
showSidebar = false
sidebarDragOffset = 0
} else {