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 import SwiftUI
/// App root, ported from `App.kt`. Owns the object graph and feature models, wires /// App root, ported from `App.kt`. Owns the object graph and feature models, wires
@@ -111,7 +112,7 @@ struct RootView: View {
#if os(macOS) #if os(macOS)
NavigationSplitView { NavigationSplitView {
List(AppDestination.allCases, selection: sidebarBinding) { destination in List(AppDestination.allCases, selection: sidebarBinding) { destination in
Label(String(localized: destination.labelKey), systemImage: destination.systemImage) Label(String(localized: destination.labelKey), systemSymbol: destination.systemSymbol)
.tag(destination) .tag(destination)
} }
.navigationSplitViewColumnWidth(min: 180, ideal: 200, max: 260) .navigationSplitViewColumnWidth(min: 180, ideal: 200, max: 260)
@@ -123,7 +124,7 @@ struct RootView: View {
ForEach(AppDestination.allCases) { destination in ForEach(AppDestination.allCases) { destination in
screen(for: destination, windowClass: windowClass) screen(for: destination, windowClass: windowClass)
.tabItem { .tabItem {
Label(String(localized: destination.labelKey), systemImage: destination.systemImage) Label(String(localized: destination.labelKey), systemSymbol: destination.systemSymbol)
} }
.tag(destination) .tag(destination)
} }

View File

@@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import SFSafeSymbols
/// Non-dismissable receiver-approval modal, presented as a native sheet that can't /// Non-dismissable receiver-approval modal, presented as a native sheet that can't
/// be swiped away. The endpoint id is the trusted identity; display names are /// be swiped away. The endpoint id is the trusted identity; display names are
@@ -40,7 +41,7 @@ private struct ApprovalSheet: View {
let busy = state.respondingIds.contains(request.id) let busy = state.respondingIds.contains(request.id)
let receiver = request.receiverName ?? request.receiverDeviceName ?? String(localized: L10n.Approval.nearbyDevice) let receiver = request.receiverName ?? request.receiverDeviceName ?? String(localized: L10n.Approval.nearbyDevice)
VStack(spacing: 16) { VStack(spacing: 16) {
Image(systemName: "checkmark.shield.fill") Image(systemSymbol: .checkmarkShieldFill)
.font(.system(size: 44)) .font(.system(size: 44))
.foregroundStyle(.tint) .foregroundStyle(.tint)
.padding(.top, 12) .padding(.top, 12)

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import SFSafeSymbols
/// Send screen, rebuilt on native SwiftUI. A grouped `List` of outgoing transfers, /// Send screen, rebuilt on native SwiftUI. A grouped `List` of outgoing transfers,
/// with the composer and detail panels as native sheets and delete as an alert. /// with the composer and detail panels as native sheets and delete as an alert.
@@ -31,7 +32,7 @@ struct SendScreen: View {
.toolbar { .toolbar {
ToolbarItem(placement: .primaryAction) { ToolbarItem(placement: .primaryAction) {
Button(action: model.openComposer) { Button(action: model.openComposer) {
Label(String(localized: L10n.Button.createNewTransfer), systemImage: "plus") Label(String(localized: L10n.Button.createNewTransfer), systemSymbol: .plus)
} }
} }
} }
@@ -101,12 +102,12 @@ struct SendScreen: View {
private var emptyState: some View { private var emptyState: some View {
ContentUnavailableView { ContentUnavailableView {
Label(String(localized: L10n.Send.emptyTitle), systemImage: "paperplane") Label(String(localized: L10n.Send.emptyTitle), systemSymbol: .paperplane)
} description: { } description: {
Text(String(localized: L10n.Send.emptyBody)) Text(String(localized: L10n.Send.emptyBody))
} actions: { } actions: {
Button(action: model.openComposer) { Button(action: model.openComposer) {
Label(String(localized: L10n.Button.createNewTransfer), systemImage: "plus") Label(String(localized: L10n.Button.createNewTransfer), systemSymbol: .plus)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.controlSize(.large) .controlSize(.large)
@@ -166,7 +167,7 @@ private struct TransferListItem: View {
.padding(.top, 2) .padding(.top, 2)
} }
} }
Image(systemName: "chevron.forward") Image(systemSymbol: .chevronForward)
.font(.footnote.weight(.semibold)).foregroundStyle(.tertiary) .font(.footnote.weight(.semibold)).foregroundStyle(.tertiary)
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
@@ -181,7 +182,7 @@ struct FileArtwork: View {
image.resizable().aspectRatio(contentMode: .fill) image.resizable().aspectRatio(contentMode: .fill)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
} else { } else {
Image(systemName: "doc") Image(systemSymbol: .doc)
.font(.system(size: 18)) .font(.system(size: 18))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }

View File

@@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import SFSafeSymbols
/// Transfer composer drawer, ported from `feature/send/TransferComposer.kt`. /// Transfer composer drawer, ported from `feature/send/TransferComposer.kt`.
/// Two steps: choose files/folder, then review + name + access policy + share. /// Two steps: choose files/folder, then review + name + access policy + share.
@@ -27,7 +28,7 @@ struct TransferComposer: View {
Text(String(localized: L10n.Send.chooseFileBody)) Text(String(localized: L10n.Send.chooseFileBody))
.font(.subheadline).foregroundStyle(.secondary) .font(.subheadline).foregroundStyle(.secondary)
VStack(spacing: 14) { VStack(spacing: 14) {
Image(systemName: "doc").font(.system(size: 30)).foregroundStyle(.tint) Image(systemSymbol: .doc).font(.system(size: 30)).foregroundStyle(.tint)
PrimaryButton(title: String(localized: L10n.Button.chooseFiles), action: model.selectFile).fixedSize() PrimaryButton(title: String(localized: L10n.Button.chooseFiles), action: model.selectFile).fixedSize()
QuietButton(title: String(localized: L10n.Button.chooseFolder), action: model.selectFolder) QuietButton(title: String(localized: L10n.Button.chooseFolder), action: model.selectFolder)
} }
@@ -57,17 +58,17 @@ struct TransferComposer: View {
value: Binding(get: { state.senderName }, set: { model.setSenderName($0) })) value: Binding(get: { state.senderName }, set: { model.setSenderName($0) }))
Text(String(localized: L10n.Send.accessTitle)).font(.headline) Text(String(localized: L10n.Send.accessTitle)).font(.headline)
PolicyOption( PolicyOption(
icon: "checkmark.shield", titleKey: L10n.Send.accessApproval, descKey: L10n.Send.accessApprovalDescription, icon: .checkmarkShield, titleKey: L10n.Send.accessApproval, descKey: L10n.Send.accessApprovalDescription,
selected: state.accessPolicy == .requireApproval, selected: state.accessPolicy == .requireApproval,
onTap: { model.setAccessPolicy(.requireApproval) } onTap: { model.setAccessPolicy(.requireApproval) }
) )
PolicyOption( PolicyOption(
icon: "globe", titleKey: L10n.Send.accessAnyone, descKey: L10n.Send.accessAnyoneDescription, icon: .globe, titleKey: L10n.Send.accessAnyone, descKey: L10n.Send.accessAnyoneDescription,
selected: state.accessPolicy == .anyoneWithTransfer, selected: state.accessPolicy == .anyoneWithTransfer,
onTap: { model.setAccessPolicy(.anyoneWithTransfer) } onTap: { model.setAccessPolicy(.anyoneWithTransfer) }
) )
if state.accessPolicy == .anyoneWithTransfer { if state.accessPolicy == .anyoneWithTransfer {
Label(String(localized: L10n.Send.accessAnyoneWarning), systemImage: "exclamationmark.triangle.fill") Label(String(localized: L10n.Send.accessAnyoneWarning), systemSymbol: .exclamationmarkTriangleFill)
.font(.caption).foregroundStyle(.orange) .font(.caption).foregroundStyle(.orange)
} }
actions actions
@@ -116,7 +117,7 @@ private struct SelectedFileCard: View {
Spacer() Spacer()
if canRemove { if canRemove {
Button(role: .destructive, action: onRemove) { Button(role: .destructive, action: onRemove) {
Image(systemName: "trash") Image(systemSymbol: .trash)
} }
.buttonStyle(.borderless) .buttonStyle(.borderless)
.tint(.red) .tint(.red)
@@ -135,7 +136,7 @@ private struct SelectedFileCard: View {
} }
private struct PolicyOption: View { private struct PolicyOption: View {
let icon: String let icon: SFSymbol
let titleKey: String.LocalizationValue let titleKey: String.LocalizationValue
let descKey: String.LocalizationValue let descKey: String.LocalizationValue
let selected: Bool let selected: Bool
@@ -144,7 +145,7 @@ private struct PolicyOption: View {
var body: some View { var body: some View {
Button(action: onTap) { Button(action: onTap) {
HStack(spacing: 12) { HStack(spacing: 12) {
Image(systemName: icon) Image(systemSymbol: icon)
.font(.system(size: 20)) .font(.system(size: 20))
.foregroundStyle(selected ? AnyShapeStyle(.tint) : AnyShapeStyle(.secondary)) .foregroundStyle(selected ? AnyShapeStyle(.tint) : AnyShapeStyle(.secondary))
.frame(width: 22) .frame(width: 22)
@@ -153,7 +154,7 @@ private struct PolicyOption: View {
Text(String(localized: descKey)).font(.caption).foregroundStyle(.secondary) Text(String(localized: descKey)).font(.caption).foregroundStyle(.secondary)
} }
Spacer() Spacer()
Image(systemName: selected ? "checkmark.circle.fill" : "circle") Image(systemSymbol: selected ? .checkmarkCircleFill : .circle)
.foregroundStyle(selected ? AnyShapeStyle(.tint) : AnyShapeStyle(.tertiary)) .foregroundStyle(selected ? AnyShapeStyle(.tint) : AnyShapeStyle(.tertiary))
} }
.padding(14) .padding(14)

View File

@@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import SFSafeSymbols
import CoreImage.CIFilterBuiltins import CoreImage.CIFilterBuiltins
/// Transfer details + drawer panels, ported from `feature/send/TransferDetails.kt`. /// Transfer details + drawer panels, ported from `feature/send/TransferDetails.kt`.
@@ -56,7 +57,7 @@ struct TransferDetailsView: View {
Button(role: .destructive) { Button(role: .destructive) {
showStopConfirmation = true showStopConfirmation = true
} label: { } label: {
Label(String(localized: L10n.Send.stopSharing), systemImage: "stop.circle") Label(String(localized: L10n.Send.stopSharing), systemSymbol: .stopCircle)
} }
} }
} }
@@ -69,7 +70,7 @@ struct TransferDetailsView: View {
.toolbar { .toolbar {
ToolbarItem(placement: .primaryAction) { ToolbarItem(placement: .primaryAction) {
Button(role: .destructive, action: model.requestDeleteTransfer) { Button(role: .destructive, action: model.requestDeleteTransfer) {
Image(systemName: "trash") Image(systemSymbol: .trash)
} }
} }
} }
@@ -115,7 +116,7 @@ private struct DetailDestination: View {
.font(.footnote) .font(.footnote)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
Image(systemName: "chevron.forward") Image(systemSymbol: .chevronForward)
.font(.footnote.weight(.semibold)).foregroundStyle(.tertiary) .font(.footnote.weight(.semibold)).foregroundStyle(.tertiary)
} }
.contentShape(Rectangle()) .contentShape(Rectangle())

View File

@@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import SFSafeSymbols
/// Settings screen, rebuilt on a native `Form` with `NavigationStack` push /// Settings screen, rebuilt on a native `Form` with `NavigationStack` push
/// navigation. The model stays the source of truth via a derived path binding. /// navigation. The model stays the source of truth via a derived path binding.
@@ -25,21 +26,21 @@ struct SettingsScreen: View {
Form { Form {
Section { Section {
NavigationLink(value: SettingsSection.preferences) { NavigationLink(value: SettingsSection.preferences) {
SettingsRow(icon: "person.crop.circle", title: String(localized: L10n.Preferences.title), value: model.state.username) SettingsRow(icon: .personCropCircle, title: String(localized: L10n.Preferences.title), value: model.state.username)
} }
NavigationLink(value: SettingsSection.appearance) { NavigationLink(value: SettingsSection.appearance) {
SettingsRow(icon: "sun.max", title: String(localized: L10n.Appearance.title), value: themeModeLabel(model.state.themeMode)) SettingsRow(icon: .sunMax, title: String(localized: L10n.Appearance.title), value: themeModeLabel(model.state.themeMode))
} }
} }
Section { Section {
NavigationLink(value: SettingsSection.notifications) { NavigationLink(value: SettingsSection.notifications) {
SettingsRow(icon: "bell", title: String(localized: L10n.Notifications.title), value: nil) SettingsRow(icon: .bell, title: String(localized: L10n.Notifications.title), value: nil)
} }
NavigationLink(value: SettingsSection.storage) { NavigationLink(value: SettingsSection.storage) {
SettingsRow(icon: "internaldrive", title: String(localized: L10n.Storage.title), value: nil) SettingsRow(icon: .internaldrive, title: String(localized: L10n.Storage.title), value: nil)
} }
NavigationLink(value: SettingsSection.about) { NavigationLink(value: SettingsSection.about) {
SettingsRow(icon: "info.circle", title: String(localized: L10n.About.title), value: nil) SettingsRow(icon: .infoCircle, title: String(localized: L10n.About.title), value: nil)
} }
} }
} }
@@ -69,7 +70,7 @@ struct SettingsScreen: View {
Button { Button {
showBugReport = true showBugReport = true
} label: { } label: {
Label(String(localized: L10n.About.bugReport), systemImage: "ladybug") Label(String(localized: L10n.About.bugReport), systemSymbol: .ladybug)
} }
} }
} }
@@ -110,13 +111,13 @@ private struct SettingsSectionContent: View {
} }
struct SettingsRow: View { struct SettingsRow: View {
let icon: String let icon: SFSymbol
let title: String let title: String
let value: String? let value: String?
var body: some View { var body: some View {
HStack(spacing: 12) { HStack(spacing: 12) {
Image(systemName: icon) Image(systemSymbol: icon)
.foregroundStyle(.tint) .foregroundStyle(.tint)
.frame(width: 26) .frame(width: 26)
Text(title).foregroundStyle(.primary) Text(title).foregroundStyle(.primary)

View File

@@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import SFSafeSymbols
/// Settings section detail views, rebuilt as native `Form` content. Each view is /// Settings section detail views, rebuilt as native `Form` content. Each view is
/// placed inside a parent `Form`, so it returns `Section`s / rows directly. /// placed inside a parent `Form`, so it returns `Section`s / rows directly.
@@ -126,24 +127,24 @@ struct AboutSettings: View {
} }
Section(String(localized: L10n.About.isTitle)) { Section(String(localized: L10n.About.isTitle)) {
AboutPoint(L10n.About.isDirect, "paperplane") AboutPoint(L10n.About.isDirect, .paperplane)
AboutPoint(L10n.About.isNoAccount, "person.crop.circle.badge.xmark") AboutPoint(L10n.About.isNoAccount, .personCropCircleBadgeXmark)
AboutPoint(L10n.About.isInControl, "checkmark.shield") AboutPoint(L10n.About.isInControl, .checkmarkShield)
AboutPoint(L10n.About.isEncrypted, "lock") AboutPoint(L10n.About.isEncrypted, .lock)
AboutPoint(L10n.About.isOpen, "chevron.left.forwardslash.chevron.right") AboutPoint(L10n.About.isOpen, .chevronLeftForwardslashChevronRight)
} }
Section(String(localized: L10n.About.isntTitle)) { Section(String(localized: L10n.About.isntTitle)) {
AboutPoint(L10n.About.isntCloud, "icloud.slash") AboutPoint(L10n.About.isntCloud, .icloudSlash)
AboutPoint(L10n.About.isntSync, "arrow.triangle.2.circlepath") AboutPoint(L10n.About.isntSync, .arrowTriangle2Circlepath)
AboutPoint(L10n.About.isntPublic, "megaphone") AboutPoint(L10n.About.isntPublic, .megaphone)
} }
Section(String(localized: L10n.About.privacyTitle)) { Section(String(localized: L10n.About.privacyTitle)) {
AboutPoint(L10n.About.privacyCapability, "qrcode") AboutPoint(L10n.About.privacyCapability, .qrcode)
AboutPoint(L10n.About.privacyDeny, "hand.raised") AboutPoint(L10n.About.privacyDeny, .handRaised)
AboutPoint(L10n.About.privacyRelay, "antenna.radiowaves.left.and.right") AboutPoint(L10n.About.privacyRelay, .antennaRadiowavesLeftAndRight)
AboutPoint(L10n.About.privacyLocal, "internaldrive") AboutPoint(L10n.About.privacyLocal, .internaldrive)
} }
Section(String(localized: L10n.About.title)) { Section(String(localized: L10n.About.title)) {
@@ -154,7 +155,7 @@ struct AboutSettings: View {
} }
LabeledContent(String(localized: L10n.About.licenseLabel), value: "Apache 2.0") LabeledContent(String(localized: L10n.About.licenseLabel), value: "Apache 2.0")
Link(destination: Self.privacyPolicyURL) { Link(destination: Self.privacyPolicyURL) {
Label(String(localized: L10n.About.privacyPolicyLabel), systemImage: "hand.raised") Label(String(localized: L10n.About.privacyPolicyLabel), systemSymbol: .handRaised)
} }
} }
@@ -205,9 +206,9 @@ struct BugReportSheet: View {
/// A bullet-style informational row with an SF Symbol and wrapping localized text. /// A bullet-style informational row with an SF Symbol and wrapping localized text.
private struct AboutPoint: View { private struct AboutPoint: View {
let key: String.LocalizationValue let key: String.LocalizationValue
let symbol: String let symbol: SFSymbol
init(_ key: String.LocalizationValue, _ symbol: String) { init(_ key: String.LocalizationValue, _ symbol: SFSymbol) {
self.key = key self.key = key
self.symbol = symbol self.symbol = symbol
} }
@@ -218,7 +219,7 @@ private struct AboutPoint: View {
.font(.subheadline) .font(.subheadline)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} icon: { } icon: {
Image(systemName: symbol).foregroundStyle(.tint) Image(systemSymbol: symbol).foregroundStyle(.tint)
} }
} }
} }

View File

@@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import SFSafeSymbols
/// Bottom toast host driven by `UiMessageController`, ported from /// Bottom toast host driven by `UiMessageController`, ported from
/// `ui/feedback/VniDropSnackbarHost.kt`. Tone drives the accent color; errors get /// `ui/feedback/VniDropSnackbarHost.kt`. Tone drives the accent color; errors get
@@ -52,7 +53,7 @@ struct SnackbarHost: View {
.buttonStyle(.borderless) .buttonStyle(.borderless)
} }
Button(action: dismiss) { Button(action: dismiss) {
Image(systemName: "xmark") Image(systemSymbol: .xmark)
.font(.footnote.weight(.semibold)) .font(.footnote.weight(.semibold))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.frame(width: 36, height: 36) .frame(width: 36, height: 36)

View File

@@ -1,4 +1,5 @@
import Foundation import Foundation
import SFSafeSymbols
/// Top-level destinations, ported from `ui/navigation/AppDestination.kt`. /// Top-level destinations, ported from `ui/navigation/AppDestination.kt`.
enum AppDestination: String, CaseIterable, Identifiable { enum AppDestination: String, CaseIterable, Identifiable {
@@ -17,11 +18,11 @@ enum AppDestination: String, CaseIterable, Identifiable {
} }
/// SF Symbol approximating the Compose line icon. /// SF Symbol approximating the Compose line icon.
var systemImage: String { var systemSymbol: SFSymbol {
switch self { switch self {
case .send: return "paperplane" case .send: return .paperplane
case .receive: return "tray.and.arrow.down" case .receive: return .trayAndArrowDown
case .settings: return "gearshape" case .settings: return .gearshape
} }
} }
} }

View File

@@ -12,6 +12,9 @@ options:
packages: packages:
VnidropCore: VnidropCore:
path: VnidropCore path: VnidropCore
SFSafeSymbols:
url: https://github.com/SFSafeSymbols/SFSafeSymbols
from: "5.3.0"
targets: targets:
VniDrop: VniDrop:
@@ -48,6 +51,7 @@ targets:
CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDrop.entitlements CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDrop.entitlements
dependencies: dependencies:
- package: VnidropCore - package: VnidropCore
- package: SFSafeSymbols
- sdk: SystemConfiguration.framework - sdk: SystemConfiguration.framework
- sdk: Security.framework - sdk: Security.framework
- sdk: libresolv.tbd - sdk: libresolv.tbd