Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/Packages
xcuserdata/
DerivedData/
.derived-data/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,11 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.phosphoricons.PhosphorSwiftExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
Expand All @@ -472,8 +475,11 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.phosphoricons.PhosphorSwiftExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
Expand Down
128 changes: 75 additions & 53 deletions Example/PhosphorSwiftExample/PhosphorSwiftExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,68 +31,90 @@ struct ContentView: View {
}

var body: some View {
#if os(macOS)
HSplitView {
Form {
Section(header: Text("Icon Style").font(.headline.smallCaps())) {
Picker("Weight", selection: $selectedWeight) {
Text("Regular").tag(Ph.IconWeight.regular)
Text("Thin").tag(Ph.IconWeight.thin)
Text("Light").tag(Ph.IconWeight.light)
Text("Bold").tag(Ph.IconWeight.bold)
Text("Fill").tag(Ph.IconWeight.fill)
Text("Duotone").tag(Ph.IconWeight.duotone)
}
ColorPicker("Color", selection: $color)
LabeledContent("Size") {
HStack {
TextField("", value: $size, formatter: NumberFormatter())
.multilineTextAlignment(.trailing)
.textFieldStyle(.plain)
.labelsHidden()
.frame(maxWidth: 60)
Stepper("Value", value: $size, in: 16 ... 256, step: 4)
.labelsHidden()
}
controlPanel
.frame(minWidth: 280, idealWidth: 300, maxHeight: .infinity)
iconGrid
}
#else
NavigationStack {
VStack(spacing: 16) {
controlPanel
Divider()
iconGrid
}
.padding()
.navigationTitle("Phosphor Icons")
}
#endif
}

private var controlPanel: some View {
Form {
Section(header: Text("Icon Style").font(.headline.smallCaps())) {
Picker("Weight", selection: $selectedWeight) {
Text("Regular").tag(Ph.IconWeight.regular)
Text("Thin").tag(Ph.IconWeight.thin)
Text("Light").tag(Ph.IconWeight.light)
Text("Bold").tag(Ph.IconWeight.bold)
Text("Fill").tag(Ph.IconWeight.fill)
Text("Duotone").tag(Ph.IconWeight.duotone)
}
ColorPicker("Color", selection: $color)
LabeledContent("Size") {
HStack {
TextField("", value: $size, formatter: NumberFormatter())
.multilineTextAlignment(.trailing)
.textFieldStyle(.plain)
.labelsHidden()
.frame(maxWidth: 60)
Stepper("Value", value: $size, in: 16 ... 256, step: 4)
.labelsHidden()
}
LabeledContent("Columns") {
HStack {
TextField("", value: $cols, formatter: NumberFormatter())
.multilineTextAlignment(.trailing)
.textFieldStyle(.plain)
.labelsHidden()
.frame(maxWidth: 60)
Stepper("", value: $cols, in: 1 ... 100)
.labelsHidden()
}
}
LabeledContent("Columns") {
HStack {
TextField("", value: $cols, formatter: NumberFormatter())
.multilineTextAlignment(.trailing)
.textFieldStyle(.plain)
.labelsHidden()
.frame(maxWidth: 60)
Stepper("", value: $cols, in: 1 ... 100)
.labelsHidden()
}
}
}

Section(header: Text("Rendering").font(.headline.smallCaps())) {
Picker("Interpolation", selection: $interp) {
Text("None").tag(Image.Interpolation.none)
Text("Low").tag(Image.Interpolation.low)
Text("Medium").tag(Image.Interpolation.medium)
Text("High").tag(Image.Interpolation.high)
}.pickerStyle(.inline)
Toggle("Antialiasing", isOn: $aa)
Section(header: Text("Rendering").font(.headline.smallCaps())) {
Picker("Interpolation", selection: $interp) {
Text("None").tag(Image.Interpolation.none)
Text("Low").tag(Image.Interpolation.low)
Text("Medium").tag(Image.Interpolation.medium)
Text("High").tag(Image.Interpolation.high)
}
.pickerStyle(.inline)
Toggle("Antialiasing", isOn: $aa)
}
.formStyle(.grouped)
.frame(minWidth: 280, idealWidth: 300, maxHeight: .infinity)
}
.formStyle(.grouped)
}

ScrollView {
LazyVGrid(columns: columns) {
ForEach(Ph.allCases) { icon in
icon.weight(selectedWeight)
.interpolation(interp)
.antialiased(aa)
.aspectRatio(contentMode: .fit)
.color(color)
.help(icon.rawValue.camelCased(with: "-"))
}
}.padding()
}.frame(maxWidth: .infinity)
private var iconGrid: some View {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(Ph.allCases) { icon in
icon.weight(selectedWeight, size: CGSize(width: CGFloat(size), height: CGFloat(size)))
.interpolation(interp)
.antialiased(aa)
.aspectRatio(contentMode: .fit)
.color(color)
.help(icon.rawValue.camelCased(with: "-"))
}
}
.padding()
}
.frame(maxWidth: .infinity)
}
}

Expand Down
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ let package = Package(
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "PhosphorSwift",
path: "Sources"),
path: "Sources",
resources: [
.copy("PhosphorSwift/Resources/BaseSVGs")
]),
.testTarget(
name: "PhosphorSwiftTests",
dependencies: ["PhosphorSwift"]),
Expand Down
Binary file modified Scripts/Build
Binary file not shown.
164 changes: 124 additions & 40 deletions Scripts/Build.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,23 @@ enum Build {
static func main() async throws {
shell("git", "submodule", "update", "--remote", "--init", "--force", "--recursive")

let icons = try await buildAssets()
// Extract base icons from regular directory
let icons = try await extractBaseIcons()
print("Extracted \(icons.count) base icons")

// Copy base SVG files
try await copyBaseSVGs(icons)
print("Copied base SVG files")

// Generate Icons.swift
try await emitSource(icons: icons)
print("Generated source code")

// Clean up old weight variants
try await cleanupOldAssets()
print("Cleaned up old assets")

print("Build complete!")
}
}

Expand Down Expand Up @@ -57,59 +72,128 @@ extension String {
}
}

func buildAssets() async throws -> Set<String> {
let CORE_DIR = URL(fileURLWithPath: "./core/assets", isDirectory: true)
let ASSETS_DIR = URL(fileURLWithPath: "./Sources/PhosphorSwift/Resources/Assets.xcassets/SVG", isDirectory: true)

func extractBaseIcons() async throws -> Set<String> {
let CORE_REGULAR_DIR = URL(fileURLWithPath: "./core/assets/regular", isDirectory: true)
let fm = FileManager.default
let encoder = JSONEncoder()
var baseIcons: Set<String> = Set()

var icons: Set<String> = Set()
guard fm.fileExists(atPath: CORE_REGULAR_DIR.path) else {
print("Core regular assets directory not found.")
throw BuildError.missingCoreAssets
}

let resourceKeys: [URLResourceKey] = [.isDirectoryKey]
guard let enumerator = fm.enumerator(
at: CORE_REGULAR_DIR,
includingPropertiesForKeys: resourceKeys,
options: [.skipsHiddenFiles]
) else {
throw BuildError.cannotEnumerateAssets
}

do {
let resourceKeys: [URLResourceKey] = [.creationDateKey, .isDirectoryKey]
let enumerator = fm.enumerator(
at: CORE_DIR,
includingPropertiesForKeys: resourceKeys,
options: [.skipsHiddenFiles])!
for case let fileURL as URL in enumerator {
let resourceValues = try fileURL.resourceValues(forKeys: Set(resourceKeys))
guard !resourceValues.isDirectory! else { continue }

for case let fileURL as URL in enumerator {
let resourceValues = try fileURL.resourceValues(forKeys: Set(resourceKeys))
if !resourceValues.isDirectory! {
let fileName = fileURL.deletingPathExtension().lastPathComponent
let directory = ASSETS_DIR.appendingPathComponent("\(fileName).imageset")
let svgURL = directory.appendingPathComponent("\(fileName).svg")

let contents = try encoder.encode(Contents.forFile(filename: "\(fileName).svg"))

try fm.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)

if fm.fileExists(atPath: svgURL.path()) {
try fm.removeItem(at: svgURL)
}

try fm.copyItem(at: fileURL, to: svgURL)
try contents.write(to: directory.appendingPathComponent("Contents.json"), options: .atomic)

if !(fileName.hasSuffix("-thin") || fileName.hasSuffix("-light") || fileName.hasSuffix("-bold") || fileName.hasSuffix("-fill") || fileName.hasSuffix("-duotone")) {
icons.insert(fileName)
}

print(fileName, contents)
let fileName = fileURL.deletingPathExtension().lastPathComponent
baseIcons.insert(fileName)
}

return baseIcons
}

enum IconWeight: String, CaseIterable {
case regular
case thin
case light
case bold
case fill
case duotone

var directoryName: String { rawValue }

var filenameSuffix: String? {
switch self {
case .regular:
return nil
case .thin, .light, .bold, .fill, .duotone:
return "-\(rawValue)"
}
}
}

func copyBaseSVGs(_ baseIcons: Set<String>) async throws {
let svgRoot = URL(fileURLWithPath: "./Sources/PhosphorSwift/Resources/BaseSVGs", isDirectory: true)
let fm = FileManager.default

if fm.fileExists(atPath: svgRoot.path) {
try fm.removeItem(at: svgRoot)
}
try fm.createDirectory(at: svgRoot, withIntermediateDirectories: true)

var missing: [IconWeight: [String]] = [:]

for weight in IconWeight.allCases {
let destinationDirectory = svgRoot.appendingPathComponent(weight.rawValue, isDirectory: true)
try fm.createDirectory(at: destinationDirectory, withIntermediateDirectories: true)

let sourceDirectory = URL(fileURLWithPath: "./core/assets/\(weight.directoryName)", isDirectory: true)
guard fm.fileExists(atPath: sourceDirectory.path) else {
print("Missing source directory for weight \(weight.rawValue) at \(sourceDirectory.path)")
continue
}

for icon in baseIcons {
let filename: String
if let suffix = weight.filenameSuffix {
filename = "\(icon)\(suffix).svg"
} else {
filename = "\(icon).svg"
}
let sourceURL = sourceDirectory.appendingPathComponent(filename)
let destinationURL = destinationDirectory.appendingPathComponent("\(icon).svg")

if fm.fileExists(atPath: sourceURL.path) {
try fm.copyItem(at: sourceURL, to: destinationURL)
} else {
missing[weight, default: []].append(icon)
}
}
} catch {
print(error)
}

if !missing.isEmpty {
for (weight, icons) in missing {
print("Missing \(icons.count) icons for weight \(weight.rawValue): \(icons.sorted().joined(separator: ", "))")
}
}
}

func cleanupOldAssets() async throws {
let ASSETS_DIR = URL(fileURLWithPath: "./Sources/PhosphorSwift/Resources/Assets.xcassets", isDirectory: true)
let fm = FileManager.default

return icons
// Remove the old SVG assets directory
let svgAssetsDir = ASSETS_DIR.appendingPathComponent("SVG")
if fm.fileExists(atPath: svgAssetsDir.path) {
try fm.removeItem(at: svgAssetsDir)
}
}

enum BuildError: Error {
case cannotEnumerateAssets
case missingCoreAssets
}

func emitSource(icons: Set<String>) async throws {
let ICONS_SOURCE = URL(fileURLWithPath: "./Sources/PhosphorSwift/Icons.swift", isDirectory: false)

let enumEntries = icons.sorted().map { name in
" case \(name.camelCased(with: "-")) = \"\(name)\""
let caseName = name.camelCased(with: "-")
// Handle Swift keywords
if caseName == "repeat" {
return " case `\(caseName)` = \"\(name)\""
}
return " case \(caseName) = \"\(name)\""
}
let source = """
//
Expand Down
Loading