Reusing Map etc on two MapViews

434
4
02-13-2024 06:00 AM
sveinhal
New Contributor II

I have a view that uses tab tab for iPhone layout (aka. "compact" size class) and a full-screen map with a floating overlay in lieu of the tab bar on iPad (aka. "regular" size class).

The map view's state (map, overlays, viewpoint, etc) are handled by an ObservableObject.

However, when the user puts the app in "slide over" or "split view" modes on iPad, the size class changes from "regular" to "compact", causing SwiftUI to re-render the hierarchy, destroy the old map view, and create a brand new MapView. This new mapview is then handed the same coordinator object.

This causes all kinds of trouble, since it seems most of the stuff are classes/reference types and it seems MapView does't support reusing those for more than a single MapView, methinks.

  • If the mapview has overlays, I get a crash with "Object is already in use and may not be reused".
  • If the mapview doesn't have overlays, the mapview won't crash, but also won't display the map after the size class change

I do want the app to preserve viewpoint and other state when changing size class.

Example: 

struct ContentView: View {
    @Environment(\.horizontalSizeClass) private var sizeClass
    @StateObject private var coordinator = MapCoordinator()

    var body: some View {
        if sizeClass == .regular {
            MyMapView(coordinator: coordinator)
                .overlay(alignment: .topLeading) { Menu() }
        } else {
            TabView {
                Menu()
                    .tabItem { Text("Menu") }
                MyMapView(coordinator)
                    .tabItem { Text("Map") }
            }
        }
    }
}

struct MyMapView: View {
    @ObservedObject var coordinator: MapCoordinator
    
    var body: some View {
        MapView(
            map: coordinator.map, 
            viewpoint: coordinator.viewPoint, 
            graphicsOverlays: coordinator.graphicsOverlays
        )
        .onScaleChanged(perform: coordinator.scaleChanged)
        .onRotationChanged(perform: coordinator.rotationChanged)
    }
}

 

Are my assumptions correct? Can I rework my app to fix these issues?

0 Kudos
4 Replies
rolson_esri
New Contributor III

This is a class of issue that we've recently come up against. Thank you for explaining your specific scenario. We will dig into this a bit and get back to you.

rolson_esri
New Contributor III

Unfortunately at this time, I don't see a great solution to your problem that doesn't involve a hack with introducing a delay in showing the second MapView when the size class changes. That hack might be enough of a workaround for you to continue development. If you really needed it, I can clean up a prototype and get that to you. Please let me know.

However, the good news is that we will have a fix for your scenario in our April release (200.4).

sveinhal
New Contributor II

Thanks for you suggestion, and super thanks for fixing this in the upcoming release!

I'll see if I can get it working with a little delayed view, or by perhaps cloning the data in my model on view life cycle events. 

CalebRasmussen
Esri Contributor

As @rolson_esri said, we will have a fix for the bug in a couple of months, but in the meantime, you can probably refactor your code a little bit to get by. In SwiftUI, it is generally a good rule of thumb to preserve a view’s structural identity where possible. This means putting conditionals in view modifiers instead of using if/else statements. In your case, you can do this by hiding the tab bar using toolbar(_:for:)  and showing the Menu in the overlay when sizeCase == .regular. That way, you just get the effect you were looking for while only using one MapView. Let me know if this works for you!

struct ContentView: View {
    @Environment(\.horizontalSizeClass) private var sizeClass
    @StateObject private var coordinator = MapCoordinator()
    
    var body: some View {
        TabView {
            Menu()
                .tabItem { Text("Menu") }
            MyMapView(coordinator: coordinator)
                .tabItem { Text("Map") }
                .toolbar(sizeClass == .regular ? .hidden : .automatic, for: .tabBar)
                .overlay(alignment: .topLeading) {
                    if sizeClass == .regular {
                        Menu()
                    }
                }
        }
    }
}