diff --git a/Package.swift b/Package.swift index faa03af..2d00cef 100644 --- a/Package.swift +++ b/Package.swift @@ -1,19 +1,27 @@ -// swift-tools-version:5.4 -// The swift-tools-version declares the minimum version of Swift required to build this package. +// swift-tools-version:5.9 import PackageDescription let package = Package( name: "MaterialDesignSymbol", - platforms: [.iOS(.v10), - .watchOS(.v3)], + platforms: [ + .iOS(.v13), + .macOS(.v11), + .watchOS(.v6), + .tvOS(.v13), + .visionOS(.v1) + ], products: [ .library(name: "MaterialDesignSymbol", targets: ["MaterialDesignSymbol"]) ], - dependencies: [], targets: [ - .target(name: "MaterialDesignSymbol", - resources: [ - .process("Resources")]) + .target( + name: "MaterialDesignSymbol", + resources: [.process("Resources")] + ), + .testTarget( + name: "MaterialDesignSymbolTests", + dependencies: ["MaterialDesignSymbol"] + ) ], swiftLanguageVersions: [.v5] ) diff --git a/Sources/MaterialDesignSymbol/FontLoader.swift b/Sources/MaterialDesignSymbol/FontLoader.swift deleted file mode 100644 index e3812d1..0000000 --- a/Sources/MaterialDesignSymbol/FontLoader.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// FontLoader -// -// Created by tichise on 2015/5/7 15/05/07. -// Copyright (c) 2015 tichise. All rights reserved. -// - -#if !os(macOS) -import UIKit - -/** - フォント読み込み用クラス - */ -public class FontLoader { - - /** - 引数で渡ってきたフォントを読み込みます - - parameter name: フォントファイル名 - */ - public class func loadFont(_ name: String) throws { - - guard let ttfPath = Bundle(for: object_getClass(self)!).path(forResource: name, ofType: "ttf") else { - throw FontError.invalidFontFile - } - - guard let fileHandle = FileHandle(forReadingAtPath: ttfPath) else { - throw FontError.fontPathNotFound - } - - let data = fileHandle.readDataToEndOfFile() - - guard let provider = CGDataProvider(data: data as CFData) else { - throw FontError.invalidFontFile - } - - guard let font = CGFont(provider) else { - throw FontError.initFontError - } - - var error: Unmanaged? - - if !CTFontManagerRegisterGraphicsFont(font, &error) { - throw FontError.registerFailed - } - } -} - -public enum FontError: Error { - case invalidFontFile - case fontPathNotFound - case initFontError - case registerFailed -} -#endif diff --git a/Sources/MaterialDesignSymbol/MaterialDesignFont.swift b/Sources/MaterialDesignSymbol/MaterialDesignFont.swift index 52c15c0..c27f14d 100644 --- a/Sources/MaterialDesignSymbol/MaterialDesignFont.swift +++ b/Sources/MaterialDesignSymbol/MaterialDesignFont.swift @@ -1,71 +1,140 @@ // -// MaterialDesignFont +// MaterialDesignFont.swift +// MaterialDesignSymbol // -// Created by tichise on 2015/5/7 15/05/07. +// Created by tichise on 2015/5/7. // Copyright (c) 2015 tichise. All rights reserved. // -#if !os(macOS) +#if canImport(UIKit) import UIKit +public typealias MDSFont = UIFont +#elseif canImport(AppKit) +import AppKit +public typealias MDSFont = NSFont +#endif + +import CoreGraphics +import CoreText +import Foundation + +/// Material Design Icons font manager +public final class MaterialDesignFont: @unchecked Sendable { + + /// Shared singleton instance + public static let shared = MaterialDesignFont() -/** - マテリアルデザインアイコンをUIFont形式で呼ぶに使うクラス - */ -public struct MaterialDesignFont { - - static let shared = MaterialDesignFont() - - /// 呼び出すアイコンファイル名 - private let name = "material-design-icons" + /// Font file name (without extension) + private let fontName = "material-design-icons" + + /// Thread-safe font registration state + private static var isRegistered = false + private static let lock = NSLock() private init() { - loadFont() + registerFontIfNeeded() } - - /// このメソッドはSPMの場合だけ使います。 - public func loadFont() { - /// 呼び出すアイコンファイル名 - registerFont(name: name, fileExtension: "ttf") + + // MARK: - Public API + + /// Get Material Design Icons font with specified size + /// - Parameter fontSize: The desired font size + /// - Returns: The font, or nil if font loading failed + public func fontOfSize(_ fontSize: CGFloat) -> MDSFont? { + registerFontIfNeeded() + return MDSFont(name: fontName, size: fontSize) } - - private func registerFont(name: String, fileExtension: String) { - #if SWIFT_PACKAGE - guard let fontURL = Bundle.module.url(forResource: name, withExtension: fileExtension) else { - print("No font named \(name).\(fileExtension) was found in the module bundle") + + /// Static convenience method for getting font + /// - Parameter fontSize: The desired font size + /// - Returns: The font, or nil if font loading failed + public static func fontOfSize(_ fontSize: CGFloat) -> MDSFont? { + shared.fontOfSize(fontSize) + } + + // MARK: - Private Methods + + private func registerFontIfNeeded() { + Self.lock.lock() + defer { Self.lock.unlock() } + + guard !Self.isRegistered else { return } + + if isFontAlreadyAvailable() { + Self.isRegistered = true return } - var error: Unmanaged? - CTFontManagerRegisterFontsForURL(fontURL as CFURL, .process, &error) - print(error ?? "Successfully registered font: \(name)") + do { + try loadFont() + Self.isRegistered = true + } catch { + print("[MaterialDesignSymbol] Font loading error: \(error)") + } + } + + private func isFontAlreadyAvailable() -> Bool { + #if canImport(UIKit) + return !MDSFont.fontNames(forFamilyName: fontName).isEmpty + #elseif canImport(AppKit) + return NSFontManager.shared.availableFonts.contains(fontName) + #endif + } + + private func loadFont() throws { + #if SWIFT_PACKAGE + guard let fontURL = Bundle.module.url(forResource: fontName, withExtension: "ttf") else { + throw FontError.invalidFontFile + } + try registerFont(from: fontURL) + #else + guard let bundle = Bundle(identifier: "org.cocoapods.MaterialDesignSymbol") ?? + Bundle(for: type(of: self) as AnyClass) as Bundle?, + let fontURL = bundle.url(forResource: fontName, withExtension: "ttf") else { + throw FontError.invalidFontFile + } + try registerFont(from: fontURL) #endif } - /** - アイコンをフォント形式で呼び出すのに使うメソッド - - parameter fontSize: フォントサイズ - - returns: UIFont - */ - public func fontOfSize(_ fontSize: CGFloat) -> UIFont { - - // アイコンを呼び出す - if UIFont.fontNames(forFamilyName: name).count == 0 { - do { - try FontLoader.loadFont(name) - } catch FontError.invalidFontFile { - print("invalidFontFile") - } catch FontError.fontPathNotFound { - print("fontPathNotFound") - } catch FontError.initFontError { - print("initFontError") - } catch FontError.registerFailed { - print("registerFailed") - } catch { - + private func registerFont(from url: URL) throws { + var error: Unmanaged? + guard CTFontManagerRegisterFontsForURL(url as CFURL, .process, &error) else { + if let cfError = error?.takeRetainedValue() { + let nsError = cfError as Error + // Ignore "already registered" errors + if (nsError as NSError).code == CTFontManagerError.alreadyRegistered.rawValue { + return + } + throw FontError.registerFailed(underlying: nsError) } + throw FontError.registerFailed(underlying: nil) } + } +} + +// MARK: - Font Error - return UIFont(name: name, size: fontSize)! +/// Errors that can occur during font loading +public enum FontError: Error, Sendable { + case invalidFontFile + case fontPathNotFound + case initFontError + case registerFailed(underlying: Error?) + + public var localizedDescription: String { + switch self { + case .invalidFontFile: + return "Font file not found or invalid" + case .fontPathNotFound: + return "Font path not found" + case .initFontError: + return "Failed to initialize font" + case .registerFailed(let underlying): + if let error = underlying { + return "Font registration failed: \(error.localizedDescription)" + } + return "Font registration failed" + } } } -#endif diff --git a/Sources/MaterialDesignSymbol/MaterialDesignIcon.swift b/Sources/MaterialDesignSymbol/MaterialDesignIcon.swift index 4b73c78..70fd6e3 100644 --- a/Sources/MaterialDesignSymbol/MaterialDesignIcon.swift +++ b/Sources/MaterialDesignSymbol/MaterialDesignIcon.swift @@ -1,17 +1,17 @@ // -// MaterialDesignIcon +// MaterialDesignIcon.swift +// MaterialDesignSymbol // -// Created by tichise on 2015/5/7 15/05/07. +// Created by tichise on 2015/5/7. // Copyright (c) 2015 tichise. All rights reserved. // -#if !os(macOS) -import UIKit +import Foundation -/** - * マテリアルデザインアイコンのコードを返す構造体 - */ -@available(iOS, deprecated: 13.0) +/// Legacy icon struct - use MaterialDesignIconEnum instead +@available(iOS, deprecated: 13.0, message: "Use MaterialDesignIconEnum instead") +@available(macOS, deprecated: 11.0, message: "Use MaterialDesignIconEnum instead") +@available(watchOS, deprecated: 6.0, message: "Use MaterialDesignIconEnum instead") +@available(tvOS, deprecated: 13.0, message: "Use MaterialDesignIconEnum instead") public struct MaterialDesignIcon { } -#endif diff --git a/Sources/MaterialDesignSymbol/MaterialDesignIcon1.swift b/Sources/MaterialDesignSymbol/MaterialDesignIcon1.swift index cf7a48a..43eb940 100644 --- a/Sources/MaterialDesignSymbol/MaterialDesignIcon1.swift +++ b/Sources/MaterialDesignSymbol/MaterialDesignIcon1.swift @@ -5,8 +5,8 @@ // Copyright (c) 2015 tichise. All rights reserved. // -#if !os(macOS) -import UIKit +import Foundation + /** * マテリアルデザインアイコンのコードを返すクラス @@ -412,4 +412,3 @@ extension MaterialDesignIcon { public static let repeat24px = "\u{e78d}" public static let repeat48px = "\u{e78e}" } -#endif diff --git a/Sources/MaterialDesignSymbol/MaterialDesignIcon2.swift b/Sources/MaterialDesignSymbol/MaterialDesignIcon2.swift index 1760460..2dde37a 100644 --- a/Sources/MaterialDesignSymbol/MaterialDesignIcon2.swift +++ b/Sources/MaterialDesignSymbol/MaterialDesignIcon2.swift @@ -5,8 +5,8 @@ // Copyright (c) 2015 tichise. All rights reserved. // -#if !os(macOS) -import UIKit +import Foundation + /** * マテリアルデザインアイコンのコードを返すクラス @@ -412,4 +412,3 @@ extension MaterialDesignIcon { public static let borderClear24px = "\u{e91c}" public static let borderClear48px = "\u{e91d}" } -#endif diff --git a/Sources/MaterialDesignSymbol/MaterialDesignIcon3.swift b/Sources/MaterialDesignSymbol/MaterialDesignIcon3.swift index ed32353..eec94f5 100644 --- a/Sources/MaterialDesignSymbol/MaterialDesignIcon3.swift +++ b/Sources/MaterialDesignSymbol/MaterialDesignIcon3.swift @@ -5,8 +5,8 @@ // Copyright (c) 2015 tichise. All rights reserved. // -#if !os(macOS) -import UIKit +import Foundation + /** * マテリアルデザインアイコンのコードを返すクラス @@ -412,4 +412,3 @@ extension MaterialDesignIcon { public static let flashOn48px = "\u{eaab}" public static let flip24px = "\u{eaac}" } -#endif diff --git a/Sources/MaterialDesignSymbol/MaterialDesignIcon4.swift b/Sources/MaterialDesignSymbol/MaterialDesignIcon4.swift index 4fc5a2a..f274c9f 100644 --- a/Sources/MaterialDesignSymbol/MaterialDesignIcon4.swift +++ b/Sources/MaterialDesignSymbol/MaterialDesignIcon4.swift @@ -5,8 +5,8 @@ // Copyright (c) 2015 tichise. All rights reserved. // -#if !os(macOS) -import UIKit +import Foundation + /** * マテリアルデザインアイコンのコードを返すクラス @@ -412,4 +412,3 @@ extension MaterialDesignIcon { public static let vibration24px = "\u{ec3a}" public static let vibration48px = "\u{ec3b}" } -#endif diff --git a/Sources/MaterialDesignSymbol/MaterialDesignIcon5.swift b/Sources/MaterialDesignSymbol/MaterialDesignIcon5.swift index 6a24594..090b8b3 100644 --- a/Sources/MaterialDesignSymbol/MaterialDesignIcon5.swift +++ b/Sources/MaterialDesignSymbol/MaterialDesignIcon5.swift @@ -5,8 +5,8 @@ // Copyright (c) 2015 tichise. All rights reserved. // -#if !os(macOS) -import UIKit +import Foundation + /** * マテリアルデザインアイコンのコードを返すクラス @@ -78,4 +78,3 @@ extension MaterialDesignIcon { public static let whatshot24px = "\u{ec7b}" public static let whatshot48px = "\u{ec7c}" } -#endif diff --git a/Sources/MaterialDesignSymbol/MaterialDesignIconEnum.swift b/Sources/MaterialDesignSymbol/MaterialDesignIconEnum.swift index d2052a2..0049fc3 100644 --- a/Sources/MaterialDesignSymbol/MaterialDesignIconEnum.swift +++ b/Sources/MaterialDesignSymbol/MaterialDesignIconEnum.swift @@ -1,13 +1,12 @@ // -// MaterialDesignIconEnum +// MaterialDesignIconEnum.swift +// MaterialDesignSymbol // -#if !os(macOS) -import UIKit +import Foundation -/** - * マテリアルデザインアイコンのコードを返すenum - */ +/// Material Design icon codes as enum cases +/// Each case contains the Unicode character for the icon public enum MaterialDesignIconEnum: String { case threeDRotation24px = "\u{e600}" case threeDRotation48px = "\u{e601}" @@ -1675,4 +1674,3 @@ public enum MaterialDesignIconEnum: String { case whatshot24px = "\u{ec7b}" case whatshot48px = "\u{ec7c}" } -#endif diff --git a/Sources/MaterialDesignSymbol/MaterialDesignSymbol.swift b/Sources/MaterialDesignSymbol/MaterialDesignSymbol.swift index fc9cb3a..5d4ccc0 100644 --- a/Sources/MaterialDesignSymbol/MaterialDesignSymbol.swift +++ b/Sources/MaterialDesignSymbol/MaterialDesignSymbol.swift @@ -1,94 +1,151 @@ // +// MaterialDesignSymbol.swift // MaterialDesignSymbol // -// Created by tichise on 2015/5/7 15/05/07. +// Created by tichise on 2015/5/7. // Copyright (c) 2015 tichise. All rights reserved. // -#if !os(macOS) +#if canImport(UIKit) import UIKit +public typealias MDSColor = UIColor +public typealias MDSImage = UIImage +#elseif canImport(AppKit) +import AppKit +public typealias MDSColor = NSColor +public typealias MDSImage = NSImage +#endif -/** - * MaterialDesignSymbolのメインクラス - */ -public class MaterialDesignSymbol { +import CoreGraphics - var text = "" - var fontOfSize: CGFloat = 30 +/// Main class for creating Material Design icon images +public final class MaterialDesignSymbol { - var mutableTextFontAttributes = [NSAttributedString.Key: Any]() - - public init(icon: MaterialDesignIconEnum, size: CGFloat) { - self.text = icon.rawValue - self.fontOfSize = size + // MARK: - Properties - self.mutableTextFontAttributes = [NSAttributedString.Key: Any]() - - if let paragraphStyle = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle { - self.mutableTextFontAttributes[NSAttributedString.Key.paragraphStyle] = paragraphStyle - } + private let text: String + private let fontSize: CGFloat + private var textAttributes: [NSAttributedString.Key: Any] + + // MARK: - Initialization - self.mutableTextFontAttributes[NSAttributedString.Key.font] = MaterialDesignFont.shared.fontOfSize(size) + /// Initialize with an icon enum + /// - Parameters: + /// - icon: The Material Design icon to display + /// - size: Font size for the icon + public init(icon: MaterialDesignIconEnum, size: CGFloat) { + self.text = icon.rawValue + self.fontSize = size + self.textAttributes = Self.makeDefaultAttributes(size: size) } + /// Initialize with a text string (Unicode character) + /// - Parameters: + /// - text: The Unicode string for the icon + /// - size: Font size for the icon public init(text: String, size: CGFloat) { self.text = text - self.fontOfSize = size + self.fontSize = size + self.textAttributes = Self.makeDefaultAttributes(size: size) + } - self.mutableTextFontAttributes = [NSAttributedString.Key: Any]() - - if let paragraphStyle = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle { - self.mutableTextFontAttributes[NSAttributedString.Key.paragraphStyle] = paragraphStyle - } + // MARK: - Attribute Configuration - self.mutableTextFontAttributes[NSAttributedString.Key.font] = MaterialDesignFont.shared.fontOfSize(size) + /// Add a custom attribute + /// - Parameters: + /// - attributeName: The attribute key + /// - value: The attribute value + public func addAttribute(attributeName: NSAttributedString.Key, value: Any) { + textAttributes[attributeName] = value } - // MARK: - Method - public func addAttribute(attributeName: NSAttributedString.Key, value: Any) { - self.mutableTextFontAttributes[attributeName] = value + /// Set the foreground color + /// - Parameter foregroundColor: The color to apply + public func addAttribute(foregroundColor: MDSColor) { + addAttribute(attributeName: .foregroundColor, value: foregroundColor) } - - public func addAttribute(foregroundColor: UIColor) { - addAttribute(attributeName: NSAttributedString.Key.foregroundColor, value: foregroundColor) + + // MARK: - Image Generation + + /// Generate an image with the specified size + /// - Parameter size: The desired image size + /// - Returns: The generated image, or nil if generation failed + public func image(size: CGSize) -> MDSImage? { + #if canImport(UIKit) + return renderUIKitImage(size: size) + #elseif canImport(AppKit) + return renderAppKitImage(size: size) + #endif } - /** - // アイコンを画像形式で取得するのに使うメソッド - - parameter size: サイズ - - returns: UIImage - */ - public func image(size: CGSize) -> UIImage { - UIGraphicsBeginImageContextWithOptions(size, false, 0) + /// Generate an image using the font size as dimensions + /// - Returns: The generated image, or nil if generation failed + public func image() -> MDSImage? { + let size = CGSize(width: fontSize, height: fontSize) + return image(size: size) + } - let textRect = CGRect(x: 0, y: 0, width: size.width, height: size.height) - self.text.draw(in: textRect, withAttributes: self.mutableTextFontAttributes) + // MARK: - Private Methods - let image = UIGraphicsGetImageFromCurrentImageContext() + private static func makeDefaultAttributes(size: CGFloat) -> [NSAttributedString.Key: Any] { + var attributes: [NSAttributedString.Key: Any] = [:] - UIGraphicsEndImageContext() + if let paragraphStyle = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle { + attributes[.paragraphStyle] = paragraphStyle + } - return image! + if let font = MaterialDesignFont.shared.fontOfSize(size) { + attributes[.font] = font + } + + return attributes } - - /** - // アイコンを画像形式で取得するのに使うメソッド - - parameter size: サイズ - - returns: UIImage - */ - public func image() -> UIImage { - let size = CGSize(width: fontOfSize, height: fontOfSize) - - UIGraphicsBeginImageContextWithOptions(size, false, 0) - let textRect = CGRect(x: 0, y: 0, width: size.width, height: size.height) - self.text.draw(in: textRect, withAttributes: self.mutableTextFontAttributes) + #if canImport(UIKit) + private func renderUIKitImage(size: CGSize) -> UIImage? { + let format = UIGraphicsImageRendererFormat() + format.scale = 0 // Use screen scale - let image = UIGraphicsGetImageFromCurrentImageContext() + let renderer = UIGraphicsImageRenderer(size: size, format: format) + return renderer.image { _ in + let rect = CGRect(origin: .zero, size: size) + text.draw(in: rect, withAttributes: textAttributes) + } + } + #endif - UIGraphicsEndImageContext() + #if canImport(AppKit) + private func renderAppKitImage(size: CGSize) -> NSImage? { + let image = NSImage(size: size) + image.lockFocus() - return image! + let rect = CGRect(origin: .zero, size: size) + text.draw(in: rect, withAttributes: textAttributes) + + image.unlockFocus() + return image + } + #endif +} + +// MARK: - Convenience Extensions + +public extension MaterialDesignSymbol { + /// Create and return an image in one call + /// - Parameters: + /// - icon: The Material Design icon + /// - size: The image size + /// - color: Optional foreground color + /// - Returns: The generated image, or nil if generation failed + static func image( + icon: MaterialDesignIconEnum, + size: CGSize, + color: MDSColor? = nil + ) -> MDSImage? { + let symbol = MaterialDesignSymbol(icon: icon, size: size.height) + if let color = color { + symbol.addAttribute(foregroundColor: color) + } + return symbol.image(size: size) } } -#endif diff --git a/Tests/MaterialDesignSymbolTests/MaterialDesignSymbolTests.swift b/Tests/MaterialDesignSymbolTests/MaterialDesignSymbolTests.swift new file mode 100644 index 0000000..765c3d3 --- /dev/null +++ b/Tests/MaterialDesignSymbolTests/MaterialDesignSymbolTests.swift @@ -0,0 +1,159 @@ +import XCTest +@testable import MaterialDesignSymbol + +final class MaterialDesignSymbolTests: XCTestCase { + + // MARK: - Font Tests + + func testFontLoading() { + let font = MaterialDesignFont.shared.fontOfSize(24) + XCTAssertNotNil(font, "Font should load successfully") + } + + func testFontSize() { + let fontSize: CGFloat = 32 + let font = MaterialDesignFont.shared.fontOfSize(fontSize) + XCTAssertNotNil(font) + XCTAssertEqual(font?.pointSize, fontSize) + } + + func testStaticFontMethod() { + let font = MaterialDesignFont.fontOfSize(20) + XCTAssertNotNil(font) + } + + // MARK: - Symbol Initialization Tests + + func testSymbolInitWithEnum() { + let symbol = MaterialDesignSymbol(icon: .home48px, size: 30) + XCTAssertNotNil(symbol) + } + + func testSymbolInitWithText() { + let symbol = MaterialDesignSymbol(text: MaterialDesignIconEnum.home48px.rawValue, size: 30) + XCTAssertNotNil(symbol) + } + + // MARK: - Image Generation Tests + + func testImageGeneration() { + let symbol = MaterialDesignSymbol(icon: .home48px, size: 30) + let image = symbol.image() + + XCTAssertNotNil(image, "Image should be generated") + } + + func testImageGenerationWithSize() { + let symbol = MaterialDesignSymbol(icon: .home48px, size: 30) + let size = CGSize(width: 50, height: 50) + let image = symbol.image(size: size) + + XCTAssertNotNil(image) + #if canImport(UIKit) + XCTAssertEqual(image?.size.width, size.width) + XCTAssertEqual(image?.size.height, size.height) + #endif + } + + func testImageWithColor() { + let symbol = MaterialDesignSymbol(icon: .home48px, size: 30) + #if canImport(UIKit) + symbol.addAttribute(foregroundColor: .red) + #elseif canImport(AppKit) + symbol.addAttribute(foregroundColor: .red) + #endif + let image = symbol.image() + + XCTAssertNotNil(image) + } + + // MARK: - Static Image Generation Tests + + func testStaticImageGeneration() { + let size = CGSize(width: 40, height: 40) + let image = MaterialDesignSymbol.image(icon: .home48px, size: size) + + XCTAssertNotNil(image) + } + + func testStaticImageGenerationWithColor() { + let size = CGSize(width: 40, height: 40) + #if canImport(UIKit) + let color = UIColor.blue + #elseif canImport(AppKit) + let color = NSColor.blue + #endif + let image = MaterialDesignSymbol.image(icon: .home48px, size: size, color: color) + + XCTAssertNotNil(image) + } + + // MARK: - Icon Enum Tests + + func testIconEnumHasRawValue() { + let icon = MaterialDesignIconEnum.home48px + XCTAssertFalse(icon.rawValue.isEmpty) + } + + func testMultipleIcons() { + let icons: [MaterialDesignIconEnum] = [ + .home48px, + .settings48px, + .search48px, + .menu48px + ] + + for icon in icons { + let symbol = MaterialDesignSymbol(icon: icon, size: 24) + let image = symbol.image() + XCTAssertNotNil(image, "Image for \(icon) should be generated") + } + } + + // MARK: - Attribute Tests + + func testAddAttribute() { + let symbol = MaterialDesignSymbol(icon: .home48px, size: 30) + symbol.addAttribute(attributeName: .kern, value: 1.0) + + let image = symbol.image() + XCTAssertNotNil(image) + } + + // MARK: - Thread Safety Tests + + func testConcurrentFontAccess() { + let expectation = XCTestExpectation(description: "Concurrent font access") + expectation.expectedFulfillmentCount = 10 + + let queue = DispatchQueue(label: "test.concurrent", attributes: .concurrent) + + for _ in 0..<10 { + queue.async { + let font = MaterialDesignFont.shared.fontOfSize(24) + XCTAssertNotNil(font) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: 5.0) + } + + func testConcurrentImageGeneration() { + let expectation = XCTestExpectation(description: "Concurrent image generation") + expectation.expectedFulfillmentCount = 10 + + let queue = DispatchQueue(label: "test.concurrent", attributes: .concurrent) + + for i in 0..<10 { + queue.async { + let symbol = MaterialDesignSymbol(icon: .home48px, size: CGFloat(20 + i)) + let image = symbol.image() + XCTAssertNotNil(image) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: 5.0) + } +}