feat(apple): type-safe SF Symbols via SFSafeSymbols

Replaces every stringly-typed SF Symbol name with a compile-time-checked
SFSymbol case, mirroring the L10n accessor approach. A mistyped or
OS-unavailable symbol is now a build error instead of a silently blank
glyph at runtime.

Adds the SFSafeSymbols SPM package (project.yml) and migrates all call
sites: Image(systemName:)/Label(systemImage:) -> systemSymbol, and the
five symbol-carrying view properties (AppDestination.systemSymbol,
SettingsRow.icon, AboutPoint.symbol, MethodRow.icon, PolicyOption.icon)
flipped from String to SFSymbol end to end.
This commit is contained in:
2026-07-23 17:40:07 +02:00
parent ef42875ddb
commit 1563bf80d6
12 changed files with 74 additions and 59 deletions

View File

@@ -1,3 +1,4 @@
import SFSafeSymbols
import SwiftUI
enum ReceiveMethodAvailability { case available, unavailable, hidden }
@@ -27,19 +28,19 @@ struct ReceiveMethodPanel: View {
Text(String(localized: L10n.Receive.chooseMethodBody)).foregroundStyle(colors.foregroundLighter)
MethodRow(
icon: "doc", titleKey: L10n.Receive.methodFile, descKey: L10n.Receive.methodFileDescription,
icon: .doc, titleKey: L10n.Receive.methodFile, descKey: L10n.Receive.methodFileDescription,
availability: actions.fileAvailability
) { actions.pickInvitation { model.onInvitationResult(.invitationFile, $0) } }
if actions.qrAvailability != .hidden {
MethodRow(
icon: "qrcode.viewfinder", titleKey: L10n.Receive.methodScan, descKey: L10n.Receive.methodScanDescription,
icon: .qrcodeViewfinder, titleKey: L10n.Receive.methodScan, descKey: L10n.Receive.methodScanDescription,
availability: actions.qrAvailability
) { actions.scanQrCode { model.onInvitationResult(.qrCode, $0) } }
}
if actions.nfcAvailability != .hidden {
MethodRow(
icon: "wave.3.right",
icon: .wave3Right,
titleOverride: model.state.isWaitingForNfc ? String(localized: L10n.Receive.nfcWaiting) : nil,
titleKey: L10n.Receive.methodNfc, descKey: L10n.Receive.methodNfcDescription,
availability: model.state.isWaitingForNfc ? .unavailable : actions.nfcAvailability
@@ -56,7 +57,7 @@ struct ReceiveMethodPanel: View {
private struct MethodRow: View {
@Environment(\.vniColors) private var colors
let icon: String
let icon: SFSymbol
var titleOverride: String? = nil
let titleKey: String.LocalizationValue
let descKey: String.LocalizationValue
@@ -67,7 +68,7 @@ private struct MethodRow: View {
let enabled = availability == .available
Button(action: onTap) {
HStack(spacing: 14) {
Image(systemName: icon).font(.system(size: 22))
Image(systemSymbol: icon).font(.system(size: 22))
.foregroundStyle(enabled ? colors.brandLink : colors.foregroundLighter)
.frame(width: 24)
VStack(alignment: .leading, spacing: 3) {

View File

@@ -1,4 +1,5 @@
import SwiftUI
import SFSafeSymbols
/// Receive screen, rebuilt on native SwiftUI. A grouped `List` of received
/// transfers with swipe-to-delete, and the acquisition flow as a native sheet.
@@ -26,13 +27,13 @@ struct ReceiveScreen: View {
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: model.openAcquisition) {
Label(String(localized: L10n.Button.receiveFiles), systemImage: "plus")
Label(String(localized: L10n.Button.receiveFiles), systemSymbol: .plus)
}
}
if !deletable.isEmpty {
ToolbarItem(placement: .primaryAction) {
Button(role: .destructive, action: model.requestClearHistory) {
Label(String(localized: L10n.Receive.clearHistory), systemImage: "trash")
Label(String(localized: L10n.Receive.clearHistory), systemSymbol: .trash)
}
}
}
@@ -74,7 +75,7 @@ struct ReceiveScreen: View {
Button(role: .destructive) {
model.requestDeleteHistoryItem(transfer.transferId)
} label: {
Label(String(localized: L10n.Button.deleteTransfer), systemImage: "trash")
Label(String(localized: L10n.Button.deleteTransfer), systemSymbol: .trash)
}
}
}
@@ -89,12 +90,12 @@ struct ReceiveScreen: View {
private var emptyState: some View {
ContentUnavailableView {
Label(String(localized: L10n.Receive.emptyTitle), systemImage: "tray.and.arrow.down")
Label(String(localized: L10n.Receive.emptyTitle), systemSymbol: .trayAndArrowDown)
} description: {
Text(String(localized: L10n.Receive.emptyBody))
} actions: {
Button(action: model.openAcquisition) {
Label(String(localized: L10n.Button.receiveFiles), systemImage: "plus")
Label(String(localized: L10n.Button.receiveFiles), systemSymbol: .plus)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
@@ -129,7 +130,7 @@ private struct ReceiveTransferRow: View {
var body: some View {
HStack(spacing: 12) {
Image(systemName: "doc")
Image(systemSymbol: .doc)
.foregroundStyle(.secondary)
.frame(width: 40, height: 40)
.background(.quaternary, in: RoundedRectangle(cornerRadius: 9))