[PM-26064] Consolidate various components used in Settings screens (#2025)

This commit is contained in:
Katherine Bertelsen 2025-10-09 15:58:33 -05:00 committed by GitHub
parent b1cc3c6795
commit 9a9c578125
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 92 additions and 622 deletions

View File

@ -1,87 +0,0 @@
import BitwardenKit
import BitwardenResources
import SwiftUI
/// A view that displays a button for use as an accessory to a field.
///
struct AccessoryButton: View {
// MARK: Types
/// A type that wraps a synchrounous or asynchrounous block that is executed by this button.
///
enum Action {
/// An action run synchrounously.
case sync(() -> Void)
/// An action run asynchrounously.
case async(() async -> Void)
}
// MARK: Properties
/// The accessibility label of the button.
var accessibilityLabel: String
/// The action to perform when the user interacts with this button.
var action: Action
/// The image to display in the button.
var asset: SharedImageAsset
var body: some View {
switch action {
case let .async(action):
AsyncButton(action: action) {
asset.swiftUIImage
.resizable()
.frame(width: 14, height: 14)
}
.buttonStyle(.accessory)
.accessibilityLabel(Text(accessibilityLabel))
case let .sync(action):
Button(action: action) {
asset.swiftUIImage
.resizable()
.frame(width: 14, height: 14)
}
.buttonStyle(.accessory)
.accessibilityLabel(Text(accessibilityLabel))
}
}
// MARK: Initialization
/// Initializes a `AccessoryButton` which styles a button for display as an accessory to a
/// field.
///
/// - Parameters:
/// - asset: The image to display in the button.
/// - accessibilityLabel: The accessibility label of the button.
/// - action: The action to perform when the user triggers the button.
///
init(asset: SharedImageAsset, accessibilityLabel: String, action: @escaping () -> Void) {
self.accessibilityLabel = accessibilityLabel
self.action = .sync(action)
self.asset = asset
}
/// Initializes a `AccessoryButton` which styles a button for display as an accessory to a
/// field.
///
/// - Parameters:
/// - asset: The image to display in the button.
/// - accessibilityLabel: The accessibility label of the button.
/// - action: The action to perform when the user triggers the button.
///
init(asset: SharedImageAsset, accessibilityLabel: String, action: @escaping () async -> Void) {
self.accessibilityLabel = accessibilityLabel
self.action = .async(action)
self.asset = asset
}
}
// MARK: Previews
#Preview {
AccessoryButton(asset: SharedAsset.Icons.copy16, accessibilityLabel: Localizations.copy) {}
}

View File

@ -1,126 +0,0 @@
import SwiftUI
// MARK: - BitwardenField
/// A standardized view used to wrap some content into a row of a list. This is commonly used in
/// forms.
struct BitwardenField<Content, AccessoryContent>: View where Content: View, AccessoryContent: View {
/// The (optional) title of the field.
var title: String?
/// The (optional) accessibility identifier to apply to the title of the field (if it exists)
var titleAccessibilityIdentifier: String?
/// The (optional) footer to display underneath the field.
var footer: String?
/// The (optional) accessibility identifier to apply to the fooder of the field (if it exists)
var footerAccessibilityIdentifier: String?
/// The vertical padding to apply around `content`. Defaults to `8`.
var verticalPadding: CGFloat
/// The content that should be displayed in the field.
var content: Content
/// Any accessory content that should be displayed on the trailing edge of the field. This
/// content automatically has the `AccessoryButtonStyle` applied to it.
var accessoryContent: AccessoryContent?
var body: some View {
VStack(alignment: .leading, spacing: 8) {
if let title {
Text(title)
.styleGuide(.subheadline, weight: .semibold)
.foregroundColor(Asset.Colors.textSecondary.swiftUIColor)
.accessibilityIdentifier(titleAccessibilityIdentifier ?? title)
}
HStack(spacing: 8) {
content
.frame(maxWidth: .infinity, minHeight: 28, alignment: .leading)
.padding(.horizontal, 16)
.padding(.vertical, verticalPadding)
.background(Asset.Colors.backgroundPrimary.swiftUIColor)
.clipShape(RoundedRectangle(cornerRadius: 10))
if let accessoryContent {
accessoryContent
.buttonStyle(.accessory)
}
}
if let footer {
Text(footer)
.styleGuide(.footnote)
.foregroundColor(Asset.Colors.textSecondary.swiftUIColor)
.accessibilityIdentifier(footerAccessibilityIdentifier ?? footer)
}
}
}
// MARK: Initialization
/// Creates a new `BitwardenField`.
///
/// - Parameters:
/// - title: The (optional) title of the field.
/// - titleAccessibilityIdentifier: The (optional) accessibility identifier to apply
/// to the title of the field (if it exists)
/// - footer: The (optional) footer to display underneath the field.
/// - footerAccessibilityIdentifier: The (optional) accessibility identifier to apply
/// to the fooder of the field (if it exists)
/// - verticalPadding: The vertical padding to apply around `content`. Defaults to `8`.
/// - content: The content that should be displayed in the field.
/// - accessoryContent: Any accessory content that should be displayed on the trailing edge of
/// the field. This content automatically has the `AccessoryButtonStyle` applied to it.
///
init(
title: String? = nil,
titleAccessibilityIdentifier: String? = nil,
footer: String? = nil,
footerAccessibilityIdentifier: String? = nil,
verticalPadding: CGFloat = 8,
@ViewBuilder content: () -> Content,
@ViewBuilder accessoryContent: () -> AccessoryContent,
) {
self.title = title
self.titleAccessibilityIdentifier = titleAccessibilityIdentifier
self.footer = footer
self.footerAccessibilityIdentifier = footerAccessibilityIdentifier
self.verticalPadding = verticalPadding
self.content = content()
self.accessoryContent = accessoryContent()
}
}
extension BitwardenField where AccessoryContent == EmptyView {
/// Creates a new `BitwardenField` without accessory content.
///
/// - Parameters:
/// - title: The (optional) title of the field.
/// - titleAccessibilityIdentifier: The (optional) accessibility identifier to apply
/// to the title of the field (if it exists)
/// - footer: The (optional) footer to display underneath the field.
/// - footerAccessibilityIdentifier: The (optional) accessibility identifier to apply
/// to the fooder of the field (if it exists)
/// - verticalPadding: The vertical padding to apply around `content`. Defaults to `8`.
/// - content: The content that should be displayed in the field.
///
init(
title: String? = nil,
titleAccessibilityIdentifier: String? = nil,
footer: String? = nil,
footerAccessibilityIdentifier: String? = nil,
verticalPadding: CGFloat = 8,
@ViewBuilder content: () -> Content,
) {
self.title = title
self.titleAccessibilityIdentifier = titleAccessibilityIdentifier
self.footer = footer
self.footerAccessibilityIdentifier = footerAccessibilityIdentifier
self.verticalPadding = verticalPadding
self.content = content()
accessoryContent = nil
}
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
import SwiftUI
/// A standardized view used to display some text into a row of a list. This is commonly used in

View File

@ -1,134 +0,0 @@
import BitwardenKit
import SwiftUI
// MARK: - SettingsMenuField
/// A standard input field that allows the user to select between a predefined set of
/// options.
///
struct SettingsMenuField<T>: View where T: Menuable {
// MARK: Properties
/// The accessibility ID for the menu field.
let accessibilityIdentifier: String?
/// Whether the menu field should have a bottom divider.
let hasDivider: Bool
/// The selection chosen from the menu.
@Binding var selection: T
/// The accessibility ID for the picker selection.
let selectionAccessibilityID: String?
/// The options displayed in the menu.
let options: [T]
/// The title of the menu field.
let title: String
// MARK: View
var body: some View {
VStack(spacing: 0) {
Menu {
Picker(selection: $selection) {
ForEach(options, id: \.hashValue) { option in
Text(option.localizedName).tag(option)
}
} label: {
Text("")
}
} label: {
HStack {
Text(title)
.multilineTextAlignment(.leading)
.foregroundColor(Asset.Colors.textPrimary.swiftUIColor)
.padding(.vertical, 19)
.fixedSize(horizontal: false, vertical: true)
Spacer()
Text(selection.localizedName)
.accessibilityIdentifier(selectionAccessibilityID ?? "")
.multilineTextAlignment(.trailing)
.foregroundColor(Asset.Colors.textSecondary.swiftUIColor)
.accessibilityIdentifier(selectionAccessibilityID ?? "")
}
}
.styleGuide(.body)
.accessibilityIdentifier(accessibilityIdentifier ?? "")
.id(title)
.padding(.horizontal, 16)
if hasDivider {
Divider()
.padding(.leading, 16)
}
}
.background(Asset.Colors.backgroundPrimary.swiftUIColor)
}
/// Initializes a new `SettingsMenuField`.
///
/// - Parameters:
/// - title: The title of the menu field.
/// - options: The options that the user can choose between.
/// - hasDivider: Whether the menu field should have a bottom divider.
/// - accessibilityIdentifier: The accessibility ID for the menu field.
/// - selectionAccessibilityID: The accessibility ID for the picker selection.
/// - selection: A `Binding` for the currently selected option.
///
init(
title: String,
options: [T],
hasDivider: Bool = true,
accessibilityIdentifier: String? = nil,
selectionAccessibilityID: String? = nil,
selection: Binding<T>,
) {
self.accessibilityIdentifier = accessibilityIdentifier
self.hasDivider = hasDivider
self.options = options
_selection = selection
self.selectionAccessibilityID = selectionAccessibilityID
self.title = title
}
}
// MARK: Previews
#if DEBUG
private enum MenuPreviewOptions: CaseIterable, Menuable {
case bear, bird, dog
var localizedName: String {
switch self {
case .bear: "🧸"
case .bird: "🪿"
case .dog: "🐕"
}
}
}
#Preview {
Group {
VStack(spacing: 0) {
SettingsMenuField(
title: "Bear",
options: MenuPreviewOptions.allCases,
selection: .constant(.bear),
)
SettingsMenuField(
title: "Dog",
options: MenuPreviewOptions.allCases,
hasDivider: false,
selection: .constant(.dog),
)
}
.padding(8)
}
.background(Color(.systemGroupedBackground))
}
#endif

View File

@ -1,61 +0,0 @@
import BitwardenKit
import SwiftUI
import ViewInspector
import XCTest
@testable import AuthenticatorShared
class SettingsMenuFieldTests: BitwardenTestCase {
// MARK: Types
enum TestValue: String, CaseIterable, Menuable {
case value1
case value2
var localizedName: String {
rawValue
}
}
// MARK: Properties
var selection: TestValue!
var subject: SettingsMenuField<TestValue>!
// MARK: Setup & Teardown
override func setUp() {
super.setUp()
selection = .value1
let binding = Binding {
self.selection!
} set: { newValue in
self.selection = newValue
}
subject = SettingsMenuField(
title: "Title",
options: TestValue.allCases,
selection: binding,
)
}
override func tearDown() {
super.tearDown()
selection = nil
subject = nil
}
// MARK: Tests
func test_newSelection() throws {
let picker = try subject.inspect().find(ViewType.Picker.self)
try picker.select(value: TestValue.value2)
XCTAssertEqual(selection, .value2)
let menu = try subject.inspect().find(ViewType.Menu.self)
let title = try menu.labelView().find(ViewType.Text.self).string()
let pickerValue = try menu.find(ViewType.HStack.self).find(text: "value2").string()
XCTAssertEqual(title, "Title")
XCTAssertEqual(pickerValue, "value2")
}
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import SwiftUI
@ -14,13 +15,11 @@ struct SelectLanguageView: View {
// MARK: View
var body: some View {
VStack(spacing: 0) {
ContentBlock(dividerLeadingPadding: 16) {
ForEach(LanguageOption.allCases) { languageOption in
languageOptionRow(languageOption)
}
}
.background(Asset.Colors.backgroundPrimary.swiftUIColor)
.cornerRadius(10)
.scrollView()
.navigationBar(title: Localizations.selectLanguage, titleDisplayMode: .inline)
.toolbar {
@ -45,7 +44,6 @@ struct SelectLanguageView: View {
private func languageOptionRow(_ languageOption: LanguageOption) -> some View {
SettingsListItem(
languageOption.title,
hasDivider: !languageOption.isLast,
) {
store.send(.languageTapped(languageOption))
} trailingContent: {

View File

@ -1,112 +0,0 @@
import BitwardenResources
import SwiftUI
// MARK: - SettingsListItem
/// A list item that appears across settings screens.
///
struct SettingsListItem<Content: View>: View {
// MARK: Properties
/// The accessibility ID for the list item.
let accessibilityIdentifier: String?
/// The action to perform when the list item is tapped.
let action: () -> Void
/// Whether or not the list item should have a divider on the bottom.
let hasDivider: Bool
/// The name of the list item.
let name: String
/// The accessibility ID for the list item name.
let nameAccessibilityID: String?
/// Content that appears on the trailing edge of the list item.
let trailingContent: () -> Content?
// MARK: View
var body: some View {
Button {
action()
} label: {
VStack(spacing: 0) {
HStack {
Text(name)
.styleGuide(.body)
.accessibilityIdentifier(nameAccessibilityID ?? "")
.foregroundColor(Asset.Colors.textPrimary.swiftUIColor)
.multilineTextAlignment(.leading)
.padding(.vertical, 19)
Spacer()
trailingContent()
.styleGuide(.body)
.foregroundColor(Asset.Colors.textSecondary.swiftUIColor)
.multilineTextAlignment(.trailing)
}
.padding(.horizontal, 16)
if hasDivider {
Divider()
.padding(.leading, 16)
}
}
}
.accessibilityIdentifier(accessibilityIdentifier ?? "")
.background(Asset.Colors.backgroundPrimary.swiftUIColor)
}
// MARK: Initialization
/// Initializes a new `SettingsListItem`.
///
/// - Parameters:
/// - name: The name of the list item.
/// - hasDivider: Whether or not the list item should have a divider on the bottom.
/// - accessibilityIdentifier: The accessibility ID for the list item.
/// - nameAccessibilityID: The accessibility ID for the list item name.
/// - action: The action to perform when the list item is tapped.
/// - trailingContent: Content that appears on the trailing edge of the list item.
///
/// - Returns: The list item.
///
init(
_ name: String,
hasDivider: Bool = true,
accessibilityIdentifier: String? = nil,
nameAccessibilityID: String? = nil,
action: @escaping () -> Void,
@ViewBuilder trailingContent: @escaping () -> Content? = { EmptyView() },
) {
self.accessibilityIdentifier = accessibilityIdentifier
self.name = name
self.hasDivider = hasDivider
self.nameAccessibilityID = nameAccessibilityID
self.trailingContent = trailingContent
self.action = action
}
}
// MARK: Previews
#if DEBUG
#Preview {
ScrollView {
VStack(spacing: 0) {
SettingsListItem("Account Security") {} trailingContent: {
Text("Trailing content")
}
SettingsListItem("Account Security") {} trailingContent: {
Image(asset: SharedAsset.Icons.externalLink16)
}
SettingsListItem("Account Security") {}
}
}
}
#endif

View File

@ -93,20 +93,21 @@ struct SettingsView: View {
/// The language picker view
private var language: some View {
VStack(alignment: .leading, spacing: 8) {
SettingsListItem(
Localizations.language,
hasDivider: false,
Button {
store.send(.languageTapped)
} label: {
BitwardenField(
title: Localizations.language,
footer: Localizations.languageChangeRequiresAppRestart,
) {
store.send(.languageTapped)
} trailingContent: {
Text(store.state.currentLanguage.title)
.styleGuide(.body)
.foregroundColor(Color(asset: SharedAsset.Colors.textPrimary))
.multilineTextAlignment(.leading)
} accessoryContent: {
SharedAsset.Icons.chevronDown24.swiftUIImage
.imageStyle(.rowIcon)
}
.cornerRadius(10)
Text(Localizations.languageChangeRequiresAppRestart)
.styleGuide(.subheadline)
.foregroundColor(Color(asset: Asset.Colors.textSecondary))
}
}
@ -115,27 +116,24 @@ struct SettingsView: View {
VStack(spacing: 0) {
biometricsSetting
SectionView(Localizations.data, contentSpacing: 0) {
VStack(spacing: 0) {
SettingsListItem(Localizations.import) {
store.send(.importItemsTapped)
}
SettingsListItem(Localizations.export) {
store.send(.exportItemsTapped)
}
SettingsListItem(Localizations.backup) {
store.send(.backupTapped)
}
syncWithPasswordManagerRow(hasDivider: store.state.shouldShowDefaultSaveOption)
if store.state.shouldShowDefaultSaveOption {
defaultSaveOption
}
ContentBlock(dividerLeadingPadding: 16) {
SettingsListItem(Localizations.import) {
store.send(.importItemsTapped)
}
SettingsListItem(Localizations.export) {
store.send(.exportItemsTapped)
}
SettingsListItem(Localizations.backup) {
store.send(.backupTapped)
}
syncWithPasswordManagerRow(hasDivider: store.state.shouldShowDefaultSaveOption)
if store.state.shouldShowDefaultSaveOption {
defaultSaveOption
}
.cornerRadius(10)
}
.padding(.bottom, 32)
@ -145,30 +143,24 @@ struct SettingsView: View {
}
.padding(.bottom, 32)
SectionView(Localizations.help, contentSpacing: 0) {
VStack(spacing: 0) {
SettingsListItem(Localizations.launchTutorial) {
store.send(.tutorialTapped)
}
externalLinkRow(Localizations.bitwardenHelpCenter, action: .helpCenterTapped, hasDivider: false)
ContentBlock(dividerLeadingPadding: 16) {
SettingsListItem(Localizations.launchTutorial) {
store.send(.tutorialTapped)
}
.cornerRadius(10)
externalLinkRow(Localizations.bitwardenHelpCenter, action: .helpCenterTapped)
}
.padding(.bottom, 32)
SectionView(Localizations.about, contentSpacing: 0) {
VStack(spacing: 0) {
externalLinkRow(Localizations.privacyPolicy, action: .privacyPolicyTapped)
ContentBlock(dividerLeadingPadding: 16) {
externalLinkRow(Localizations.privacyPolicy, action: .privacyPolicyTapped)
SettingsListItem(store.state.version, hasDivider: false) {
store.send(.versionTapped)
} trailingContent: {
SharedAsset.Icons.copy16.swiftUIImage
.imageStyle(.rowIcon)
}
SettingsListItem(store.state.version) {
store.send(.versionTapped)
} trailingContent: {
SharedAsset.Icons.copy24.swiftUIImage
.imageStyle(.rowIcon)
}
.cornerRadius(10)
}
.padding(.bottom, 16)
@ -193,23 +185,16 @@ struct SettingsView: View {
/// The application's color theme picker view
private var theme: some View {
VStack(alignment: .leading, spacing: 8) {
SettingsMenuField(
title: Localizations.theme,
options: AppTheme.allCases,
hasDivider: false,
selection: store.binding(
get: \.appTheme,
send: SettingsAction.appThemeChanged,
),
)
.cornerRadius(10)
.accessibilityIdentifier("ThemeChooser")
Text(Localizations.themeDescription)
.styleGuide(.subheadline)
.foregroundColor(Color(asset: Asset.Colors.textSecondary))
}
BitwardenMenuField(
title: Localizations.theme,
footer: Localizations.themeDescription,
accessibilityIdentifier: "ThemeChooser",
options: AppTheme.allCases,
selection: store.binding(
get: \.appTheme,
send: SettingsAction.appThemeChanged,
),
)
}
/// A toggle for the user's biometric unlock preference.
@ -245,15 +230,11 @@ struct SettingsView: View {
/// - action: An action to send when the row is tapped.
/// - Returns: A `SettingsListItem` configured for an external web link.
///
private func externalLinkRow(
_ name: String,
action: SettingsAction,
hasDivider: Bool = true,
) -> some View {
SettingsListItem(name, hasDivider: hasDivider) {
private func externalLinkRow(_ name: String, action: SettingsAction) -> some View {
SettingsListItem(name) {
store.send(action)
} trailingContent: {
SharedAsset.Icons.externalLink16.swiftUIImage
SharedAsset.Icons.externalLink24.swiftUIImage
.imageStyle(.rowIcon)
}
}

View File

@ -40,7 +40,7 @@ class SettingsViewTests: BitwardenTestCase {
@MainActor
func test_appThemeChanged_updateValue() throws {
processor.state.appTheme = .light
let menuField = try subject.inspect().find(settingsMenuField: Localizations.theme)
let menuField = try subject.inspect().find(bitwardenMenuField: Localizations.theme)
try menuField.select(newValue: AppTheme.dark)
XCTAssertEqual(processor.dispatchedActions.last, .appThemeChanged(.dark))
}

View File

@ -5,7 +5,7 @@ import SwiftUI
/// A view that displays some text surrounded by a circular background, similar to an iOS icon badge.
///
struct BitwardenBadge: View {
public struct BitwardenBadge: View {
// MARK: Properties
/// Padding applied between the text and the circular background. Scales with dynamic type)
@ -30,7 +30,7 @@ struct BitwardenBadge: View {
// MARK: View
var body: some View {
public var body: some View {
Text(badgeValue)
.styleGuide(.callout, weight: .bold, includeLinePadding: false, includeLineSpacing: false)
.foregroundStyle(SharedAsset.Colors.iconBadgeForeground.swiftUIColor)
@ -44,6 +44,16 @@ struct BitwardenBadge: View {
.background(SharedAsset.Colors.iconBadgeBackground.swiftUIColor)
.clipShape(Circle())
}
// MARK: Initializers
/// Public version of synthesized initializer.
///
/// - Parameters:
/// - badgeValue: The value to display in the badge.
public init(badgeValue: String) {
self.badgeValue = badgeValue
}
}
// MARK: Previews

View File

@ -5,7 +5,7 @@ import SwiftUI
/// A standardized view used to wrap some content into a row of a list. This is commonly used in
/// forms.
struct BitwardenField<Content: View, AccessoryContent: View, FooterContent: View>: View {
public struct BitwardenField<Content: View, AccessoryContent: View, FooterContent: View>: View {
// MARK: Properties
/// The (optional) title of the field.
@ -29,7 +29,7 @@ struct BitwardenField<Content: View, AccessoryContent: View, FooterContent: View
// MARK: View
var body: some View {
public var body: some View {
VStack(alignment: .leading, spacing: 0) {
contentView()
@ -57,7 +57,7 @@ struct BitwardenField<Content: View, AccessoryContent: View, FooterContent: View
/// the field. This content automatically has the `AccessoryButtonStyle` applied to it.
/// - footerContent: The (optional) footer content to display underneath the field.
///
init(
public init(
title: String? = nil,
titleAccessibilityIdentifier: String? = nil,
@ViewBuilder content: () -> Content,
@ -139,7 +139,7 @@ struct BitwardenField<Content: View, AccessoryContent: View, FooterContent: View
}
}
extension BitwardenField where AccessoryContent == EmptyView {
public extension BitwardenField where AccessoryContent == EmptyView {
/// Creates a new `BitwardenField`.
///
/// - Parameters:
@ -163,7 +163,7 @@ extension BitwardenField where AccessoryContent == EmptyView {
}
}
extension BitwardenField where FooterContent == EmptyView {
public extension BitwardenField where FooterContent == EmptyView {
/// Creates a new `BitwardenField`.
///
/// - Parameters:
@ -187,7 +187,7 @@ extension BitwardenField where FooterContent == EmptyView {
}
}
extension BitwardenField where AccessoryContent == EmptyView, FooterContent == EmptyView {
public extension BitwardenField where AccessoryContent == EmptyView, FooterContent == EmptyView {
/// Creates a new `BitwardenField` without accessory content.
///
/// - Parameters:
@ -209,7 +209,7 @@ extension BitwardenField where AccessoryContent == EmptyView, FooterContent == E
}
}
extension BitwardenField where FooterContent == Text {
public extension BitwardenField where FooterContent == Text {
/// Creates a new `BitwardenField`.
///
/// - Parameters:
@ -236,7 +236,7 @@ extension BitwardenField where FooterContent == Text {
}
}
extension BitwardenField where AccessoryContent == EmptyView, FooterContent == Text {
public extension BitwardenField where AccessoryContent == EmptyView, FooterContent == Text {
/// Creates a new `BitwardenField`.
///
/// - Parameters:

View File

@ -1,4 +1,3 @@
import BitwardenKit
import BitwardenResources
import SwiftUI
@ -6,7 +5,7 @@ import SwiftUI
/// A list item that appears across settings screens.
///
struct SettingsListItem<Content: View>: View {
public struct SettingsListItem<Content: View>: View {
// MARK: Properties
/// The accessibility ID for the list item.
@ -32,7 +31,7 @@ struct SettingsListItem<Content: View>: View {
// MARK: View
var body: some View {
public var body: some View {
Button {
action()
} label: {
@ -83,7 +82,7 @@ struct SettingsListItem<Content: View>: View {
///
/// - Returns: The list item.
///
init(
public init(
_ name: String,
accessibilityIdentifier: String? = nil,
badgeValue: String? = nil,

View File

@ -1,4 +1,3 @@
import BitwardenKit
import BitwardenResources
import SwiftUI
@ -7,7 +6,7 @@ import SwiftUI
/// A standard input field that allows the user to select between a predefined set of
/// options.
///
struct SettingsMenuField<T>: View where T: Menuable {
public struct SettingsMenuField<T>: View where T: Menuable {
// MARK: Properties
/// The accessibility ID for the menu field.
@ -33,7 +32,7 @@ struct SettingsMenuField<T>: View where T: Menuable {
// MARK: View
var body: some View {
public var body: some View {
VStack(spacing: 0) {
Menu {
Picker(selection: $selection) {
@ -92,7 +91,7 @@ struct SettingsMenuField<T>: View where T: Menuable {
/// - selectionAccessibilityID: The accessibility ID for the picker selection.
/// - selection: A `Binding` for the currently selected option.
///
init(
public init(
title: String,
options: [T],
hasDivider: Bool = true,

View File

@ -3,8 +3,6 @@ import SwiftUI
import ViewInspector
import XCTest
@testable import BitwardenShared
class SettingsMenuFieldTests: BitwardenTestCase {
// MARK: Types

View File

@ -5,7 +5,7 @@ import SwiftUI
/// A field that displays a `CountdownDatePicker` when interacted with.
///
struct SettingsPickerField: View {
public struct SettingsPickerField: View {
// MARK: Properties
/// The accessibility label used for the custom timeout value.
@ -28,7 +28,7 @@ struct SettingsPickerField: View {
// MARK: View
var body: some View {
public var body: some View {
VStack(spacing: 0) {
Button {
withAnimation {
@ -83,7 +83,7 @@ struct SettingsPickerField: View {
/// - hasDivider: Whether or not the field has a bottom edge divider.
/// - customTimeoutAccessibilityLabel: The accessibility label used for the custom timeout value.
///
init(
public init(
title: String,
customTimeoutValue: String,
pickerValue: Binding<Int>,

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import SnapshotTesting
import ViewInspector

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import SnapshotTesting
import XCTest

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import SwiftUI

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import SnapshotTesting
import XCTest

View File

@ -54,7 +54,7 @@ struct SettingsMenuFieldType: BaseViewType {
static var typePrefix: String = "SettingsMenuField"
static var namespacedPrefixes: [String] = [
"AuthenticatorShared.SettingsMenuField",
"BitwardenKit.SettingsMenuField",
]
}