Merge e422cfd0a8930b523a41269d9265a095f8bb5d5f into 1f59e814f90c3f489f48d68262cb1bf640bf6181
74
bitchat/Assets.xcassets/AppIconNeutral.appiconset/Contents.json
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "icon_1024x1024.png",
|
||||
"idiom": "universal",
|
||||
"platform": "ios",
|
||||
"size": "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename": "icon_16x16.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "16x16"
|
||||
},
|
||||
{
|
||||
"filename": "icon_16x16@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "16x16"
|
||||
},
|
||||
{
|
||||
"filename": "icon_32x32.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "32x32"
|
||||
},
|
||||
{
|
||||
"filename": "icon_32x32@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "32x32"
|
||||
},
|
||||
{
|
||||
"filename": "icon_128x128.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "128x128"
|
||||
},
|
||||
{
|
||||
"filename": "icon_128x128@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "128x128"
|
||||
},
|
||||
{
|
||||
"filename": "icon_256x256.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "256x256"
|
||||
},
|
||||
{
|
||||
"filename": "icon_256x256@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "256x256"
|
||||
},
|
||||
{
|
||||
"filename": "icon_512x512.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "512x512"
|
||||
},
|
||||
{
|
||||
"filename": "icon_512x512@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "512x512"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
BIN
bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_1024x1024.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_128x128.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_128x128@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_16x16.png
vendored
Normal file
|
After Width: | Height: | Size: 271 B |
BIN
bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_16x16@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 491 B |
BIN
bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_256x256.png
vendored
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_256x256@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_32x32.png
vendored
Normal file
|
After Width: | Height: | Size: 491 B |
BIN
bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_32x32@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 972 B |
BIN
bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_512x512.png
vendored
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
bitchat/Assets.xcassets/AppIconNeutral.appiconset/icon_512x512@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
74
bitchat/Assets.xcassets/AppIconNotes.appiconset/Contents.json
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "icon_1024x1024.png",
|
||||
"idiom": "universal",
|
||||
"platform": "ios",
|
||||
"size": "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename": "icon_16x16.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "16x16"
|
||||
},
|
||||
{
|
||||
"filename": "icon_16x16@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "16x16"
|
||||
},
|
||||
{
|
||||
"filename": "icon_32x32.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "32x32"
|
||||
},
|
||||
{
|
||||
"filename": "icon_32x32@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "32x32"
|
||||
},
|
||||
{
|
||||
"filename": "icon_128x128.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "128x128"
|
||||
},
|
||||
{
|
||||
"filename": "icon_128x128@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "128x128"
|
||||
},
|
||||
{
|
||||
"filename": "icon_256x256.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "256x256"
|
||||
},
|
||||
{
|
||||
"filename": "icon_256x256@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "256x256"
|
||||
},
|
||||
{
|
||||
"filename": "icon_512x512.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "512x512"
|
||||
},
|
||||
{
|
||||
"filename": "icon_512x512@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "512x512"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
BIN
bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_1024x1024.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_128x128.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_128x128@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_16x16.png
vendored
Normal file
|
After Width: | Height: | Size: 281 B |
BIN
bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_16x16@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 509 B |
BIN
bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_256x256.png
vendored
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_256x256@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_32x32.png
vendored
Normal file
|
After Width: | Height: | Size: 509 B |
BIN
bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_32x32@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1017 B |
BIN
bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_512x512.png
vendored
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
bitchat/Assets.xcassets/AppIconNotes.appiconset/icon_512x512@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
74
bitchat/Assets.xcassets/AppIconQuiet.appiconset/Contents.json
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "icon_1024x1024.png",
|
||||
"idiom": "universal",
|
||||
"platform": "ios",
|
||||
"size": "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename": "icon_16x16.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "16x16"
|
||||
},
|
||||
{
|
||||
"filename": "icon_16x16@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "16x16"
|
||||
},
|
||||
{
|
||||
"filename": "icon_32x32.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "32x32"
|
||||
},
|
||||
{
|
||||
"filename": "icon_32x32@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "32x32"
|
||||
},
|
||||
{
|
||||
"filename": "icon_128x128.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "128x128"
|
||||
},
|
||||
{
|
||||
"filename": "icon_128x128@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "128x128"
|
||||
},
|
||||
{
|
||||
"filename": "icon_256x256.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "256x256"
|
||||
},
|
||||
{
|
||||
"filename": "icon_256x256@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "256x256"
|
||||
},
|
||||
{
|
||||
"filename": "icon_512x512.png",
|
||||
"idiom": "mac",
|
||||
"scale": "1x",
|
||||
"size": "512x512"
|
||||
},
|
||||
{
|
||||
"filename": "icon_512x512@2x.png",
|
||||
"idiom": "mac",
|
||||
"scale": "2x",
|
||||
"size": "512x512"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
BIN
bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_1024x1024.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_128x128.png
vendored
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_128x128@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_16x16.png
vendored
Normal file
|
After Width: | Height: | Size: 243 B |
BIN
bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_16x16@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 443 B |
BIN
bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_256x256.png
vendored
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_256x256@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_32x32.png
vendored
Normal file
|
After Width: | Height: | Size: 443 B |
BIN
bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_32x32@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 881 B |
BIN
bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_512x512.png
vendored
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
bitchat/Assets.xcassets/AppIconQuiet.appiconset/icon_512x512@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
@ -45,6 +45,10 @@ struct BitchatApp: App {
|
||||
.onAppear {
|
||||
appDelegate.runtime = runtime
|
||||
runtime.start()
|
||||
// No-op on macOS / devices without alternate-icon support;
|
||||
// kept outside `#if os(iOS)` so the macOS Periphery scan
|
||||
// sees the launch-time apply path.
|
||||
AlternateAppIconSettings.applyStoredPreference()
|
||||
}
|
||||
.onOpenURL { url in
|
||||
runtime.handleOpenURL(url)
|
||||
|
||||
121
bitchat/Services/AlternateAppIconSettings.swift
Normal file
@ -0,0 +1,121 @@
|
||||
//
|
||||
// AlternateAppIconSettings.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#endif
|
||||
import BitLogger
|
||||
|
||||
/// Low-profile alternate home-screen icons (#1447). Neutral / notes / quiet
|
||||
/// variants live as App Icon sets in the asset catalog and are enabled via
|
||||
/// `ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS`.
|
||||
enum AlternateAppIconSettings {
|
||||
static let storageKey = "appearance.alternateAppIcon"
|
||||
|
||||
/// Catalog names. `nil` means the primary AppIcon.
|
||||
enum Icon: String, CaseIterable, Identifiable {
|
||||
case primary
|
||||
case neutral = "AppIconNeutral"
|
||||
case notes = "AppIconNotes"
|
||||
case quiet = "AppIconQuiet"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
/// Name passed to `UIApplication.setAlternateIconName`. Primary is nil.
|
||||
var systemName: String? {
|
||||
switch self {
|
||||
case .primary: return nil
|
||||
case .neutral, .notes, .quiet: return rawValue
|
||||
}
|
||||
}
|
||||
|
||||
// periphery:ignore - read from the iOS App Info icon picker; the macOS
|
||||
// Periphery scheme cannot see that use site (#1447).
|
||||
var title: String {
|
||||
switch self {
|
||||
case .primary:
|
||||
return String(
|
||||
localized: "app_info.settings.icon.primary",
|
||||
defaultValue: "bitchat",
|
||||
comment: "Label for the default bitchat home-screen icon"
|
||||
)
|
||||
case .neutral:
|
||||
return String(
|
||||
localized: "app_info.settings.icon.neutral",
|
||||
defaultValue: "neutral",
|
||||
comment: "Label for the grayscale low-profile alternate app icon"
|
||||
)
|
||||
case .notes:
|
||||
return String(
|
||||
localized: "app_info.settings.icon.notes",
|
||||
defaultValue: "notes",
|
||||
comment: "Label for the blue-gray notes-like alternate app icon"
|
||||
)
|
||||
case .quiet:
|
||||
return String(
|
||||
localized: "app_info.settings.icon.quiet",
|
||||
defaultValue: "quiet",
|
||||
comment: "Label for the dark monochrome alternate app icon"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func selected(in defaults: UserDefaults) -> Icon {
|
||||
let raw = defaults.string(forKey: storageKey) ?? Icon.primary.rawValue
|
||||
return Icon(rawValue: raw) ?? .primary
|
||||
}
|
||||
|
||||
static func setSelected(_ icon: Icon, in defaults: UserDefaults, applySystem: Bool = true) {
|
||||
if icon == .primary {
|
||||
defaults.removeObject(forKey: storageKey)
|
||||
} else {
|
||||
defaults.set(icon.rawValue, forKey: storageKey)
|
||||
}
|
||||
if applySystem {
|
||||
apply(icon)
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the stored preference to the system (iOS only). Safe no-op elsewhere.
|
||||
static func applyStoredPreference() {
|
||||
apply(selected(in: .standard))
|
||||
}
|
||||
|
||||
/// Panic wipe clears the *stored* preference only — do not call `apply`.
|
||||
/// `setAlternateIconName` pops a system alert that names "bitchat", which
|
||||
/// would un-disguise the phone at the worst moment. The home-screen glyph
|
||||
/// stays until the next intentional picker change or relaunch policy.
|
||||
static func reset(in defaults: UserDefaults = .standard) {
|
||||
defaults.removeObject(forKey: storageKey)
|
||||
}
|
||||
|
||||
private static func apply(_ icon: Icon) {
|
||||
#if os(iOS)
|
||||
// Unit tests and early launch may not have a ready UIApplication.
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async { apply(icon) }
|
||||
return
|
||||
}
|
||||
guard UIApplication.shared.supportsAlternateIcons else { return }
|
||||
let target = icon.systemName
|
||||
// Avoid a no-op system prompt when already on the desired icon.
|
||||
let current = UIApplication.shared.alternateIconName
|
||||
guard current != target else { return }
|
||||
UIApplication.shared.setAlternateIconName(target) { error in
|
||||
if let error {
|
||||
SecureLogger.warning(
|
||||
"alternate icon change failed: \(error.localizedDescription)",
|
||||
category: .session
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@ -1637,6 +1637,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
||||
MeshSightingsTracker.shared.clear()
|
||||
MeshEchoSettings.reset()
|
||||
NotificationPrivacySettings.reset()
|
||||
AlternateAppIconSettings.reset()
|
||||
// A hand-added relay names an operator someone chose to route through,
|
||||
// which is the kind of trace a wipe should not leave behind.
|
||||
NostrRelaySettings.reset()
|
||||
|
||||
@ -34,6 +34,9 @@ struct AppInfoView: View {
|
||||
/// The override changed this session; localization resolves at process
|
||||
/// start, so surface the restart hint.
|
||||
@State private var showLanguageRestartNote = false
|
||||
#if os(iOS)
|
||||
@State private var selectedAppIcon = AlternateAppIconSettings.selected(in: .standard)
|
||||
#endif
|
||||
|
||||
private enum Pane: String {
|
||||
case settings
|
||||
@ -61,6 +64,9 @@ struct AppInfoView: View {
|
||||
static let tabSettings = String(localized: "app_info.tab.settings", defaultValue: "settings", comment: "Segmented control label for the settings pane of the app info sheet")
|
||||
static let tabInfo = String(localized: "app_info.tab.info", defaultValue: "info", comment: "Segmented control label for the info pane of the app info sheet")
|
||||
|
||||
static let iconTitle = String(localized: "app_info.settings.icon.title", defaultValue: "HOME SCREEN ICON", comment: "Section header (uppercase) for alternate app icon picker") // periphery:ignore - iOS App Info settings UI
|
||||
static let iconSubtitle = String(localized: "app_info.settings.icon.subtitle", defaultValue: "optional low-profile icons. the home-screen label still reads “bitchat”, and the real name still shows in settings and search — this only changes the glyph.", comment: "Caption under the alternate app icon picker explaining the limits of disguise") // periphery:ignore - iOS App Info settings UI
|
||||
|
||||
static let connectivityTitle = String(localized: "app_info.settings.connectivity.title", defaultValue: "CONNECTIVITY", comment: "Section header (uppercase) for the connectivity toggles: mesh bridge, internet gateway, tor routing")
|
||||
|
||||
static let languageTitle = String(localized: "app_info.settings.language.title", defaultValue: "LANGUAGE", comment: "Section header (uppercase) for the app language picker in settings")
|
||||
@ -357,6 +363,51 @@ struct AppInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
// Low-profile home-screen icons for the opsec / disguise path (#1447).
|
||||
// macOS has no alternate-icon API equivalent, so this stays iOS-only.
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
SectionHeader(verbatim: Strings.Settings.iconTitle)
|
||||
|
||||
settingsCard {
|
||||
Text(verbatim: Strings.Settings.iconSubtitle)
|
||||
.bitchatFont(size: 11)
|
||||
.foregroundColor(secondaryTextColor)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
ForEach(AlternateAppIconSettings.Icon.allCases) { icon in
|
||||
Button {
|
||||
selectedAppIcon = icon
|
||||
AlternateAppIconSettings.setSelected(icon, in: .standard, applySystem: true)
|
||||
} label: {
|
||||
Text(verbatim: icon.title)
|
||||
.bitchatFont(
|
||||
size: 12,
|
||||
weight: selectedAppIcon == icon ? .semibold : .regular
|
||||
)
|
||||
.foregroundColor(
|
||||
selectedAppIcon == icon ? palette.accent : secondaryTextColor
|
||||
)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||
.fill(
|
||||
selectedAppIcon == icon
|
||||
? palette.accent.opacity(0.15)
|
||||
: Color.clear
|
||||
)
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityAddTraits(selectedAppIcon == icon ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Language — an in-app override so the UI language can differ
|
||||
// from the device language (takes effect on next launch).
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
|
||||
52
bitchatTests/Services/AlternateAppIconSettingsTests.swift
Normal file
@ -0,0 +1,52 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import bitchat
|
||||
|
||||
struct AlternateAppIconSettingsTests {
|
||||
private func makeDefaults() -> (suite: String, defaults: UserDefaults) {
|
||||
let suite = "bitchat.tests.alticon.\(UUID().uuidString)"
|
||||
return (suite, UserDefaults(suiteName: suite)!)
|
||||
}
|
||||
|
||||
private func cleanup(_ suite: String) {
|
||||
UserDefaults().removePersistentDomain(forName: suite)
|
||||
}
|
||||
|
||||
@Test func primarySystemNameIsNil() {
|
||||
#expect(AlternateAppIconSettings.Icon.primary.systemName == nil)
|
||||
}
|
||||
|
||||
@Test func alternateSystemNamesMatchCatalog() {
|
||||
#expect(AlternateAppIconSettings.Icon.neutral.systemName == "AppIconNeutral")
|
||||
#expect(AlternateAppIconSettings.Icon.notes.systemName == "AppIconNotes")
|
||||
#expect(AlternateAppIconSettings.Icon.quiet.systemName == "AppIconQuiet")
|
||||
}
|
||||
|
||||
@Test func allCasesCoverDisguiseSet() {
|
||||
let ids = Set(AlternateAppIconSettings.Icon.allCases.map(\.id))
|
||||
#expect(ids == ["primary", "AppIconNeutral", "AppIconNotes", "AppIconQuiet"])
|
||||
}
|
||||
|
||||
@Test func selectedPersistsRawValueWithoutTouchingSystem() {
|
||||
let (suite, defaults) = makeDefaults()
|
||||
defer { cleanup(suite) }
|
||||
#expect(AlternateAppIconSettings.selected(in: defaults) == .primary)
|
||||
|
||||
AlternateAppIconSettings.setSelected(.notes, in: defaults, applySystem: false)
|
||||
#expect(defaults.string(forKey: AlternateAppIconSettings.storageKey) == "AppIconNotes")
|
||||
#expect(AlternateAppIconSettings.selected(in: defaults) == .notes)
|
||||
|
||||
AlternateAppIconSettings.setSelected(.primary, in: defaults, applySystem: false)
|
||||
#expect(defaults.string(forKey: AlternateAppIconSettings.storageKey) == nil)
|
||||
#expect(AlternateAppIconSettings.selected(in: defaults) == .primary)
|
||||
}
|
||||
|
||||
@Test func resetClearsStoredPreferenceWithoutApplyingPrimary() {
|
||||
let (suite, defaults) = makeDefaults()
|
||||
defer { cleanup(suite) }
|
||||
AlternateAppIconSettings.setSelected(.quiet, in: defaults, applySystem: false)
|
||||
AlternateAppIconSettings.reset(in: defaults)
|
||||
#expect(defaults.string(forKey: AlternateAppIconSettings.storageKey) == nil)
|
||||
#expect(AlternateAppIconSettings.selected(in: defaults) == .primary)
|
||||
}
|
||||
}
|
||||