Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add inspection support for ViewThatFits. #255

Merged
merged 1 commit into from
Aug 26, 2023

Conversation

rafael-assis
Copy link

Changes

Similar to the issue fixed by #228, this change aims to fix blocking inspection for Views that are nested inside a ViewThatFits View.

ViewThatFits has a structure similar to a TreeView and needs to be inspected for its content property for ViewInspector to find its children.

ViewThatFits_debugger

Discussion

This is a first step that unblocks the inspection of children Views of ViewThatFits.

However, the documented behavior of this View states that only one of its children will be provided (rendered) based on horizontal space. Implementing inspection in a way that all children Views are returned may be problematic as all children are likely going to be similar in content, labels and accessibility traits.

This can potentially cause multiple Views returned in an inspection done with a findAll(where:) call.

I will investigate the possibility of hosting the ViewThatFits View inside a ViewHosting View and validate whether only one of the children view is available. That will be the scenario that is the closest to SwiftUI running on a real simulator/device.

Another suggestion we're considering is the addition of a capability that enables the test code to pick an index of the child view wanted. Alternatively we can just always default to the first child in the ViewThatFits.

@nalexn we'd love to have your thoughts and recommendations on this.

Testing

I added a test to explicitly find 2 children Views inside a ViewThatFits, which enforces the behavior being proposed in this PR.

Review

@bachand
@nalexn

static func child(_ content: Content) throws -> Content {
let view: Any = try {
guard let rootContent = try? Inspector.attribute(path: "_tree|content", value: content.view) else {
// A ViewThatFits View renders only one of its child Views based on the available horizontal space.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment was added to explicitly document that we currently return all children of a ViewThatFits View.

cc: @bachand

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inspector.attribute(path: "_tree|content", value: content.view) is what will return all children right? If so I wonder if this comment should go above that line instead of in the else of the guard statement.

let shortText = try sut.inspect().find(text: "Short text")

XCTAssertEqual(try longText.string(), "Very long text that will only get picked if there is available space.")
XCTAssertEqual(try shortText.string(), "Short text")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test intentionally validates that both children Views are inspectable even though in a real runtime scenario only one of them would be provided.

Copy link
Contributor

@bachand bachand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thank you for unblocking inspection of ViewThatFits, which is used in some critical views within our codebase. Will be curious to see what @nalexn thinks about this change and about whether it makes sense to provide further customization options during inspection (e.g. an option to return the first child of ViewThatFits, instead of all children).

import SwiftUI

@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
internal extension ViewType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be public? For comparison:

public extension ViewType {
struct VStack: KnownViewType {
public static let typePrefix: String = "VStack"
}
}

static func child(_ content: Content) throws -> Content {
let view: Any = try {
guard let rootContent = try? Inspector.attribute(path: "_tree|content", value: content.view) else {
// A ViewThatFits View renders only one of its child Views based on the available horizontal space.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// A ViewThatFits View renders only one of its child Views based on the available horizontal space.
// A ViewThatFits View renders only one of its child Views based on the available space.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The view accepts an axis on initialization.
Screenshot 2023-08-23 at 10 39 31 AM

static func child(_ content: Content) throws -> Content {
let view: Any = try {
guard let rootContent = try? Inspector.attribute(path: "_tree|content", value: content.view) else {
// A ViewThatFits View renders only one of its child Views based on the available horizontal space.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inspector.attribute(path: "_tree|content", value: content.view) is what will return all children right? If so I wonder if this comment should go above that line instead of in the else of the guard statement.

// A ViewThatFits View renders only one of its child Views based on the available horizontal space.
// This inspection returns all children that can potentially be picked by `ViewThatFits`.
// https://developer.apple.com/documentation/swiftui/viewthatfits
return try Inspector.attribute(path: "content", value: content.view)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help me understand this fallback? When would we expect this to be encountered?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we had similar code in #228 but I want to make sure it's necessary here.


return rootContent
}()
return try Inspector.unwrap(view: view, medium: content.medium)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nalexn can you help me better understand what it's appropriate to invoke this unwrap(…) method? I am trying to understand if we should always be unwrapping when extracting content from views.

Copy link
Owner

@nalexn nalexn Aug 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unwrap should be called when you get to a user-provided view that might be "wrapped" in modifiers. For example, AnyView(x). This x can be a standard view, a custom user-defined view, or either of these but "wrapped" in a modifier, such as x.padding(10). "unwrap" makes sure the root view of "x" is revealed for ease of further inspection (but modifiers are remembered as well, just stored separately).
On the other hand, when you inspect an internal child of a standard SwiftUI view, there is no need to "unwrap" them, if you can see from the code there is no generic parameter that can alter the view and turn it from Text into a ModifiedContent<Text>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the explanation!

@@ -0,0 +1,26 @@
import XCTest
import SwiftUI
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import SwiftUI
import SwiftUI
@testable import ViewInspector

@nalexn
Copy link
Owner

nalexn commented Aug 24, 2023

Thanks! I'll review the PR as soon as possible. As a side note, I feel like I've already given you this tip, but instead of po view you can use po Inspector.print(view) as AnyObject. It's identical to po view for primitive views, but gives much better results on complex hierarchies.

@rafael-assis
Copy link
Author

Thanks! I'll review the PR as soon as possible. As a side note, I feel like I've already given you this tip, but instead of po view you can use po Inspector.print(view) as AnyObject. It's identical to po view for primitive views, but gives much better results on complex hierarchies.

Thank you @nalexn for making time to review it. We appreciate your efforts here!

nalexn added a commit that referenced this pull request Aug 26, 2023
nalexn added a commit that referenced this pull request Aug 26, 2023
nalexn added a commit that referenced this pull request Aug 26, 2023
@nalexn nalexn merged commit 0f872b5 into nalexn:0.9.8 Aug 26, 2023
@nalexn nalexn added pending release A fixed issue that'll be released in an upcoming update and removed pending release A fixed issue that'll be released in an upcoming update labels Aug 26, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants