SwiftUI.View does not have properties like a standard view. There is no frame, no color, nothing that a view should have. It is just a protocol that you can conform ANY value to. It does not even have to be a struct. It is just a value.
All functionality that is inside the SwiftUI.View conforming value is some business logic.
The body property is also some business logic. We usually keep body inside the SwiftUI.View struct. We never decouple body into some piece of junk code just so we could test it.
However we can decouple some of the business logic out of the SwiftUI.View into a separate unit of code, but we have to do it properly.
In his SwiftData article, Paul Hudson clearly shows the failure of Observable classes with SwiftData and states:
a number of people have said outright that they think MVVM is dead with SwiftData
Actually, this kind of class type abusement called MVVM should have been dead long time ago in SwiftUI. I described it in more detail here: From SwiftUI to MVVM .
Unlike Observable classes the DynamicProperty structs will fully work inside SwiftUI view hierachies. We can decouple some of the business logic out of the SwiftUI.View without breaking stuff:
@propertyWrapper struct SwiftDataModel: DynamicProperty {
@Environment(\.modelContext) private var modelContext
@Query var items: [Item]
func addItem() {}
}
struct SwiftDataView: View {
@SwiftDataModel var model
var body: some View {
VStack {
Text("There are \(model.items.count) items")
Button("Add item", action: model.addItem)
}
}
}
We can test our model in the same way as we would test a view because it is a struct, a value - so it can conform to SwiftUI.View!
For models we use @ViewModelify:
@ViewModelify
@propertyWrapper struct SwiftDataModel: DynamicProperty {
@Environment(\.modelContext) private var modelContext
@Query var items: [Item]
func addItem() {}
}
For views we use @InspectedView with var inspectedBody:
@InspectedView
struct SomeView {
var inspectedBody: some View {
Text("text")
}
}
For view modifiers we use @InspectedViewModifier with func inspectedBody:
@InspectedViewModifier
struct TestModifier {
func inspectedBody(content: Content) -> some View {
content.disabled(true)
}
}
This package implements some of the boilerplate for out APP target:
- Inspection class
- applyViewInspectorModifiers
Only thing left for you to add in your TEST target is:
import ViewModelify
import ViewInspector
extension Inspection: InspectionEmissary { }