---
title: "Creating a Dynamic Draggable ScrollView with UIKit for SwiftUI"
source: "/writing/creating-a-dynamic-draggable-scrollview-with-uikit-for-swiftui/"
---

Article

# Creating a Dynamic Draggable ScrollView with UIKit for SwiftUI

Alex Constancio

September 23, 2024

In one of our recent products, we undertook an exciting challenge to redesign a map and search view. The design we implemented features a hybrid state, where a map sits behind a collapsible bottom sheet list view, very similar to popular applications like Apple Maps and Google Maps! This approach presented several challenges, especially since we needed to build this feature within a SwiftUI view while supporting iOS 15. One of the key difficulties was the lack of control that is now available in iOS 16+ with `[presentationDetents](https://developer.apple.com/documentation/swiftui/view/presentationdetents\(_:\))` . I plan to cover `[presentationDetents](https://developer.apple.com/documentation/swiftui/view/presentationdetents\(_:\))` more in depth in an upcoming article!

Example of the snap points and scroll behavior.

## The Challenge

Our primary objective was to create a scrollable bottom sheet within SwiftUI to maintain consistency with other SwiftUI components. Initially, we attempted to handle all the logic within SwiftUI. However, we encountered significant obstacles, particularly around customizing gesture recognizers for a scrollable view.

This approach quickly became impractical due to the complexity and hackiness required to manage drag gestures and scrollable views effectively. You'd think Apple would have included something as basic as knowing when a drag gesture starts to change, especially in a framework that's supposed to simplify UI development. But unfortunately, this isn't directly provided in SwiftUI's `ScrollView` and `ScrollViewReader` (as of iOS 15). So, we had to put our heads down and turn to classic UIKit to handle the drag gestures, ultimately leading to a more reusable and customized solution--though not without some challenges.

## The Solution

What follows below is based on a simplified re-implementation in a [sample project](https://github.com/aconstancio-livefront/DraggableSwiftUIBottomSheet) . The final implementation is a blend of UIKit and SwiftUI, resulting in a bottom sheet with multiple snap points that supports a scrollable list. The key component of this implementation is our custom `DraggableScrollView` view object, which wraps SwiftUI content in a `UIScrollView` and provides easy panning functionality through an `offset` parameter.

Here's how the `DraggableScrollView` is used in the SwiftUI view struct:

DraggableScrollView(offset: offset) {
    ForEach(0..<20) { item in
       Text("List item \($0)")
    }
}
.onDragChanged { value in
    onDragChanged(by: value.translation.y)
}
.onDragEnded { value in
    // Handle drag
}

This implementation offers the `onDragChanged` and `onDragEnded` view modifiers, which are essential for customizing behavior based on user interactions. These modifiers are key to controlling the list, giving us the vital information needed to make this work, which I'll discuss more in depth later on. This highlights one of the limitations of SwiftUI's `ScrollView` and `ScrollViewReader` in iOS 15 and earlier.

## Implementation Details

### Creating the DraggableScrollView

This view object integrates a SwiftUI content view with a `UIScrollView`, allowing for the required drag functionality.

**UIViewRepresentable**: The `DraggableScrollView` conforms to `UIViewRepresentable`, which is a protocol that lets us wrap UIKit view content in a SwiftUI `View` object.

struct DraggableScrollView<Content: View>: UIViewRepresentable {
  var offset: Binding<CGFloat>
  var content: Content

  func makeUIView(context: Context) -> UIScrollView {
    let scrollView = UIScrollView()
    let hostingController = UIHostingController(rootView: content)
    scrollView.addSubview(hostingController.view)

    // Further setup...
    return scrollView
  }

  func updateUIView(_ uiView: UIScrollView, context: Context) {
    // Update the view...
  }

  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
}

**Coordinator**: A coordinator class is created to manage the gesture recognizers and coordinate the scrolling.

class Coordinator: NSObject, UIGestureRecognizerDelegate {
  var parent: DraggableScrollView

  init(_ parent: DraggableScrollView) {
    self.parent = parent
  }

  @objc func handlePan(_ gesture: UIPanGestureRecognizer) {
    // Handle the pan gesture...
  }
}

**Gesture Recognizers**: The `UIScrollView` is enhanced with a `UIPanGestureRecognizer`, whose delegate is the coordinator. This setup enables us to handle drag gestures and determine when to begin or end them.

func makeUIView(context: Context) -> UIScrollView {
  let scrollView = UIScrollView()
  let hostingController = UIHostingController(rootView: content)
  scrollView.addSubview(hostingController.view)
  let panGesture = UIPanGestureRecognizer(
    target: context.coordinator, action: #selector(context.coordinator.handlePan(_:))
  )

  panGesture.delegate = context.coordinator
  scrollView.addGestureRecognizer(panGesture)
  return scrollView
}

## Custom Behavior with Gesture Recognizers

The `handlePan` method within the coordinator processes the pan gestures, updating the view state accordingly. By implementing `gestureRecognizerShouldBegin`, we can determine whether the gesture should proceed based on the current scroll position and the velocity of the pan gesture.

/// Determine whether to recognize if the gesture should drag the sheet, or to
/// scroll the list.
func gestureRecognizerShouldBegin(
_ gestureRecognizer: UIGestureRecognizer
) -> Bool {
  guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
    return true
  }
  let velocity = panGesture.velocity(in: panGesture.view)
  let isPanningDown = velocity.y > 0
  let isScrolledToTop =
  scrollView.collectionView.contentOffset.y <= 0

  let isAtTop = (dragOffset == 0)
  if !isScrolledToTop { return false }
  if isPanningDown && isScrolledToTop { return true }
  if !isPanningDown && !isAtTop { return true }

  return false
}

## Creating Snap Points

Moving over to our SwiftUI view struct. The `onDragChanged`and `onDragEnded` methods are particularly crucial for applying transformations to the bottom sheet based on the drag's end state. Here are the transformations I came up with for achieving optimal snap points:

private func onDragChanged(
    by translation: CGFloat
) {
  let sheetPosition = translation + currentPosition
  self.finishedTranslation = sheetPosition
}

private func onDragEnded(
  screenHeight: Double,
  translation: CGFloat,
  velocity: CGFloat
) {
  /// The current position of the bottom sheet
  let sheetPosition = translation + currentPosition
  /// 1/4 of the screens height
  let quarterScreen = screenHeight / 4
  /// 3/4 of the screens height
  let threeFourthsScreen = quarterScreen * 3

  /// Setting for the half screen position
  let halfPosition = screenHeight / 2
  /// Setting for the full screen position
  let higherPosition: CGFloat = 0
  /// Setting for the closed position
  let closedPosition = screenHeight

  withAnimation(.interactiveSpring()) {
      // Adjust sheet position based on location sheet at end of gesture.
      if sheetPosition < threeFourthsScreen {
          // List is full screen
          currentPosition = higherPosition
          finishedTranslation = higherPosition
      } else if sheetPosition > quarterScreen {
          // List is half screen
          currentPosition = halfPosition
          finishedTranslation = halfPosition
      } else {
          // List is closed
          currentPosition = closedPosition
          finishedTranslation = closedPosition
      }
   }
}

Using this approach, we set different snap zones. This method leverages some key values, translation, and velocity, values not provided by default SwiftUI modifiers. This is important data needed to enable the bottom sheet to expand or collapse smoothly based on user interactions!

## Conclusion

This hybrid approach, combining UIKit's gesture handling with SwiftUI's declarative UI framework, provided a great solution to our design challenge. The `DraggableScrollView` enables a smooth, interactive experience that behaves as expected within a bottom sheet!

_Alex engineers with creativity at_ [_Livefront_](https://livefront.com/) _._

-   Alex Constancio

    Software Engineer

    [More from Alex](/team/alex-constancio/)

    Alex Constancio

### Tags

[Digital Engineering](/insights/all/?topic=digital-engineering) [Mobile Apps](/insights/all/?topic=mobile-apps)

### Share

-   Copied

## View these next.

[See all](/insights/all/?type=writing)

-   [

    Article

    How To Sabotage Your Project Using Inconsistency

    Collin Flynn

    ](/writing/how-to-sabotage-your-project-using-inconsistency/)
-   [

    Article

    Unit Testing race conditions by creating chaos (Swift)

    Sean Berry

    ](/writing/unit-testing-race-conditions-by-creating-chaos-swift/)
-   [

    Article

    UIApplicationDelegate call sequence reference

    Sean Berry

    ](/writing/uiapplicationdelegate-call-sequence-reference/)
