-
-
Notifications
You must be signed in to change notification settings - Fork 6
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
Introduce handling for identifiable onboarding views #45
Introduce handling for identifiable onboarding views #45
Conversation
Regarding the We both want the view's type name as a default value for In the case that the implementing view does not specify extension OnboardingIdentifiableView {
public func onboardingIdentifier(_ id: String) -> Self {
var copy = self
copy.id = id
return copy
}
} At the moment I can imagine two ways out of this:
|
I just stumbled across the PR and had some random thoughts. I thought I just add them here. Hope this adds to some of the discussion points. If there are already other directions you went and discussed just ignore these points 🙈 Does it make senes to avoid implementing a custom As you explored the Otherwise, a different approach could be to use the modifier(_:) to implement a |
To add my thoughts to @Supereg's answer (thanks a lot for your input!): I definitely agree with the I briefly explored using the SwiftUI-native |
Wouldn't even bother to create a type alias for that. Just View and Identifiable conformance that's it. |
Hello @Supereg and @philippzagar, Thank you for your thorough review! I replaced the Furthermore, I created a new Please let me know what you think! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @felixschlegel,
thanks for all the improvements here. I think the current approach turned out great and we are on a good way forward 🚀
I had a few, smaller comments here in there that would add some refinements. Only one bigger comment, where I'm currently not sure if the implementation of the id(_:)
modifier actually works, but I might just be wrong and missing something.
Reach out if there are any questions or if you already discussed different solutions with Paul.
import SwiftUI | ||
|
||
/// Wrap `Content` `View` in an `Identifiable` `View`. | ||
public struct OnboardingIdentifiableView<Content>: View, Identifiable where Content: View { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this view private/internal as it doesn't seems like this should be used directly?
Otherwise, if that doesn't work, it might make sense to prefix it with _
(e.g., _OnboardingIdentifiableView
) such that it doesn't show up in the Xcode autocompletion and documentation?
/// Unique identifier of the wrapped `View`. | ||
public let id: String | ||
/// Wrapped `View`. | ||
public var body: Content |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be let probably?
/// When applying this modifier repeatedly, the outermost ``id(_:)`` counts. | ||
/// - Parameters: | ||
/// - identifier: The `String` identifier given to the view. | ||
public func id(_ identifier: String) -> some View { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does it have to be a String? Can't we just use an arbitrary Hashable identifier (as the Identifiable
protocol requires it)?
Also, isn't the current naming conflicting with SwiftUI's id(_:)
modifier or how do we ensure disambiguity?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As this is new public interface, make sure to build the DocC bundle once and see if we can highlight this modifier somewhere in the main topics section (or on the topics section of a nested documentation type) to ensure exploitability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a code example to the DocC documentation of OnboardingStack
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great, thank you 👍
|
||
extension View { | ||
/// `ViewModifier` assigning an identifier to the `View` it is applied to. | ||
/// When applying this modifier repeatedly, the outermost ``id(_:)`` counts. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sene to add a Note
bubble to the documentation, that this modifier only has an effect if the view modifier is directly used in the OnboardingStack view builder (maybe even demonstrate this with a short code example)?
public var body: Content | ||
} | ||
|
||
struct OnboardingIdentifiableViewModifier: ViewModifier { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can also be private
/// - Parameters: | ||
/// - identifier: The `String` identifier given to the view. | ||
public func id(_ identifier: String) -> some View { | ||
modifier(OnboardingIdentifiableViewModifier(identifier: identifier)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To my understanding this modifier is currently not tested and I have the suspicion that this doesn't work as this has a return type of type ModifiedContent<Self, OnboardingIdentifiableViewModifier> which does conform to View, but doesn't conform to Identifiable. We might need to add a conditional conformance to ModifiedContent if
Content` is Identifiable, so we can detect a modified, identifiable view (e.g., also if there are other modifiers used with a normal view etc).
Also, do we ever consider Identifiable views in the @OnboardingViewBuilder
, doesn't currently seem like it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing that out, it was naive of me to not have a test for this!
The reason why we cannot use Content: Identifiable
as a type constraint here is that Content
refers to the View
before the modifier is applied, thus the View
that we want to make Identifiable
.
Therefore I extended ModifiedContent
to be Identifiable
when its Modifier
is Identifiable
(see code).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For what exact reason do you want to consider Identifiable
views in the OnboardingViewBuilder
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Therefore I extended ModifiedContent to be Identifiable when its Modifier is Identifiable (see code).
Sounds great. Yes, I confused the Content
associated type. Making the modifier itself conform to Identifiable is the way to go. Thank you.
For what exact reason do you want to consider Identifiable views in the OnboardingViewBuilder?
My approach would have been to construct OnboardingStepIdentifier
s already within the OnboardingViewBuilder
. This would eliminate the dynamic type check within the OnboardingStepIdentifier
, though would also require to create a dedicated component type that stores this within the collection, instead of just using [any View]
.
I'm not sure if it is just me learning Swift at a time where dynamic type checks using protocols that contained associated type requirements weren't possible 🙊.
Just to illustrate my approach, you would have added expression builders as below. No need to implement, just to illustrate my train of thought here.
public static func buildExpression<Content: View & Identifiable>(_ expression: Content) -> [OnboardingView]
// bonus, would eliminate the Identifiable extension on ModifiedContent.
public static func buildExpression<Content: View, Modifier: Identifiable>(_ expression: ModifiedContent<Content, Modifier>) -> [OnboardingView]
public static func buildExpression<Content: View>(_ expression: Content) -> [OnboardingView]
/// It isn't required to declare this view within the ``OnboardingStack``. | ||
public func append<V: View & Identifiable>(customView: V) { | ||
let customOnboardingStepIdentifier = OnboardingStepIdentifier( | ||
onboardingStepType: String(describing: customView.id), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we turning the Hashable
id into a String here? There is no guarantee that the String representation of two Hashables that have the same hash is the same.
I would propose to change the implementation of OnboardingStepIdentifier
. Making onboardingStepType
to store the hash of the id of a given onboarding view (maybe renaming the property would also make sense to reflect the new semantics).
Refer to the documentation of Hasher
on how to calculate the hash of a Hashable
.
@PSchmiedmayer I tried to workaround our issues with codecov by adding a repository upload token and updating the pull request action. This doesn't seem to work (maybe as this PR comes from an external repository). Not sure how to best resolve this issue? |
@Supereg Thank you for the review; great input! Regarding the code coverage issue: Tokenless uploads should be supported for forks when creating PRs for public repos: https://docs.codecov.com/docs/codecov-uploader#supporting-tokenless-uploads . Might very well be that codecov is broken here, we are having the exact same flow as described in the docs. |
538b10b
to
167f639
Compare
Hey @Supereg , Thank you so much for this great review! I left some comments inline and overhauled the entire code.
Technically, Best wishes, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the hard work here. 🚀 Looks good from my side 🥳
Last few comments were mainly about some minor improvements regarding documentation and some unused code we can eliminate 👍
/// | ||
/// ### Identifying Onboarding Views | ||
/// | ||
/// Apply the `onboardingIdentifier(_:)` modifier to clearly identify a view in the `OnboardingStack`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this a DocC symbol to properly link to the modifier.
/// Apply the `onboardingIdentifier(_:)` modifier to clearly identify a view in the `OnboardingStack`. | |
/// Apply the ``SwiftUI/View/onboardingIdentifier(_:)`` modifier to clearly identify a view in the `OnboardingStack`. |
You might need to verify that this is spelled correctly.
/// } | ||
/// ``` | ||
/// | ||
/// - Note: When the `onboardingIdentifier(_:)` modifier is applied multiple times to the same view, the most recently applied identifier takes precedence. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
/// - Note: When the `onboardingIdentifier(_:)` modifier is applied multiple times to the same view, the most recently applied identifier takes precedence. | |
/// - Note: When the ``SwiftUI/View/onboardingIdentifier(_:)`` modifier is applied multiple times to the same view, the outermost identifier takes precedence. |
OnboardingIdentifiableView( | ||
id: self.id, | ||
body: content | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something I noticed just now, I think it isn't necessary to wrap this into a OnboardingIdentifiableView
anymore and we can remove the view above, as the ViewModifier now conforms to Identifiable
.
/// ```swift | ||
/// struct Onboarding: View { | ||
/// @AppStorage(StorageKeys.onboardingFlowComplete) var completedOnboardingFlow = false | ||
/// | ||
/// var body: some View { | ||
/// OnboardingStack(onboardingFlowComplete: $completedOnboardingFlow) { | ||
/// MyOwnView().onboardingIdentifier("my-own-view-1") | ||
/// MyOwnView().onboardingIdentifier("my-own-view-2") | ||
/// } | ||
/// } | ||
/// } | ||
/// ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for adding the example to the modifier docs. This looks great 🚀
/// `ViewModifier` assigning an identifier to the `View` it is applied to. | ||
/// When applying this modifier repeatedly, the outermost ``onboardingIdentifier(_:)`` counts. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DocC uses the first line of a documentation as a brief summary and embeds that e.g. into the topics section at the bottom a documentation page to provide some details of a reference type. Any text following the first empty line is put into the overview section of a symbol (see Writing symbol documentation in your source files).
I would suggest to put a single, short and descriptive sentence into the first line of a type or symbol.
Feel free to adapt to your liking.
/// `ViewModifier` assigning an identifier to the `View` it is applied to. | |
/// When applying this modifier repeatedly, the outermost ``onboardingIdentifier(_:)`` counts. | |
/// Assign a unique identifier to a `View` appearing in an `OnboardingStack`. | |
/// | |
/// A `ViewModifier` assigning an identifier to the `View` it is applied to. | |
/// When applying this modifier repeatedly, the outermost ``onboardingIdentifier(_:)`` counts. |
OnboardingTestViewNotIdentifiable(text: "Leland").onboardingIdentifier("a") | ||
OnboardingTestViewNotIdentifiable(text: "Stanford").onboardingIdentifier("b") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for adding tests for the modifier 🚀
XCTAssert(app.staticTexts["Leland"].waitForExistence(timeout: 2)) | ||
XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) | ||
app.buttons["Next"].tap() | ||
|
||
XCTAssert(app.staticTexts["Stanford"].waitForExistence(timeout: 2)) | ||
XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) | ||
app.buttons["Next"].tap() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We assert that these views are inserted in the right order. But are we asserting that their identifiers are set as expected within those tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the moment we are not, we cannot test the internal state from a UI Test so the only possibility here would be to assert the value of a TextView
which would have to access the id
from its wrapping OnboardingIdentifiableViewModifier
which is all too much of an effort for such a basic assertion imo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would highly recommend that we add a test for that. It's very implicit that this works currently (we rely on the Identifiable extension of ModifiedContent
) and it might easily break without someone noticing with future changes.
I agree, that it is pretty clumsy to do this using a UI test. I would propose the following unit test that would verify this behavior and prevent future regressions. Just add it as part of the unit tests of the swift package.
I would argue that we can make the firstOnboardingStepIdentifier
property internal (currently it is private) to support this unit test.
@MainActor
func testOnboardingIdentifierModifier() throws {
let stack = OnboardingStack {
Text("Hello World")
.onboardingIdentifier("Custom Identifier")
}
let identifier = try XCTUnwrap(stack.onboardingNavigationPath.firstOnboardingStepIdentifier)
var hasher = Hasher()
hasher.combine("Custom Identifier")
let final = hasher.finalize()
XCTAssertEqual(identifier.identifierHash, final)
}
Thank you for the detailed review @Supereg 🚀 |
Motivation: Closes StanfordSpezi#43. Modifications: * introduce `OnboardingIdentifiableView` `protocol` requiring `id` property * use view's type name if `id` not explicitly specified * create new UI Test for the new idenfitiable onboarding views
Modifications: * replace `protocol` `OnboardingIdentifiableView` with simple conformance to `View & Identifiable` * create a new view modifier `.id(_:)` wrapping any `View` in a `struct OnboardingIdenfiableView` which is `View & Identifiable` and contains the specified `id` as a property * hoist `OnboardingStepIdentifier` overloaded intializers into different `OnboardingNavigationPath.append()` methods * remove `OnboardingIdentifiableTestViewDefault`
Modifications: * identify `OnboardingStack` views through `id: any Hashable` instead of `id: String` * rename `.id(_:)` view modifier to `.onboardingIdentifier(_:)` to disambiguate from SwiftUI's own `.id(_:)` view modifier * refactor `OnboardingStepIdentifier` to store a custom hash of the given view / view id * add `extension ModifiedContent` to conditionally make it `Identifiable` * adjust UITest to verify that the `.onboardingIdentifier(_:)` works * add `.onboardingIdentifier(_:)` example in `OnboardingStack` DocC documentation * make sure to only expose API that has to be exposed
Modifications: * add links in DocC * remove superflous type `OnboardingIdentifiableView`
167f639
to
74aaecb
Compare
Just added a small comment on how we can fully test the new behavior: #45 (comment) Feel free to mark all comments resolved that you addressed. Then feel free to merge 🚀 |
Modifications: * change `OnboardingNavigationPath.firstOnboardingStepIdentifier` access level from `private` to `internal`
Hey @Supereg , thanks for giving this so much thought! I implemented your suggested change so we should be good to go @PSchmiedmayer 🚀 |
@Supereg @PSchmiedmayer I cannot merge since codecov fails |
@felixschlegel Seems like this issue is stemming from a codecov rate limit as we are using a fork and this uses the upload API without a token. I will merge the PR despite this error, it shouldn't affect a build on our main branch or if you use a feature branch in this repo. Thank you for all the work here and thank you for the reviews @Supereg! |
Introduce handling for identifiable onboarding views
♻️ Current situation & Problem
Closes #43.
Previously, the
OnboardingStack
identified views by their type name for ordered navigation operations.By introducing the protocol
OnboardingIdentifiableView
we require views to set anid: String
allowing for multiple views of the same type to be part of anOnboardingStack
.⚙️ Release Notes
OnboardingIdentifiableView
(new):protocol
OnboardingIdentifiableView
conforming toView
andIdentifiable
id
that uses the view's type name as its identifierOnboardingNavigationPath
(changed):func append(identifiableView: any OnboardingIdentifiableView)
that allows navigating to any view that conforms to the newOnboardingIdentifiableView
protocolExample Usage:
📚 Documentation
Documentation added to all new public interface members.
✅ Testing
testIdentifiableViews
that clicks through a set of views with both the default view id and custom view identifiers📝 Code of Conduct & Contributing Guidelines
By submitting creating this pull request, you agree to follow our Code of Conduct and Contributing Guidelines: