mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-11 04:34:55 -06:00
[PM-26064] Consolidate various components used in Settings screens (#2025)
This commit is contained in:
parent
b1cc3c6795
commit
9a9c578125
@ -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) {}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
@ -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: {
|
||||
|
||||
@ -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
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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:
|
||||
@ -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,
|
||||
@ -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,
|
||||
@ -3,8 +3,6 @@ import SwiftUI
|
||||
import ViewInspector
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
class SettingsMenuFieldTests: BitwardenTestCase {
|
||||
// MARK: Types
|
||||
|
||||
@ -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>,
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenResources
|
||||
import SnapshotTesting
|
||||
import ViewInspector
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenResources
|
||||
import SnapshotTesting
|
||||
import XCTest
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenResources
|
||||
import SwiftUI
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenResources
|
||||
import SnapshotTesting
|
||||
import XCTest
|
||||
|
||||
@ -54,7 +54,7 @@ struct SettingsMenuFieldType: BaseViewType {
|
||||
static var typePrefix: String = "SettingsMenuField"
|
||||
|
||||
static var namespacedPrefixes: [String] = [
|
||||
"AuthenticatorShared.SettingsMenuField",
|
||||
"BitwardenKit.SettingsMenuField",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user