-
Notifications
You must be signed in to change notification settings - Fork 6
[MOB-3199] Default Browser Settings Card #889
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
[MOB-3199] Default Browser Settings Card #889
Conversation
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Explore these optional code suggestions:
|
Same issue with the failing linter. We can tackle this separately |
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! Awesome to be pushing for more SwiftUI ❤️
I left some questions, I think the only major one would be a discussion about the different approaches to applying themes in SwiftUI, let me know what you think 🙂
firefox-ios/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift
Outdated
Show resolved
Hide resolved
|
||
public static func makeDefaultCoordinatorAndShowDetailViewFrom(_ navigationController: UINavigationController?, | ||
analyticsLabel: Analytics.Label.DefaultBrowser, | ||
topViewContentBackground: Color, |
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 you elaborate why topViewContentBackground
is a configurable parameter? Seems like we always have the same color, so I guess it could be part of the view's styling?
This also introduces a new color snowflake hardcoding EcosiaColor.DarkGreen50.color
, but I guess that part is okay if it's a conscious decision and specific case.
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.
Of course! Let me explain the main reason:
The InstructionStepsView
and its InstructionStepsViewStyle
aim to be as much theme-agnostic as possible.
The EcosiaColor.DarkGreen50.color
valued with 0x275243
doesn't have any dark/light correspondence (hence becoming a snowflake ❄️ ).
The EcosiaColor.DarkGreen50.color
can't be accessed via the Ecosia framework as it lies under the Client/Ecosia
folder.
There is a DefaultBrowserCoordinator
that contains a handy factory function to make the InstructionStepsViewStyle
and inject it into its showDetailView(::)
function.
This coordinator also lies under the Ecosia framework and cannot access the EcosiaColor
constants.
We could pass the style
as parameter of the DefaultBrowserCoordinator.makeDefaultCoordinatorAndShowDetailViewFrom(:::)
but then we would need to create the style twice (one for the EcosiaSettings
and one for the AppSettingsTableViewController+Ecosia
).
This function makes it more straightforward to make the style once and pass the topViewContentBackground
and its theme
(as the coordinator not being a SwiftUI.View
but only a navigation pattern that we can bin 🗑️ whenever we want, keeping the UI InstructionStepsView
theme agnostic).
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! Agreed, we can keep it.
This coordinator also lies under the Ecosia framework and cannot access the EcosiaColor constants.
This one issue I think we'll need to fix in the future, but no need now since there are other reasons behind it 🙏
firefox-ios/Ecosia/UI/Settings/DefaultBrowser/DefaultBrowserCoordinator.swift
Show resolved
Hide resolved
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { | ||
let headerView = super.tableView( | ||
tableView, | ||
viewForHeaderInSection: section | ||
) as! ThemedTableSectionHeaderFooterView | ||
return headerView | ||
} | ||
*/ | ||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { | ||
if shouldShowDefaultBrowserNudgeCardInSection(section), |
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 will always be the first section, won't it? Any reason for having the shouldShowDefaultBrowserNudgeCardInSection
instead of hardcoding?
You'd still need to check the User object to see if it should be shown, but I wonder why we need the accessibilityIdentifier
logic.
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.
Mmmmh good point. The suggestion would basically check whether we are at the first section (and the card should be shown) instead of a more complex check via the accessibilityIdentifier
?
To better elaborate it, I chose to check whether:
- it is the first section
- the accessibility identifier is "default browser"
...only to make sure we are adding the card in the correct place, as if the card shouldn't be shown, there won't be a Default Browser section either
func applyTheme(theme: Theme) { | ||
mainContainerStackView.backgroundColor = theme.colors.ecosia.backgroundSecondary | ||
closeButton.tintColor = theme.colors.ecosia.iconDecorative | ||
titleLabel.textColor = theme.colors.ecosia.textPrimary | ||
descriptionLabel.textColor = theme.colors.ecosia.textSecondary | ||
actionButton.setTitleColor(theme.colors.ecosia.buttonBackgroundPrimary, for: .normal) | ||
guard let viewModel else { return } | ||
configure(with: viewModel, theme: theme) |
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.
🔌 No dependency on Firefox theming:
All new components (cards, instruction steps, etc.) define their own Style structs (e.g. NudgeCardStyle, InstructionStepsViewStyle) and accept them as parameters.
This avoids relying on shared theme singletons or assumptions from the broader Firefox app.
🧳 Self-contained views:
Each SwiftUI component (e.g. EcosiaText, InstructionStepsView, ConfigurableNudgeCardView) can be previewed, tested, and used independently. Most of them rely on their own "style" (e.g. NudgeCardStyle, InstructionStepsViewStyle).
I noticed all SwiftUI views receive all of the colors on their init and believe the reasoning is explained on the PR description (quote above).
Even so, I think this has two negative consequences:
- When theme changes, we have to manually recreate the whole view and change the root instead of just updating colors like seen on this code here. This is a simple view and not a big problem, but I think for more complex ones this pattern would be an issue.
- The views themselves don't know about their own colors and all of the styling is extracted to the parent hosting view controller (like in this file or on the new coordinator). I think this in the end is harder to read and maintain compared to leaving the responsibility to the views to know their own colors and react to theme changes.
I think those are solved when using @Environment(\.themeManager) var themeManager
and ThemeDidChange
notifications like Firefox does and like we did for the SeedCounterView.
I understand the benefits you mentioned of "This avoids relying on shared theme singletons or assumptions from the broader Firefox app."
and "Each SwiftUI component (e.g. EcosiaText, InstructionStepsView, ConfigurableNudgeCardView) can be previewed, tested, and used independently"
, but can't we still achieve those using the same theme manager pattern?
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.
Love these kinds of discussions on updates and indeed I should have clarified the intentions behind it 🙏 .
As it's SwiftUI, the inner body of its struct gets re-valued every time we try to mutate any of the view's content (e.g. any of its inner view's backgrounds). So it kind of "recreates" the view but I agree that performance-wise it's not the same as destroying and rebuilding it in memory. The reason why I chose this one this time in contrast to the SeedCounterView
(which will still be changed anytime soon in favor of any newer UI) is that this view gets accessed from an inner section of our App which isn't the NTP.
The lifetime of these views is likely less than a view that always stays on the NTP.
For this specific scenario, where the SwiftUI view is wired into the hierarchy via UIKit, we are re-assigning the rootView
of a "sticky" card.
I wouldn't worry much about performance, considering that we will only have one sticky on the NTP.
We could further optimize it, making the viewModel
of that NTPConfigurableNudgeCardCell
conform to equatable
but to the previous point, considering it's just one (and the view itself doesn't hold any specific "state") I don't see the issue. Happy to discuss this any further if you like 💪
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 the explanation! Understand the reasoning better now and agree it makes sense to proceed like this 👍 Tbh the main downside I saw of this structure was about readability and separation of responsibilities, not even much related to performance. Happy to discuss more on other use cases and even pair together at some point 🙌
let nudgeCardStyle = NudgeCardStyle(backgroundColor: Color(theme.colors.ecosia.barBackground), | ||
textPrimaryColor: Color(theme.colors.ecosia.textPrimary), | ||
textSecondaryColor: Color(theme.colors.ecosia.textSecondary), | ||
closeButtonTextColor: Color(theme.colors.ecosia.iconDecorative), | ||
actionButtonTextColor: Color(theme.colors.ecosia.buttonBackgroundPrimary)) |
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.
In addition to the comment above about SwitUI theming, I like this intermediate approach better too, where the view owns their own colors. But I guess this is more related to it being ThemeApplicable
instead of Themeable
, meaning it's parent will call apply theme and handle responding to theme changes.
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 agree with you on that 💪 . We can elaborate even further if the comment above needs more clarification 🙏
7c1bf48
to
f0898d4
Compare
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 the clarifications! All good ✅
func applyTheme(theme: Theme) { | ||
mainContainerStackView.backgroundColor = theme.colors.ecosia.backgroundSecondary | ||
closeButton.tintColor = theme.colors.ecosia.iconDecorative | ||
titleLabel.textColor = theme.colors.ecosia.textPrimary | ||
descriptionLabel.textColor = theme.colors.ecosia.textSecondary | ||
actionButton.setTitleColor(theme.colors.ecosia.buttonBackgroundPrimary, for: .normal) | ||
guard let viewModel else { return } | ||
configure(with: viewModel, theme: theme) |
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 the explanation! Understand the reasoning better now and agree it makes sense to proceed like this 👍 Tbh the main downside I saw of this structure was about readability and separation of responsibilities, not even much related to performance. Happy to discuss more on other use cases and even pair together at some point 🙌
|
||
public static func makeDefaultCoordinatorAndShowDetailViewFrom(_ navigationController: UINavigationController?, | ||
analyticsLabel: Analytics.Label.DefaultBrowser, | ||
topViewContentBackground: Color, |
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! Agreed, we can keep it.
This coordinator also lies under the Ecosia framework and cannot access the EcosiaColor constants.
This one issue I think we'll need to fix in the future, but no need now since there are other reasons behind it 🙏
Implemented by moving the NTPConfigurableNudgeCard into SwiftUI and leverage its details.
- Modify the `AppSettingsTableViewController` so that implements the changes needed to display the custom card first and the classic one later - Replace the `Experiment`naming for a more convenient `Detail` one - Implemented Analytics to track all the click and dismissal event - Added animation and configured to play correctly
Reset it's settings back to standard Firefox to reduce conflicts
In both Settings and details
06c5a28
to
56e52aa
Compare
@lucaschifino updated and tested the work with the |
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.
🎨 ✅ ✨
MOB-3199
Context
We wanted to improve the User's discovery of how to set the Ecosia mobile app as the default browser.
Upon upgrading, all users will see this new card appear, which will take them to a page detail showing how to set Ecosia as their default browser.
Approach
The inspiration for rewriting SwiftUI a reusable component (the configurable NTP card we developed a while ago) was that the two designs were similar in the high-level structure.
The card has been reviewed matching this new SwiftUI configurable component.
We wanted to use Lottie (which Firefox already implements) and we had to make some small adjustments in order to make the animation aligned with the expected design 🎨 .
The focus here wasn’t just shipping a new card, but building an isolated, reusable system that:
Everything from layout to styling was scoped specifically for Ecosia (see
EcosiaText
below).🧱 SwiftUI + UIKit Integration
ConfigurableNudgeCardView
is a standalone SwiftUI view used in bothUICollectionViewCell
andUITableViewHeaderFooterView
, without needing UIKit layout or dependencies.UIKit still handles screen flow and navigation — SwiftUI is used where it shines: layout, styling, and interaction.
The new
DefaultBrowserCoordinator
(potentially utilizable for the NudgeCard show/hide too 🤷 ) bridges between UIKit navigation and SwiftUI instruction views, keeping presentation logic clean.🔌 No dependency on Firefox theming:
All new components (cards, instruction steps, etc.) define their own
Style
structs (e.g.NudgeCardStyle
,InstructionStepsViewStyle
) and accept them as parameters.This avoids relying on shared theme singletons or assumptions from the broader Firefox app.
🧳 Self-contained views:
Each SwiftUI component (e.g.
EcosiaText
,InstructionStepsView
,ConfigurableNudgeCardView
) can be previewed, tested, and used independently. Most of them rely on their own "style" (e.g.NudgeCardStyle
,InstructionStepsViewStyle
).🗣️ Markdown Localization Support
EcosiaText
, a SwiftUI wrapper that reads from.strings
and parses Markdown usingAttributedString(markdown:)
.**bold**
, no extra styling logic is required.NSAttributedString
construction in code as we also rely on our ownString.swift
enum.🧪 Testing
Introduced https://github.com/nalexn/ViewInspector to better write tests for SwiftUI
🧹 Cleanup & Removal
Ecosia/UI/Settings/DefaultBrowser/
Checklist
// Ecosia:
helper comments where needed