ViewInspector is a library for unit testing SwiftUI-based projects. It allows for traversing SwiftUI view hierarchy in runtime providing direct access to the underlying View structs.
SwiftUI views are a function of state. We can provide the input, but couldn't verify the output. Until now.
You can dig into the hierarchy and read actual state values on any SwiftUI View:
let view = ContentView()
let value = try view.inspect().text().string()
XCTAssertEqual(value, "Hello, world!")
It is possible to obtain a copy of your custom view with actual state and references from the hierarchy of any depth:
let customView = try view.inspect().anyView().view(CustomView.self)
let sut = customView.actualView()
XCTAssertTrue(sut.isToggleOn)
Simulate user interaction by programmatically triggering system controls callbacks:
let view = ContentView()
let button = try view.inspect().hStack().button(3)
try button.tap()
let textField = try view.inspect().hStack().textField(2)
try textField.callOnCommit()
ViewInspector is using official Swift reflection API to dissect the view structures. So this library is production-friendly, although it's strongly recommended to use it for debugging and unit testing purposes only.
- In Xcode select File ⭢ Swift Packages ⭢ Add Package Dependency...
- Copy-paste repository URL: https://github.com/nalexn/ViewInspector
- Hit Next two times, under Add to Target select your test target
- Hit Finish
Cosidering you have a view:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
}
}
Your test file would look like this:
import XCTest
import ViewInspector // 1.
@testable import MyApp
extension ContentView: Inspectable { } // 2.
final class ContentViewTests: XCTestCase {
func testStringValue() throws { // 3.
let sut = ContentView()
let value = try sut.inspect().text().string() // 4.
XCTAssertEqual(value, "Hello, world!")
}
}
So, you need to do the following:
- Add
import ViewInspector
- Extend your view to conform to
Inspectable
in the test target scope. - Annotate the test function with
throws
keyword to not mess with the bulkydo { } catch { }
. Test fails automatically upon exception. - Start the inspection with
.inspect()
function
After the .inspect()
call you need to repeat the structure of the body
by chaining corresponding functions named after the SwiftUI views.
struct MyView: View {
var body: some View {
HStack {
Text("Hi")
AnyView(OtherView())
}
}
}
struct OtherView: View {
var body: some View {
Text("Ok")
}
}
In this case you can obtain access to the Text("Ok")
with the following chain:
let view = MyView()
view.inspect().hStack().anyView(1).view(OtherView.self).text()
Note that after .hStack()
you're required to provide the index of the view you're retrieving: .anyView(1)
. For obtaining Text("Hi")
you'd call .text(0)
.
You can save the intermediate result in a variable and reuse it for further inspection:
let view = MyView()
let hStack = try view.inspect().hStack()
let hiText = try hStack.text(0)
let okText = try hStack.anyView(1).view(OtherView.self).text()
Currently, ViewInspector does not support SwiftUI's native environment injection through .environmentObject(_:)
, however you still can inspect such views by explicitely providing the environment object to every view that uses it. A small refactoring of the view's source code is required.
Consider you have a view that has a @EnvironmentObject
variable:
struct MyView: View {
@EnvironmentObject var state: GlobalState
var body: some View {
Text(state.showHi ? "Hi" : "Bye")
}
}
You can inspect it with ViewInspector after refactoring the following way:
struct MyView: View, InspectableWithEnvObject {
@EnvironmentObject var state: GlobalState
var body: Body {
content(state)
}
func content(_ state: GlobalState) -> some View {
Text(state.showHi ? "Hi" : "Bye")
}
}
After that you can extract the view in tests by explicitely providing the environment object:
let envObject = GlobalState()
let view = MyView()
let value = try view.inspect(envObject).text().string()
XCTAssertEqual(value, "Hi")
For the case when view is embedded in the hierarchy:
let envObject = GlobalState()
let view = HStack { AnyView(MyView()) }
try view.inspect().anyView(0).view(MyView.self, envObject)
Note that you don't need to call .environmentObject(_:)
in these cases.
- AngularGradient
- AnyView
- Button
- ButtonStyleConfiguration.Label
- Custom view (SwiftUI and UIKit)
- DatePicker
- Divider
- EditButton
- EquatableView
- ForEach
- Form
- GeometryReader
- Group
- GroupBox
- HSplitView
- HStack
- Image
- LinearGradient
- List
- MenuButton
- ModifiedContent
- NavigationLink
- NavigationView
- PasteButton
- Picker
- PrimitiveButtonStyleConfiguration.Label
- RadialGradient
- ScrollView
- Section
- SecureField
- Slider
- Stepper
- TabView
- Text
- TextField
- Toggle
- ToggleStyleConfiguration.Label
- VSplitView
- VStack
- ZStack