Skip to content

Commit

Permalink
Merge pull request #5 from bookingcom/gtarasov/if_in_introspection
Browse files Browse the repository at this point in the history
Fix SwiftUI root view introspection for `if` conditions
  • Loading branch information
pilot34 authored Dec 19, 2023
2 parents 9f58fa6 + 2295058 commit 42b1d17
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 4 deletions.
13 changes: 9 additions & 4 deletions PerformanceSuite/Sources/RootViewIntrospection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ final class RootViewIntrospection {
/// Looking for all the meaningful views in the hierarchy.
/// Method similar to `rootView(view:)`, but it looks deeper into the hierarchy and looks not for the single child,
/// but for all possible meaningful views.
/// For example, it takes all the children of `VStack { ... }`, takes both values of `.if(...)`, etc.
/// For example, it takes all the children of `VStack { ... }`, `HStack { ... }`, etc.
/// - Parameter view: view hierarchy to look up in
/// - Returns: all children views we consider meaningful
func meaningfulViews(view: Any) -> [Any] {
Expand All @@ -64,26 +64,31 @@ final class RootViewIntrospection {

if hadChild {
return result
} else if mirror.displayStyle == .optional && mirror.children.isEmpty {
// this is an optional value with nil inside, return nothing
return []
} else {
// no children found, returning view itself
return [view]
}
}

private let possibleRootChildAttributeNames: Set<String?> = [
"some", // is used in Optional<SomeView>
"storage", // is used in AnyView
"storage", // is used in AnyView, Text
"anyTextStorage", // is used in Text
"view", // is used in AnyViewStorage
"content", // is used in ModifiedContent
"base", // is used in ModifiedElements
"_tree", // is used in VStack/HStack
"tree", // is used in LazyVStack/LazyHStack
"value", // is used in TupleView
"custom", // is used in Base
"trueContent", // is used in `if ... else ...`
"falseContent", // is used in `if ... else ...`
]

private let possibleMeaningfulChildAttributeNames: Set<String?> = [
"trueContent", // is used in `if ... else ...`
"falseContent", // is used in `if ... else ...`
".0", ".1", ".2", ".3", ".4", ".5", ".6", ".7", ".8", ".9", // are used in view builders with multiple children
"elements", // is used in _ViewList_View
]
Expand Down
124 changes: 124 additions & 0 deletions PerformanceSuite/Tests/RootViewIntrospectorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ class RootViewIntrospectionTests: XCTestCase {

private let introspector = RootViewIntrospection()

func testMyView() {
let view = MyView()
let rootView = introspector.rootView(view: view)
XCTAssert(rootView is MyView)
}

func testAnyView() {
let view = AnyView(MyView())
let rootView = introspector.rootView(view: view)
Expand Down Expand Up @@ -67,7 +73,47 @@ class RootViewIntrospectionTests: XCTestCase {
}
}

func testIfViewTrue() {
condition = true
let controller = UIHostingController(rootView: ifView)
let root = controller.introspectRootView()
XCTAssert(root is MyFirstView)
}

func testIfViewFalse() {
condition = false
let controller = UIHostingController(rootView: ifView)
let root = controller.introspectRootView()
XCTAssert(root is MySecondView)
}

var condition: Bool = false

@ViewBuilder var ifView: some View {
if condition {
MyFirstView()
} else {
MySecondView()
}
}

private struct MyFirstView: View {
var body: some View {
Text("first")
}
}

private struct MySecondView: View {
var body: some View {
Text("second")
}
}

private struct MyView: View {
// adding some props to make sure Mirror.children are not empty
let prop1 = "my_prop1"
var prop2 = 2

var body: some View {
Text("blablabla")
.frame(width: 10, height: 20, alignment: .center)
Expand Down Expand Up @@ -101,11 +147,26 @@ class RootViewIntrospectionTests: XCTestCase {
XCTAssertEqual("MyViewController", introspector.description(viewController: myController))
}

func testDescriptionForText() {
let controller = UIHostingController(rootView: Text("my text"))
XCTAssertEqual("LocalizedTextStorage", introspector.description(viewController: controller))
}

func testDescriptionForTextWithModifier() {
let controller = UIHostingController(rootView: Text("my text").onAppear { })
XCTAssertEqual("LocalizedTextStorage", introspector.description(viewController: controller))
}

func testDescriptionForSimpleView() {
let controller = UIHostingController(rootView: MyView())
XCTAssertEqual("MyView", introspector.description(viewController: controller))
}

func testDescriptionForSimpleViewWithModifiers() {
let controller = UIHostingController(rootView: MyView().onAppear { }.onDisappear { }.onTapGesture { })
XCTAssertEqual("MyView", introspector.description(viewController: controller))
}

func testDescriptionForComplexView() {
let controller = UIHostingController(rootView: makeComplexView())
XCTAssertEqual("MyView", introspector.description(viewController: controller))
Expand All @@ -116,6 +177,18 @@ class RootViewIntrospectionTests: XCTestCase {
XCTAssertEqual("InputText, SearchDestinationList, ProgressView", introspector.description(viewController: controller))
}

func testDescriptionForVeryComplexViewWithConditionsTrue() {
condition = true
let controller = UIHostingController(rootView: makeComplexViewWithConditions())
XCTAssertEqual("InputText", introspector.description(viewController: controller))
}

func testDescriptionForVeryComplexViewWithConditionsFalse() {
condition = false
let controller = UIHostingController(rootView: makeComplexViewWithConditions())
XCTAssertEqual("SearchDestinationList, ProgressView", introspector.description(viewController: controller))
}

@ViewBuilder private func makeVeryComplexView() -> some View {
LazyHStack {
EmptyView()
Expand Down Expand Up @@ -160,6 +233,57 @@ class RootViewIntrospectionTests: XCTestCase {
}
}
}

@ViewBuilder private func makeComplexViewWithConditions() -> some View {
LazyHStack {
EmptyView()
Button("my title", action: { })
VStack {
EmptyView()
ZStack {
if condition {
CardContainer {
HStack {
Button("Button") {
print("Button Tapped")
}
.padding()
.background(Color.blue)
.zIndex(1.0)
if !condition {
AnyView(SearchDestinationList())
}
InputText()
.accessibility(identifier: "inputText")
.font(.callout)
.autocorrectionDisabled()
}
}
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(lineWidth: 2)
.foregroundColor(.gray)
)
.padding()
.padding()
.zIndex(1.0)
} else {
if !condition {
AnyView(SearchDestinationList())
}

VStack {
ProgressView()
.padding()
Spacer()
}
}
}
Spacer()
EmptyView()
}
}
}
}

private class MyViewController: UIViewController { }
Expand Down

0 comments on commit 42b1d17

Please sign in to comment.