These five types of views have many in common, so is their inspection mechanism. Due to limited capabilities of what can be achieved in reflection, the native SwiftUI modifiers for presenting these views (.alert, .actionSheet, .sheet, .fullScreenCover, .popover) cannot be inspected as-is by the ViewInspector.
This section discusses how you still can gain the full access to the internals of these views by adding a couple of code snippets to your source code while not making ViewInspector a dependency for the main target.
Note: ViewInspector fully supports confirmationDialog inspection without any code tweaking.
If you're using alert functions added in iOS 15 (those not marked as deprecated) - you're good to go.
Otherwise, you'll need to add the following snippet to your main target:
extension View {
func alert2(isPresented: Binding<Bool>, content: @escaping () -> Alert) -> some View {
return self.modifier(InspectableAlert(isPresented: isPresented, popupBuilder: content))
}
}
struct InspectableAlert: ViewModifier {
let isPresented: Binding<Bool>
let popupBuilder: () -> Alert
let onDismiss: (() -> Void)? = nil
func body(content: Self.Content) -> some View {
content.alert(isPresented: isPresented, content: popupBuilder)
}
}And tweak the code of your view to use alert2 instead of alert. Feel free to use another name instead of alert2.
Then, add this line in your test target scope:
extension InspectableAlert: PopupPresenter { }After that you'll be able to inspect the Alert in the tests: read the title, message, and access the buttons:
func testAlertExample() throws {
let binding = Binding(wrappedValue: true)
let sut = EmptyView().alert2(isPresented: binding) {
Alert(title: Text("Title"), message: Text("Message"),
primaryButton: .destructive(Text("Delete")),
secondaryButton: .cancel(Text("Cancel")))
}
let alert = try sut.inspect().emptyView().alert()
XCTAssertEqual(try alert.title().string(), "Title")
XCTAssertEqual(try alert.message().string(), "Message")
XCTAssertEqual(try alert.primaryButton().style(), .destructive)
try sut.inspect().find(ViewType.AlertButton.self, containing: "Cancel").tap()
}SwiftUI has a second variant of the Alert presentation API, which takes a generic Item parameter.
Here is the corresponding snippet for the main target:
extension View {
func alert2<Item>(item: Binding<Item?>, content: @escaping (Item) -> Alert) -> some View where Item: Identifiable {
return self.modifier(InspectableAlertWithItem(item: item, popupBuilder: content))
}
}
struct InspectableAlertWithItem<Item: Identifiable>: ViewModifier {
let item: Binding<Item?>
let popupBuilder: (Item) -> Alert
let onDismiss: (() -> Void)? = nil
func body(content: Self.Content) -> some View {
content.alert(item: item, content: popupBuilder)
}
}And for the test scope:
extension InspectableAlertWithItem: ItemPopupPresenter { }Feel free to add both sets to the project as needed.
Just like with Alert, there are two APIs for showing ActionSheet in SwiftUI - a simple one taking a isPresented: Binding<Bool> parameter, and a generic version taking item: Binding<Item?> parameter.
extension View {
func actionSheet2(isPresented: Binding<Bool>, content: @escaping () -> ActionSheet) -> some View {
return self.modifier(InspectableActionSheet(isPresented: isPresented, popupBuilder: content))
}
}
struct InspectableActionSheet: ViewModifier {
let isPresented: Binding<Bool>
let popupBuilder: () -> ActionSheet
let onDismiss: (() -> Void)? = nil
func body(content: Self.Content) -> some View {
content.actionSheet(isPresented: isPresented, content: popupBuilder)
}
}Test target:
extension InspectableActionSheet: PopupPresenter { }extension View {
func actionSheet2<Item>(item: Binding<Item?>, content: @escaping (Item) -> ActionSheet) -> some View where Item: Identifiable {
return self.modifier(InspectableActionSheetWithItem(item: item, popupBuilder: content))
}
}
struct InspectableActionSheetWithItem<Item: Identifiable>: ViewModifier {
let item: Binding<Item?>
let popupBuilder: (Item) -> ActionSheet
let onDismiss: (() -> Void)? = nil
func body(content: Self.Content) -> some View {
content.actionSheet(item: item, content: popupBuilder)
}
}Test target:
extension InspectableActionSheetWithItem: ItemPopupPresenter { }Make sure to use actionSheet2 in your view's body (or a different name of your choice).
Similarly to the Alert and ActionSheet, there are two APIs for presenting the Sheet thus two sets of snippets to add to the project, depending on your needs.
extension View {
func sheet2<Sheet>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Sheet
) -> some View where Sheet: View {
return self.modifier(InspectableSheet(isPresented: isPresented, onDismiss: onDismiss, popupBuilder: content))
}
}
struct InspectableSheet<Sheet>: ViewModifier where Sheet: View {
let isPresented: Binding<Bool>
let onDismiss: (() -> Void)?
let popupBuilder: () -> Sheet
func body(content: Self.Content) -> some View {
content.sheet(isPresented: isPresented, onDismiss: onDismiss, content: popupBuilder)
}
}Test target:
extension InspectableSheet: PopupPresenter { }extension View {
func sheet2<Item, Sheet>(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, content: @escaping (Item) -> Sheet
) -> some View where Item: Identifiable, Sheet: View {
return self.modifier(InspectableSheetWithItem(item: item, onDismiss: onDismiss, popupBuilder: content))
}
}
struct InspectableSheetWithItem<Item, Sheet>: ViewModifier where Item: Identifiable, Sheet: View {
let item: Binding<Item?>
let onDismiss: (() -> Void)?
let popupBuilder: (Item) -> Sheet
func body(content: Self.Content) -> some View {
content.sheet(item: item, onDismiss: onDismiss, content: popupBuilder)
}
}Test target:
extension InspectableSheetWithItem: ItemPopupPresenter { }Don't forget that you'll need to use sheet2 in place of sheet in your views.
Similarly to the Alert and Sheet, there are two APIs for presenting the FullScreenCover thus two sets of snippets to add to the project, depending on your needs.
extension View {
func fullScreenCover2<FullScreenCover>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> FullScreenCover
) -> some View where FullScreenCover: View {
return self.modifier(InspectableFullScreenCover(isPresented: isPresented, onDismiss: onDismiss, popupBuilder: content))
}
}
struct InspectableFullScreenCover<FullScreenCover>: ViewModifier where FullScreenCover: View {
let isPresented: Binding<Bool>
let onDismiss: (() -> Void)?
let popupBuilder: () -> FullScreenCover
func body(content: Self.Content) -> some View {
content.fullScreenCover(isPresented: isPresented, onDismiss: onDismiss, content: popupBuilder)
}
}Test target:
extension InspectableFullScreenCover: PopupPresenter { }extension View {
func fullScreenCover2<Item, FullScreenCover>(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, content: @escaping (Item) -> FullScreenCover
) -> some View where Item: Identifiable, FullScreenCover: View {
return self.modifier(InspectableFullScreenCoverWithItem(item: item, onDismiss: onDismiss, popupBuilder: content))
}
}
struct InspectableFullScreenCoverWithItem<Item, FullScreenCover>: ViewModifier where Item: Identifiable, FullScreenCover: View {
let item: Binding<Item?>
let onDismiss: (() -> Void)?
let popupBuilder: (Item) -> FullScreenCover
func body(content: Self.Content) -> some View {
content.fullScreenCover(item: item, onDismiss: onDismiss, content: popupBuilder)
}
}Test target:
extension InspectableFullScreenCoverWithItem: ItemPopupPresenter { }Don't forget that you'll need to use fullScreenCover2 in place of fullScreenCover in your views.
extension View {
func popover2<Popover>(isPresented: Binding<Bool>,
attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds),
arrowEdge: Edge = .top,
@ViewBuilder content: @escaping () -> Popover
) -> some View where Popover: View {
return self.modifier(InspectablePopover(
isPresented: isPresented,
attachmentAnchor: attachmentAnchor,
arrowEdge: arrowEdge,
popupBuilder: content))
}
}
struct InspectablePopover<Popover>: ViewModifier where Popover: View {
let isPresented: Binding<Bool>
let attachmentAnchor: PopoverAttachmentAnchor
let arrowEdge: Edge
let popupBuilder: () -> Popover
let onDismiss: (() -> Void)? = nil
func body(content: Self.Content) -> some View {
content.popover(isPresented: isPresented, attachmentAnchor: attachmentAnchor,
arrowEdge: arrowEdge, content: popupBuilder)
}
}Test target:
extension InspectablePopover: PopupPresenter { }extension View {
func popover2<Item, Popover>(item: Binding<Item?>,
attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds),
arrowEdge: Edge = .top,
content: @escaping (Item) -> Popover
) -> some View where Item: Identifiable, Popover: View {
return self.modifier(InspectablePopoverWithItem(
item: item,
attachmentAnchor: attachmentAnchor,
arrowEdge: arrowEdge,
popupBuilder: content))
}
}
struct InspectablePopoverWithItem<Item, Popover>: ViewModifier where Item: Identifiable, Popover: View {
let item: Binding<Item?>
let attachmentAnchor: PopoverAttachmentAnchor
let arrowEdge: Edge
let popupBuilder: (Item) -> Popover
let onDismiss: (() -> Void)? = nil
func body(content: Self.Content) -> some View {
content.popover(item: item, attachmentAnchor: attachmentAnchor,
arrowEdge: arrowEdge, content: popupBuilder)
}
}Test target:
extension InspectablePopoverWithItem: ItemPopupPresenter { }Don't forget that you'll need to use popover2 in place of popover in your views.