Show and hide SwiftUI inspectors with an identifiable item
Runway handles the release coordination and busywork so you can focus on building great apps. You do the building, we'll do the shipping.
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).