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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,7 @@ node_modules
# yarn
yarn-error.log

# Swift
.build

# End of https://www.gitignore.io/api/macos
31 changes: 31 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "CarLogos",
platforms: [
.macOS(.v15),
.iOS(.v18),
.tvOS(.v18),
.watchOS(.v11)
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "CarLogos",
targets: ["CarLogos"]
),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "CarLogos",
resources: [
.copy("logos")
]
),
]
)
27 changes: 27 additions & 0 deletions Sources/CarLogos/Models.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation

public struct CarLogo: Decodable, Sendable, Hashable {
public struct Images: Decodable, Sendable, Hashable {
// Paths are relative to the `logos/` folder (as shipped in the dataset)
public let localThumb: String?
public let localOptimized: String?
public let localOriginal: String?
// Remote fallbacks if you prefer network loading
public let remoteThumb: String?
public let remoteOptimized: String?
public let remoteOriginal: String?

enum CodingKeys: String, CodingKey {
case localThumb = "localThumb"
case localOptimized = "localOptimized"
case localOriginal = "localOriginal"
case remoteThumb = "thumb"
case remoteOptimized = "optimized"
case remoteOriginal = "original"
}
}

public let name: String
public let slug: String
public let image: Images
}
142 changes: 142 additions & 0 deletions Sources/CarLogos/Store.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import Foundation
import SwiftUI

public enum CarLogosStore {
public enum Variant {
case thumb
case optimized
case original
}

private static func logosBaseURL() -> URL? {
// Look for the `logos` directory inside bundled resources
if let explicit = Bundle.module.url(forResource: "logos", withExtension: nil) {
return explicit
}

// Fallback (some SPM setups): resourceURL/logos
if let res = Bundle.module.resourceURL?.appendingPathComponent("logos"),
FileManager.default.fileExists(atPath: res.path) {
return res
}

return nil
}

private static func dataJSONURL() -> URL? {
// data.json lives directly under logos/
if let base = logosBaseURL() {
let url = base.appendingPathComponent("data.json")
if FileManager.default.fileExists(atPath: url.path) {
return url
}
}
// As an extra fallback, try subdirectory lookup
return Bundle.module.url(
forResource: "data",
withExtension: "json",
subdirectory: "logos")
}

/// Load and decode all logo metadata from the bundled `data.json`.
public static func all() throws -> [CarLogo] {
guard let url = dataJSONURL() else {
throw NSError(
domain: "CarLogos",
code: 1,
userInfo: [
NSLocalizedDescriptionKey: "data.json not found in bundled resources. Ensure Resources/logos is included."
])
}

let data = try Data(contentsOf: url)
let decoder = JSONDecoder()

return try decoder.decode([CarLogo].self, from: data)
}

public static func logoForSlug(_ slug: String) -> CarLogo? {
return try? all().first(where: { $0.slug == slug })
}

public static func imageForLogo(_ logo: CarLogo, variant: Variant = .optimized) -> Image? {
guard let url = localURL(for: logo, variant: variant) else {
return nil
}

#if os(macOS)
if let nsImage = NSImage(contentsOf: url) {
return Image(nsImage: nsImage)
}
#else
if let uiImage = UIImage(contentsOfFile: url.path) {
return Image(uiImage: uiImage)
}
#endif

return nil
}

/// Resolve the best local path for a given variant, falling back sensibly.
public static func localPath(for logo: CarLogo, variant: Variant) -> String? {
switch variant {
case .thumb:
return logo.image.localThumb ?? logo.image.localOptimized ?? logo.image.localOriginal
case .optimized:
return logo.image.localOptimized ?? logo.image.localOriginal ?? logo.image.localThumb
case .original:
return logo.image.localOriginal ?? logo.image.localOptimized ?? logo.image.localThumb
}
}

/// Get a file URL for the selected image variant inside the bundle.
public static func localURL(for logo: CarLogo, variant: Variant) -> URL? {
guard let path = localPath(for: logo, variant: variant) else {
return nil
}

guard let base = logosBaseURL() else {
return nil
}

let url = base.appendingPathComponent(path)
if FileManager.default.fileExists(atPath: url.path) {
return url
}

// Some JSON variants include a leading slash — normalize
let trimmed = path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
let normalized = base.appendingPathComponent(trimmed)

return FileManager.default.fileExists(atPath: normalized.path) ? normalized : nil
}

/// Load platform-native image data for advanced uses.
public static func data(for logo: CarLogo, variant: Variant) -> Data? {
guard let url = localURL(for: logo, variant: variant) else {
return nil
}

return try? Data(contentsOf: url)
}

/// Optional: remote URLs if you prefer network loading and a smaller app binary.
public static func remoteURL(for logo: CarLogo, variant: Variant) -> URL? {
let raw: String?

switch variant {
case .thumb:
raw = logo.image.remoteThumb
case .optimized:
raw = logo.image.remoteOptimized
case .original:
raw = logo.image.remoteOriginal
}

guard let s = raw, let url = URL(string: s) else {
return nil
}

return url
}
}
1 change: 1 addition & 0 deletions Sources/CarLogos/logos