Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 56 additions & 12 deletions Example/SFSymbolsExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,64 @@ import SFSymbols
import SwiftUI

struct ContentView: View {
@State private var searchText = ""
@State private var symbolScope = SymbolScope.publicSymbols
@State private var selectedSymbol: SymbolCatalog.Item?

private let catalog = SymbolCatalog.system
private let gridColumns = [
GridItem(.adaptive(minimum: 120), spacing: 12),
]

var body: some View {
VStack {
Section("Public") {
Text("Symbols count: \(SFSymbols.symbol_order.count)")
Text("Name alias count: \(SFSymbols.name_aliases.count)")
Text("No-fill-to-fill count: \(SFSymbols.nofill_to_fill.count)")
let matchingSymbols = catalog.symbols(
matching: searchText,
scope: symbolScope
)

NavigationStack {
ScrollView {
if matchingSymbols.isEmpty {
ContentUnavailableView(
"No Symbols Found",
systemImage: "magnifyingglass",
description: Text("Try another name or search scope.")
)
.frame(maxWidth: .infinity, minHeight: 300)
} else {
LazyVGrid(columns: gridColumns, spacing: 12) {
ForEach(matchingSymbols) { symbol in
Button {
selectedSymbol = symbol
} label: {
SymbolTile(symbol: symbol)
}
.buttonStyle(.plain)
}
}
.padding()
}
}
.navigationTitle("SF Symbols")
.searchable(text: $searchText, prompt: "Search names and aliases")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Picker("Symbol scope", selection: $symbolScope) {
ForEach(SymbolScope.allCases) { scope in
Text(scope.title).tag(scope)
}
}
.pickerStyle(.menu)
}

ToolbarItem(placement: .status) {
Text("Showing \(matchingSymbols.count) of \(catalog.count(for: symbolScope)) symbols")
.font(.footnote)
.foregroundStyle(.secondary)
}
}

Divider()

Section("Private") {
Text("Symbols count: \(SFSymbols.private_symbol_order.count)")
Text("Name alias count: \(SFSymbols.private_name_aliases.count)")
Text("No-fill-to-fill count: \(SFSymbols.private_nofill_to_fill.count)")
.sheet(item: $selectedSymbol) { symbol in
SymbolDetailsView(symbol: symbol)
}
}
}
Expand Down
90 changes: 90 additions & 0 deletions Example/SFSymbolsExample/Support/SymbolAvailabilityItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// SymbolAvailabilityItem.swift
// Example
//
// Created by Yanan Li on 2026/7/11.
//

import Foundation
import SFSymbols

struct SymbolCatalog {
static let system = SymbolCatalog()

private let publicSymbols: [SymbolCatalog.Item]
private let privateSymbols: [SymbolCatalog.Item]

private init() {
let publicAliases = Dictionary(grouping: SFSymbols.name_aliases.keys) {
SFSymbols.name_aliases[$0] ?? ""
}
let privateAliases = Dictionary(grouping: SFSymbols.private_name_aliases.keys) {
SFSymbols.private_name_aliases[$0] ?? ""
}

publicSymbols = SFSymbols.symbol_order.map { name in
SymbolCatalog.Item(
name: name,
aliases: publicAliases[name, default: []].sorted(),
filledSymbolName: SFSymbols.nofill_to_fill[name],
isPrivate: false,
availability: SFSymbols.SymbolMetadataStore.system.availability(forSystemName: name)
)
}
privateSymbols = SFSymbols.private_symbol_order.map { name in
SymbolCatalog.Item(
name: name,
aliases: privateAliases[name, default: []].sorted(),
filledSymbolName: SFSymbols.private_nofill_to_fill[name],
isPrivate: true,
availability: SFSymbols.SymbolMetadataStore.system.availability(forSystemName: name)
)
}
}

func symbols(matching searchText: String, scope: SymbolScope) -> [SymbolCatalog.Item] {
let symbols: [SymbolCatalog.Item]
switch scope {
case .publicSymbols:
symbols = publicSymbols
case .privateSymbols:
symbols = privateSymbols
case .allSymbols:
symbols = publicSymbols + privateSymbols
}

guard !searchText.isEmpty else {
return symbols
}

return symbols.filter { symbol in
symbol.name.localizedCaseInsensitiveContains(searchText)
|| symbol.aliases.contains {
$0.localizedCaseInsensitiveContains(searchText)
}
}
}

func count(for scope: SymbolScope) -> Int {
switch scope {
case .publicSymbols:
publicSymbols.count
case .privateSymbols:
privateSymbols.count
case .allSymbols:
publicSymbols.count + privateSymbols.count
}
}

struct Item: Identifiable {
let name: String
let aliases: [String]
let filledSymbolName: String?
let isPrivate: Bool
let availability: SFSymbols.Availability?

var id: String {
"\(isPrivate ? "private" : "public"):\(name)"
}
}
}
27 changes: 27 additions & 0 deletions Example/SFSymbolsExample/Support/SymbolScope.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// SymbolScope.swift
// Example
//
// Created by Yanan Li on 2026/7/11.
//

import Foundation

enum SymbolScope: String, CaseIterable, Identifiable {
case publicSymbols
case privateSymbols
case allSymbols

var id: Self { self }

var title: String {
switch self {
case .publicSymbols:
"Public"
case .privateSymbols:
"Private"
case .allSymbols:
"All"
}
}
}
150 changes: 150 additions & 0 deletions Example/SFSymbolsExample/SymbolDetailsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//
// SymbolDetailsView.swift
// Example
//
// Created by Yanan Li on 2026/7/11.
//

import SFSymbols
import SwiftUI

struct SymbolDetailsView: View {
@Environment(\.dismiss) private var dismiss

let symbol: SymbolCatalog.Item

private let availabilityPlatforms: [SFSymbols.Availability.Platform] = [
.sfSymbols,
.iOS,
.macOS,
.macCatalyst,
.watchOS,
.tvOS,
.visionOS,
]

var body: some View {
#if os(macOS)
detailsContent
.frame(width: 600, height: 640)
#else
detailsContent
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
#endif
}

private var detailsContent: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: 24) {
VStack(spacing: 16) {
Group {
if symbol.isPrivate {
Image(_internalSystemName: symbol.name)
} else {
Image(systemName: symbol.name)
}
}
.font(.system(size: 72))
.frame(height: 84)
.accessibilityHidden(true)

Text(symbol.name)
.font(.title3.monospaced())
.fontWeight(.medium)
.multilineTextAlignment(.center)
.textSelection(.enabled)

if !symbol.aliases.isEmpty {
Text("Previously: \(symbol.aliases.joined(separator: ", "))")
.font(.callout)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.textSelection(.enabled)
}

Text(symbol.isPrivate ? "Private" : "Public")
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(.quaternary, in: Capsule())
}
.frame(maxWidth: .infinity)

if let availability = symbol.availability {
DetailSection(title: "Availability") {
LazyVGrid(
columns: [GridItem(.adaptive(minimum: 150), spacing: 12)],
spacing: 12
) {
ForEach(availabilityPlatforms, id: \.rawValue) { platform in
if let version = availability.earliestSupportedRelease(for: platform) {
VStack(alignment: .leading, spacing: 4) {
Text(platform.rawValue)
.font(.caption)
.foregroundStyle(.secondary)
Text(version.description)
.font(.body.monospacedDigit())
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background(.quaternary, in: RoundedRectangle(cornerRadius: 10))
}
}
}
}
}

if let filledSymbolName = symbol.filledSymbolName {
DetailSection(title: "Filled Variant") {
HStack(spacing: 12) {
Group {
if symbol.isPrivate {
Image(_internalSystemName: filledSymbolName)
} else {
Image(systemName: filledSymbolName)
}
}
.font(.title2)
.frame(width: 32)

Text(filledSymbolName)
.font(.body.monospaced())
.textSelection(.enabled)

Spacer()
}
.padding(14)
.background(.quaternary, in: RoundedRectangle(cornerRadius: 10))
}
}
}
.padding(24)
}
.navigationTitle("Symbol Details")
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
dismiss()
}
}
}
}
}

struct DetailSection<Content: View>: View {
let title: String
@ViewBuilder let content: Content

var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(title)
.font(.headline)

content
}
}
}
}
Loading
Loading