diff --git a/Sources/YCoreUI/Protocols/ImageAsset.swift b/Sources/YCoreUI/Protocols/ImageAsset.swift index ae209d9..f1dd575 100644 --- a/Sources/YCoreUI/Protocols/ImageAsset.swift +++ b/Sources/YCoreUI/Protocols/ImageAsset.swift @@ -6,7 +6,6 @@ // Copyright © 2022 Y Media Labs. All rights reserved. // -import Foundation import UIKit /// Any named image asset can be loaded from an asset catalog. diff --git a/Sources/YCoreUI/Protocols/SystemImage.swift b/Sources/YCoreUI/Protocols/SystemImage.swift new file mode 100644 index 0000000..2089e84 --- /dev/null +++ b/Sources/YCoreUI/Protocols/SystemImage.swift @@ -0,0 +1,54 @@ +// +// SystemImage.swift +// YCoreUI +// +// Created by Mark Pospesel on 3/8/23. +// Copyright © 2023 Y Media Labs. All rights reserved. +// + +import UIKit + +/// Any string corresponding to a system image (SF Symbols). +/// +/// All properties and functions have default implementations. At a minimum just have your string-based enum conform +/// to `SystemImage`. The raw value of the enum should match a sytem image name (e.g. `checkmark.seal`). +public protocol SystemImage: RawRepresentable where RawValue == String { + /// Fallback image to use in case a system image cannot be loaded. + /// (default is a 16 x 16 square filled with `.systemPink`) + static var fallbackImage: UIImage { get } + + /// A system image for this name value. + /// + /// Default implementation calls `loadImage` and nil-coalesces to `fallbackImage`. + var image: UIImage { get } + + /// Loads the named system image. + /// - Returns: The named system image or else `nil` if the system image cannot be loaded. + func loadImage() -> UIImage? +} + +extension SystemImage { + /// Fallback image to use in case a system image cannot be loaded. + /// (default is a 16 x 16 square filled with `.systemPink`) + public static var fallbackImage: UIImage { + let renderer = UIGraphicsImageRenderer(size: CGSize(width: 16, height: 16)) + let image = renderer.image { ctx in + UIColor.systemPink.setFill() + ctx.fill(CGRect(origin: .zero, size: renderer.format.bounds.size)) + } + return image + } + + /// Loads the named system image. + /// + /// Default implementation uses `UIImage(systemName:)` passing in the associated `rawValue`. + /// - Returns: The named system image or else `nil` if the system image cannot be loaded. + public func loadImage() -> UIImage? { + UIImage(systemName: rawValue) + } + + /// A system image for this name value. + /// + /// Default implementation calls `loadImage` and nil-coalesces to `fallbackImage`. + public var image: UIImage { loadImage() ?? Self.fallbackImage } +} diff --git a/Tests/YCoreUITests/Protocols/SystemImageTests.swift b/Tests/YCoreUITests/Protocols/SystemImageTests.swift new file mode 100644 index 0000000..a476254 --- /dev/null +++ b/Tests/YCoreUITests/Protocols/SystemImageTests.swift @@ -0,0 +1,57 @@ +// +// SystemImageTests.swift +// YCoreUI +// +// Created by Mark Pospesel on 3/8/23. +// Copyright © 2023 Y Media Labs. All rights reserved. +// + +import XCTest +import YCoreUI + +final class SystemImageTests: XCTestCase { + func test_fallbackImage_deliversImage() { + XCTAssertNotNil(Symbols.fallbackImage) + XCTAssertEqual(MissingSymbols.fallbackImage, UIImage(systemName: "x.squareroot")) + } + + func test_loadImage_deliversImage() { + Symbols.allCases.forEach { + XCTAssertNotNil($0.loadImage()) + } + } + + func test_missingImage_deliversCustomFallback() { + MissingSymbols.allCases.forEach { + XCTAssertNil($0.loadImage()) + XCTAssertEqual($0.image, UIImage(systemName: "x.squareroot")) + } + } + + func test_systemImage_deliversDefaultFallback() { + XCTAssertEqual(DefaultSymbols.defaultCase.image.pngData(), DefaultSymbols.fallbackImage.pngData()) + } +} + +extension SystemImageTests { + enum Symbols: String, CaseIterable, SystemImage { + case checked = "checkmark.square" + case unchecked = "square" + case warning = "exclamationmark.triangle.fill" + case error = "exclamationmark.octagon" + } + + enum MissingSymbols: String, CaseIterable, SystemImage { + case notHere + case gone + + static var fallbackImage: UIImage { + let image: UIImage! = UIImage(systemName: "x.squareroot") + return image + } + } + + enum DefaultSymbols: String, CaseIterable, SystemImage { + case defaultCase + } +}