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.
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?
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.
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).
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.
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()
}
}
}
}
}