fix: privacy manifests, history/settings headers, and dock contrast

Resolve TestFlight ITMS-91056 by correcting privacy manifest keys and reason arrays. Add left-aligned page headers with circular confirm buttons, keep the keyboard surface transparent, and improve unselected dock icon contrast in light and dark mode.
This commit is contained in:
Rocky
2026-06-20 14:10:18 +08:00
parent db5151ad9a
commit 642c97c540
8 changed files with 213 additions and 112 deletions
+3 -1
View File
@@ -6,7 +6,7 @@
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedUsageTypes</key>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
@@ -14,7 +14,9 @@
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
</dict>
@@ -32,6 +32,7 @@ enum AppTab: Int, CaseIterable {
struct MinimalTabBar: View {
@Environment(\.themePalette) private var palette: ThemePalette
@Environment(\.colorScheme) private var colorScheme
@Binding var selection: AppTab
var body: some View {
@@ -44,7 +45,7 @@ struct MinimalTabBar: View {
name: tab.icon,
size: 24
)
.foregroundStyle(selection == tab ? palette.accent : palette.textTertiary)
.foregroundStyle(tabIconColor(for: tab))
.frame(maxWidth: .infinity)
.frame(height: 48)
.contentShape(Rectangle())
@@ -61,4 +62,12 @@ struct MinimalTabBar: View {
.frame(maxWidth: .infinity, alignment: .center)
.padding(.bottom, Spacing.xs)
}
private func tabIconColor(for tab: AppTab) -> Color {
if selection == tab { return palette.accent }
// dock
return colorScheme == .dark
? Color(white: 0.76)
: palette.textSecondary
}
}
@@ -0,0 +1,91 @@
// PageHeaderConfirmButton.swift
// OSGKeyboard · Main App
//
// popover action sheet
import SwiftUI
import OSGKeyboardShared
struct PageHeaderConfirmButton: View {
@Environment(\.themePalette) private var palette: ThemePalette
@Environment(\.colorScheme) private var colorScheme
let systemImage: String
let accessibilityLabel: LocalizedStringKey
let confirmTitle: LocalizedStringKey
let confirmMessage: LocalizedStringKey
let confirmActionTitle: LocalizedStringKey
let onConfirm: () -> Void
@State private var showConfirm = false
private let buttonSize: CGFloat = 36
var body: some View {
Button {
showConfirm = true
} label: {
Image(systemName: systemImage)
.font(.system(size: 16, weight: .medium))
.foregroundStyle(iconColor)
.frame(width: buttonSize, height: buttonSize)
.background(circleFill, in: Circle())
.overlay(Circle().stroke(circleStroke, lineWidth: 0.5))
}
.buttonStyle(.plain)
.accessibilityLabel(Text(accessibilityLabel))
.popover(isPresented: $showConfirm, arrowEdge: .top) {
confirmPopover
.presentationCompactAdaptation(.popover)
}
}
/// + +
private var circleFill: Color {
switch colorScheme {
case .dark:
return palette.surfaceElevated
default:
return Color.white
}
}
private var circleStroke: Color {
colorScheme == .dark ? palette.dividerStrong : palette.divider
}
private var iconColor: Color {
colorScheme == .dark ? palette.textPrimary : .black
}
private var confirmPopover: some View {
VStack(alignment: .leading, spacing: Spacing.md) {
Text(confirmTitle)
.font(TypeStyle.headline)
.foregroundStyle(palette.textPrimary)
Text(confirmMessage)
.font(TypeStyle.footnote)
.foregroundStyle(palette.textSecondary)
.fixedSize(horizontal: false, vertical: true)
HStack(spacing: Spacing.sm) {
Spacer(minLength: 0)
Button(LocalizedStringKey("common.cancel")) {
showConfirm = false
}
.font(TypeStyle.bodyEmph)
.foregroundStyle(palette.textSecondary)
Button(confirmActionTitle) {
showConfirm = false
onConfirm()
}
.font(TypeStyle.bodyEmph)
.foregroundStyle(palette.danger)
}
}
.padding(Spacing.md)
.frame(minWidth: 260)
}
}
@@ -0,0 +1,37 @@
// PageHeaderRow.swift
// OSGKeyboard · Main App
//
// + navigation toolbar
// iOS leading/trailing
import SwiftUI
import OSGKeyboardShared
struct PageHeaderRow<Trailing: View>: View {
@Environment(\.themePalette) private var palette: ThemePalette
let title: LocalizedStringKey
@ViewBuilder var trailing: () -> Trailing
var body: some View {
HStack(alignment: .center, spacing: Spacing.sm) {
Text(title)
.font(TypeStyle.title2)
.foregroundStyle(palette.textPrimary)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
trailing()
}
.padding(.horizontal, Spacing.md)
.padding(.top, Spacing.md)
.padding(.bottom, Spacing.sm)
}
}
extension PageHeaderRow where Trailing == EmptyView {
init(title: LocalizedStringKey) {
self.title = title
self.trailing = { EmptyView() }
}
}
+17 -32
View File
@@ -7,7 +7,6 @@ import OSGKeyboardShared
struct HistoryView: View {
@Environment(\.themePalette) private var palette: ThemePalette
@ObservedObject private var store = SpeechHistoryStore.shared
@State private var showClearConfirm = false
private static let dayFormatter: DateFormatter = {
let f = DateFormatter()
@@ -25,13 +24,27 @@ struct HistoryView: View {
var body: some View {
NavigationStack {
VStack(spacing: 0) {
PageHeaderRow(title: "history.title") {
if !store.entries.isEmpty {
PageHeaderConfirmButton(
systemImage: "trash",
accessibilityLabel: "history.clear.button",
confirmTitle: "history.clear.title",
confirmMessage: "history.clear.message",
confirmActionTitle: "history.clear.confirm"
) {
store.clearAll()
}
}
}
ZStack {
palette.background.ignoresSafeArea()
if store.entries.isEmpty {
emptyState
} else {
VStack(spacing: 0) {
ScrollView {
LazyVStack(alignment: .leading, spacing: Spacing.xl) {
ForEach(store.groupedByDay, id: \.day) { group in
@@ -42,25 +55,11 @@ struct HistoryView: View {
.padding(.vertical, Spacing.md)
.padding(.bottom, 100)
}
clearFooter
}
}
}
.navigationTitle(LocalizedStringKey("history.title"))
.navigationBarTitleDisplayMode(.inline)
}
.confirmationDialog(
LocalizedStringKey("history.clear.title"),
isPresented: $showClearConfirm,
titleVisibility: .visible
) {
Button(LocalizedStringKey("history.clear.confirm"), role: .destructive) {
store.clearAll()
}
Button(LocalizedStringKey("common.cancel"), role: .cancel) {}
} message: {
Text("history.clear.message")
.background(palette.background)
.toolbar(.hidden, for: .navigationBar)
}
}
@@ -116,18 +115,4 @@ struct HistoryView: View {
.padding(Spacing.md)
.frame(maxWidth: .infinity, alignment: .leading)
}
private var clearFooter: some View {
Button(role: .destructive) {
showClearConfirm = true
} label: {
Text("history.clear.button")
.font(TypeStyle.caption)
.foregroundStyle(palette.danger)
.frame(maxWidth: .infinity, minHeight: 44)
}
.buttonStyle(.plain)
.padding(.horizontal, Spacing.md)
.padding(.bottom, Spacing.sm)
}
}
+24 -40
View File
@@ -18,7 +18,6 @@ struct SettingsView: View {
@ObservedObject var config = ProviderConfig.shared
@Environment(\.dismiss) private var dismiss
@State private var showResetConfirm = false
@State private var safariURL: URL?
let presentation: SettingsPresentation
@@ -32,6 +31,27 @@ struct SettingsView: View {
var body: some View {
NavigationStack {
VStack(spacing: 0) {
PageHeaderRow(title: "settings.title") {
HStack(spacing: Spacing.xs) {
PageHeaderConfirmButton(
systemImage: "arrow.counterclockwise",
accessibilityLabel: "settings.reset.confirm",
confirmTitle: "settings.reset.title",
confirmMessage: "settings.reset.message",
confirmActionTitle: "common.reset"
) {
config.reset()
}
if presentation == .sheet {
Button("common.done") { dismiss() }
.font(TypeStyle.headline)
.foregroundStyle(palette.accent)
.frame(minHeight: 44)
}
}
}
ZStack {
palette.background.ignoresSafeArea()
ScrollView {
@@ -48,41 +68,20 @@ struct SettingsView: View {
if presentation == .tab {
footerLinks
}
resetButton
}
.padding(.horizontal, Spacing.md)
.padding(.vertical, Spacing.md)
.padding(.bottom, presentation == .tab ? 100 : Spacing.lg)
}
}
.navigationTitle(LocalizedStringKey("settings.title"))
.navigationBarTitleDisplayMode(.inline)
}
.background(palette.background)
.toolbar(.hidden, for: .navigationBar)
.task { await loadDynamicLocales() }
.toolbar {
if presentation == .sheet {
ToolbarItem(placement: .confirmationAction) {
Button("common.done") { dismiss() }
.font(TypeStyle.headline)
.foregroundStyle(palette.accent)
}
}
}
.sheet(item: $safariURL) { url in
SafariSheet(url: url)
}
}
.confirmationDialog(
LocalizedStringKey("settings.reset.title"),
isPresented: $showResetConfirm,
titleVisibility: .visible
) {
Button(LocalizedStringKey("common.reset"), role: .destructive) {
config.reset()
}
Button(LocalizedStringKey("common.cancel"), role: .cancel) {}
} message: {
Text("settings.reset.message")
}
}
// MARK: - Engine
@@ -313,21 +312,6 @@ struct SettingsView: View {
.buttonStyle(.plain)
}
// MARK: - Reset
private var resetButton: some View {
Button(role: .destructive) {
showResetConfirm = true
} label: {
Text("settings.reset.confirm")
.font(TypeStyle.caption)
.foregroundStyle(palette.danger)
.frame(maxWidth: .infinity, minHeight: 36)
}
.buttonStyle(.plain)
.padding(.top, presentation == .tab ? Spacing.xs : Spacing.sm)
}
// MARK: - Header
private func sectionHeader(_ title: LocalizedStringKey) -> some View {
+3 -7
View File
@@ -4,7 +4,7 @@
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyCollectedUsageTypes</key>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
@@ -12,13 +12,9 @@
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryActiveKeyboards</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<string>DDA9.1</string>
</array>
</dict>
</array>
</dict>
+2 -5
View File
@@ -57,16 +57,13 @@ public struct KeyboardRootView: View {
}
.padding(.top, 4)
.padding(.bottom, 6)
.background(keyboardBackground)
// chrome
.background(Color.clear)
.frame(height: Self.totalHeight)
// Feed the resolved palette to all nested chips/buttons.
.environment(\.themePalette, palette)
}
private var keyboardBackground: Color {
colorScheme == .dark ? palette.background : Color.clear
}
// MARK: - Top bar
private var topBar: some View {