Show and hide SwiftUI inspectors with an identifiable item
Helm Pro yearly subscribers now get a 30% discount on RocketSim thanks to contingent pricing on the App Store.
SwiftUI has a powerful API that allows you to insert a side panel view that shows alongside the view it’s attached to called inspector.
The API is available through the inspector view modifier that takes a binding to a boolean that controls the visibility of the inspector and a closure that returns the view that should be shown in the inspector.
This is a pattern that is widely used in the Apple ecosystem, feels natural to the user, and it’s a great way to show additional information about a view or a component.
In fact, I have recently used an inspector for a feature I have been building for Helm that allows users to select a specific TestFlight build from a list and see its details in a side panel:
Limitations of the inspector modifier
As you can see in the example above, the visibility of the inspector is controlled by whether an item is selected or not. Initially, I started looking for an overload of the inspector
modifier that would allow me to pass a binding with an optional identifiable item instead of a boolean, similar to the way the sheet
modifier works:
nonisolated
func sheet<Item, Content>(
item: Binding<Item?>,
onDismiss: (() -> Void)? = nil,
@ViewBuilder content: @escaping (Item) -> Content
) -> some View where Item : Identifiable, Content : View
Unfortunately, the inspector
modifier has its limitations and, at least for now, it only accepts a boolean binding:
nonisolated
func inspector<V>(
isPresented: Binding<Bool>,
@ViewBuilder content: () -> V
) -> some View where V : View
A custom view modifier
To improve the developer experience and make the API more flexible, I decided to create a custom view modifier that would allow me to pass an optional identifiable item instead of a boolean:
import SwiftUI
struct InspectorViewModifier<Item: Equatable, InspectorView: View>: ViewModifier {
@Binding var item: Item?
@ViewBuilder var inspectorContent: (Item) -> InspectorView
func body(content: Content) -> some View {
content
.inspector(isPresented: _item.map(to: { $0 != nil }, from: { _ in item })) {
item.map(inspectorContent)
}
}
}
extension View {
func inspector<Item: Equatable, InspectorContent: View>(item: Binding<Item?>, @ViewBuilder content: @escaping (Item) -> InspectorContent) -> some View {
self.modifier(InspectorViewModifier(item: item, inspectorContent: content))
}
}
extension Binding {
func map<T>(to: @escaping (Value) -> T, from: @escaping (T) -> Value) -> Binding<T> {
Binding<T>(
get: { to(self.wrappedValue) },
set: { (value: T) in self.wrappedValue = from(value) }
)
}
}
The view modifier takes in two parameters: a binding to an optional identifiable and equatable item that will control the visibility of the inspector and a closure that returns the inspector view to render.
The modifier then maps the item binding to a boolean binding that is passed to the inspector modifier.
While this solution works well for my use case, I would love to see Apple provide first-party support for this pattern in the future, so I have filed a feedback request (FB14177256).