I am trying to create a resizable split view in SwiftUI where a Map is on top and a List(which will contain locations later) is on the bottom, separated by a draggable handle. The user should be able to drag the handle to smoothly resize both the map and the list. The current implementation is functional but nowhere near usable because the dragging animation is very laggy and jittery. Also at times, Xcode continuously prints the following error to the console during the drag: Publishing changes from within view updates is not allowed, this will cause undefined behavior. I believe the issue is being caused by the continuous view rendering loop with the resizing of the map & list with the map probably trying to render the appropriate region to account for the change in size. Here's what I have so far:
import SwiftUI
import MapKit
struct DraggableSplitMapView: View {
@State private var mapRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
)
@State private var mapHeight: CGFloat = 300
@GestureState private var dragOffset: CGSize = .zero
var body: some View {
NavigationView {
GeometryReader { geometry in
VStack(spacing: 0) {
Map(coordinateRegion: $mapRegion)
.frame(height: calculateMapHeight(geometry: geometry))
// Drag handle
ZStack {
Color.clear.frame(height: 12)
Capsule()
.fill(Color.secondary.opacity(0.5))
.frame(width: 40, height: 6)
}
.gesture(
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}
.onEnded { value in
let newHeight = self.mapHeight + value.translation.height
// Clamp the height from getting too big or small.
self.mapHeight = max(100, min(geometry.size.height - 150, newHeight))
}
)
// Dummy list
List(0..<50) { i in
Text("List Item \(i)")
}
.listStyle(.plain)
}
}
.navigationTitle("Draggable Map")
.navigationBarTitleDisplayMode(.inline)
}
}
private func calculateMapHeight(geometry: GeometryProxy) -> CGFloat {
let proposedHeight = self.mapHeight + self.dragOffset.height
// Ensure the height stays within reasonable bounds during the drag.
return max(100, min(geometry.size.height - 150, proposedHeight))
}
}
struct DraggableSplitMapView_Previews: PreviewProvider {
static var previews: some View {
DraggableSplitMapView()
}
}
The map will contain several hundred annotations displayed later so I think the issue will only get even worse with data bound to the map. How can I fix this issue and achieve smooth dragging while still using the native SwiftUI Map view? Is there a way to prevent the Map from writing back to its region binding during a drag gesture, or a better way to structure this view to avoid the conflict? Any help is appreciated.
This issue is similar to the one in Resizing frame with DragGesture causes jittering. The position of the drag handle is being determined by the height of the map, but the height of the map is being determined by the drag gesture on the handle. This inter-dependency causes the jittery behavior.
To fix this particular case, you could try reserving space for the map by adding top-padding to the drag handle, then show the map as an overlay in this space. This way, the position of the drag handle no longer depends on the map. Instead, the size of the map depends on the size of the padding above the drag handle.
It is important that the drag gesture is applied after the top padding, so that in fact the gesture applies to the full area of handle + padding. However, if a drag is started in the padding/overlay region then it is intercepted by the Map and causes the map position to be dragged (as you would want it to), it does not cause the map area to be re-sized. This happens automatically and does not require any extra code.
You could also consider re-using the function calculateMapHeight for setting the map height at end-of-drag, instead of repeating the logic in .onEnded. This requires passing the drag height as a parameter to the function. While we're at it, it might be simpler to pass the height of the container, instead of the GeometryProxy:
private func calculateMapHeight(containerHeight: CGFloat, dragHeight: CGFloat) -> CGFloat {
let proposedHeight = mapHeight + dragHeight
// Ensure the height stays within reasonable bounds during the drag.
return max(100, min(containerHeight - 150, proposedHeight))
}
The body function can then be updated as follows:
var body: some View {
NavigationStack { // NavigationView is deprecated
GeometryReader { geometry in
let containerHeight = geometry.size.height
VStack(spacing: 0) {
// Drag handle
Capsule()
.fill(Color.secondary.opacity(0.5))
.frame(width: 40, height: 6)
.frame(maxWidth: .infinity)
.padding(.vertical, 7)
.padding(.top, calculateMapHeight(
containerHeight: containerHeight,
dragHeight: dragOffset.height
))
.contentShape(.rect)
.gesture(
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}
.onEnded { value in
mapHeight = calculateMapHeight(
containerHeight: containerHeight,
dragHeight: value.translation.height
)
}
)
.overlay(alignment: .top) {
Map(coordinateRegion: $mapRegion) // ⚠️ deprecated
.padding(.bottom, 20) // handle height
}
// Dummy list
List(0..<50) { i in
Text("List Item \(i)")
}
.listStyle(.plain)
}
}
.navigationTitle("Draggable Map")
.navigationBarTitleDisplayMode(.inline)
}
}

You were also asking:
Is there a way to prevent the Map from writing back to its region binding during a drag gesture, or a better way to structure this view to avoid the conflict?
One option might be to show the map at maximum size, then clip it to the available space.
EDIT To try it this way, change the overlay to the following:
.overlay(alignment: .top) {
Map(coordinateRegion: $mapRegion)
.frame(height: calculateMapHeight(
containerHeight: containerHeight,
dragHeight: 9999 // Any large value
))
.frame(height: calculateMapHeight(
containerHeight: containerHeight,
dragHeight: dragOffset.height
))
.clipped()
.contentShape(.rect)
}
Doing it this way will probably mean that the binding does not get updated at all, so I guess this means, you won't know which part of the map is actually visible. Initializing a Map in this way is actually deprecated, so it would probably be better to be tracking the area a different way anyway, but I'm afraid I'm not able to suggest exactly how. You might like to post a new question that is dedicated to this issue if you are not able to find a better way to do it.