Dependency-free SVG parsing and rasterization for Apple platforms, with iOS and macOS example apps.
Features · Installation · Quick Start · API Surface · Documentation · Supported SVG Subset · Performance Guardrails · Planned · Package Layout · Example Apps · Testing
- dependency-free SVG detection, parsing, and rasterization
- public-SDK-safe implementation with no Apple private framework usage
- focused
VectorImageCoretarget for loading and rendering SVGs intoUIImageorNSImage - real
VectorImageUItarget with a SwiftUI async SVG image view backed byVectorImageCore - documented supported SVG subset with diagnostics for unsupported features
- support for local, file-based, and remote SVG loading
- optional in-memory caching for repeated source-based renders
- in-flight coalescing for identical source-based render requests
- SwiftUI environment configuration for sharing source-rendering policy across async image views
- support for practical SVG fidelity features such as clip paths, group transforms, simple nested SVG containers, gradients, arc commands, focused stylesheet rules, visibility handling, and stroke presentation attributes
- placeholder
VectorImageAdvancedtarget reserved for future expansion - fixture-based tests for the initial SVG subset
- included iOS and macOS example apps for manual validation
Add VectorImage to your Swift Package Manager dependencies:
dependencies: [
.package(url: "https://github.com/AltiAntonov/VectorImage.git", from: "0.8.0")
]Then add the core product to your target:
.target(
name: "YourApp",
dependencies: [
.product(name: "VectorImageCore", package: "VectorImage")
]
)For SwiftUI integration, add the UI product as well:
.target(
name: "YourFeature",
dependencies: [
.product(name: "VectorImageCore", package: "VectorImage"),
.product(name: "VectorImageUI", package: "VectorImage")
]
)import UIKit
import VectorImageCore
let data: Data = ...
let result = try VectorImageRenderer.render(
svgData: data,
options: .init(size: CGSize(width: 120, height: 120))
)
let image = result.imageIf you want to preflight content before rendering:
let isSVG = VectorImageDetector.isSVG(data: data)For SwiftUI integration, VectorImageUI now provides VectorImageAsyncImage on top of the same core renderer.
import SwiftUI
import VectorImageUI
struct LogoView: View {
let url: URL
var body: some View {
VectorImageAsyncImage(
url: url,
options: .init(size: CGSize(width: 120, height: 120))
) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fit)
} else if let error = phase.error {
Text(error.localizedDescription)
} else {
ProgressView()
}
}
}
}Current public entry points in VectorImageCore:
VectorImageDetector.isSVG(data:)Quick preflight check for raw bytes.VectorImageRenderer.renderImage(svgData:options:)Convenience helper that returns only the rendered image for raw SVG data.VectorImageRenderer.render(svgData:options:)Render raw SVGDatainto an image plus diagnostics.VectorImageRenderer.renderImage(from:loader:options:cache:)Convenience helper that returns only the rendered image for aVectorImageSource.VectorImageRenderer.render(from:loader:options:cache:)Async rendering from aVectorImageSourceinto an image plus diagnostics.VectorImageRenderer.renderImage(from:configuration:options:)Convenience helper that returns only the rendered image using source-rendering configuration.VectorImageRenderer.render(from:configuration:options:)Async rendering from aVectorImageSourceusing source-rendering configuration.VectorImageCacheOptional in-memory cache for repeated source-based renders.VectorImageConfigurationSource-rendering configuration for loader, cache, and in-flight request policies.VectorImageCachePolicyEnables or disables completed-result caching for source-based renders.VectorImageInFlightRequestPolicyEnables or disables coalescing for identical in-flight source-based renders.
Current public entry points in VectorImageUI:
VectorImageAsyncImageSwiftUI async SVG view backed byVectorImageCore. Default loader/cache initializers readEnvironmentValues.vectorImageConfiguration; explicit loader, cache, or configuration initializers keep using the values passed to that view.VectorImageAsyncImagePhasePhase enum for loading, success, and failure states.VectorImageAsyncImageValueSuccessful render payload withImage, platform image, and diagnostics.EnvironmentValues.vectorImageConfigurationDefaultVectorImageConfigurationused by descendantVectorImageAsyncImageviews.View.vectorImageConfiguration(_:)SwiftUI modifier for setting shared source-rendering configuration.
Use the API in two layers:
render(...)The full-result API. Use this when you want diagnostics, warnings, or the clearest picture of what the renderer did.renderImage(...)The convenience API. Use this when you only need the rasterized image and do not care about diagnostics.
The methods are also paired across two input styles:
svgDataUse this when you already have the SVG payload in memory.from sourceUse this when you want the renderer to resolve.data,.fileURL, or.remoteURLfor you.
import UIKit
import VectorImageCore
let data: Data = ...
let isSVG = VectorImageDetector.isSVG(data: data)
guard isSVG else {
throw VectorImageError.notSVG
}
let result = try VectorImageRenderer.render(
svgData: data,
options: .init(size: CGSize(width: 120, height: 120))
)
let image = result.image
let warnings = result.diagnostics.warningsIf you only want the image:
let image = try VectorImageRenderer.renderImage(
svgData: data,
options: .init(size: CGSize(width: 120, height: 120))
)import UIKit
import VectorImageCore
let source: VectorImageSource = .remoteURL(
URL(string: "https://example.com/logo.svg")!
)
let cache = VectorImageCache(countLimit: 64)
let result = try await VectorImageRenderer.render(
from: source,
options: .init(size: CGSize(width: 120, height: 120)),
cache: cache
)
let image = result.image
let warnings = result.diagnostics.warningsThe source-based API keeps loader public so clients can inject a custom URLSession, loader policy, or test double when needed. In the common case, the default loader is enough and you can omit it.
If you only want the image:
let image = try await VectorImageRenderer.renderImage(
from: source,
options: .init(size: CGSize(width: 120, height: 120)),
cache: cache
)import Foundation
import VectorImageCore
let configuration = VectorImageConfiguration(
loader: VectorImageLoader(session: .shared),
cachePolicy: .enabled(countLimit: 64),
inFlightRequestPolicy: .coalesceIdenticalRequests
)
let result = try await VectorImageRenderer.render(
from: .remoteURL(URL(string: "https://example.com/logo.svg")!),
configuration: configuration,
options: .init(size: CGSize(width: 120, height: 120))
)Use .disabled for cachePolicy when the same source may return changing SVG content and you always want a fresh completed render. Use .disabled for inFlightRequestPolicy when identical concurrent requests must not share the same in-flight load/render task.
Supported VectorImageSource cases:
.data(Data).fileURL(URL).remoteURL(URL)
import VectorImageCore
let data: Data = ...
let source: VectorImageSource = .data(data)import Foundation
import VectorImageCore
let fileURL = Bundle.main.url(forResource: "logo", withExtension: "svg")!
let source: VectorImageSource = .fileURL(fileURL)import Foundation
import VectorImageCore
let url = URL(string: "https://example.com/logo.svg")!
let source: VectorImageSource = .remoteURL(url)Current VectorImageRasterizationOptions parameters:
sizescalecontentModeSupported values:.fit,.fill,.stretchopaquebackgroundColor
import CoreGraphics
import VectorImageCore
let options = VectorImageRasterizationOptions(
size: CGSize(width: 160, height: 80),
scale: 2,
contentMode: .fit,
opaque: false,
backgroundColor: VectorImageColor(
red: 1,
green: 1,
blue: 1,
alpha: 1
)
)Not currently provided as first-class package APIs:
- asset-catalog lookup by image name
- SF Symbols /
systemImage
Those remain intentionally outside the first-class package surface so the public API stays focused on SVG loading and rendering rather than general app resource lookup.
import VectorImageCore
let cache = VectorImageCache(countLimit: 64)
let result = try await VectorImageRenderer.render(
from: .remoteURL(URL(string: "https://example.com/logo.svg")!),
options: .init(size: CGSize(width: 120, height: 120)),
cache: cache
)
let image = result.imagePass nil to disable caching for source-based renders when you want to inspect fetch or memory behavior directly.
Source-based rendering also coalesces identical in-flight requests. If two callers ask for the same VectorImageSource with the same VectorImageRasterizationOptions and loader identity while the first render is still running, they await the same render task instead of starting duplicate network or file work. This is separate from VectorImageCache: coalescing prevents duplicate concurrent work, while caching stores completed render results for later calls.
The coalescing key includes the loader identity, so custom VectorImageLoader instances with different URLSession policies are not merged together.
import SwiftUI
import VectorImageCore
import VectorImageUI
struct LogoListView: View {
let urls: [URL]
private let configuration = VectorImageConfiguration(
cachePolicy: .enabled(countLimit: 64),
inFlightRequestPolicy: .coalesceIdenticalRequests
)
var body: some View {
List(urls, id: \.self) { url in
VectorImageAsyncImage(
url: url,
options: .init(size: CGSize(width: 96, height: 96))
) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fit)
} else {
ProgressView()
}
}
}
.vectorImageConfiguration(configuration)
}
}Use reloadID when the source is the same but the host app needs to force a reload, for example after a manual refresh action:
@State private var reloadID = 0
VectorImageAsyncImage(
url: url,
reloadID: reloadID
) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fit)
} else {
ProgressView()
}
}
Button("Reload") {
reloadID += 1
}The package includes DocC catalogs for both public library layers.
- In Xcode, open the package and build documentation for
VectorImageCoreorVectorImageUI. - In Swift Package Index,
.spi.ymlis configured so hosted documentation includes both public modules.
VectorImageCore is intentionally focused rather than aiming to be a full browser-grade SVG engine.
Currently supported:
svg,rect,circle,ellipse,line,polyline,polygon,path,g,defs,clipPath,linearGradient,radialGradient,stop- basic
fill,stroke,stroke-width,opacity,fill-opacity,stroke-opacity - stroke presentation attributes:
stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-dasharray,stroke-dashoffset - inline
style="..." - focused
<style>block rules for practical class, id, and element selectors currentColorresolved from inherited SVGcolorattributes- visibility controls for supported render nodes:
display="none",visibility="hidden",visibility="collapse" fill-rule="evenodd"- simple shape and group transforms:
translate,scale,rotate,skewX,skewY,matrix - simple nested
svgcontainers with inherited presentation attributes andx/yoffsets - clip-path references used by supported grouped assets
- deferred
defsresolution for supported clip paths and gradients - basic linear and radial gradient fills used by the supported subset
Currently not supported:
- masks
- filters
use- text nodes
- full CSS selector support
- the full SVG specification
Unsupported features should fail safely and surface diagnostics rather than crashing.
The package is intended to stay lightweight and predictable for app integration.
Current guardrails for VectorImageCore:
- parsing and rasterization should be safe to run off the main thread
- repeated renders should not show unbounded resident memory growth
- representative small fixtures should stay within baseline render-time budgets
- unsupported features should produce diagnostics rather than expensive fallback behaviour
The test suite includes baseline performance checks for:
- average render time of a simple fixture
- average render time of a representative compound fixture
- approximate resident memory growth during repeated rendering
These are baseline guardrails, not strict cross-machine benchmarks.
This section tracks what is already included in 0.1.0 and what is planned on the road to 1.0.
- Dependency-free SVG detection, parsing, and rasterization
- Public-SDK-safe implementation with no private Apple SVG framework usage
- iOS 15 minimum deployment target
- macOS 12 minimum deployment target for
VectorImageCore - Support for
Data, file URLs, and remote URLs - Async source-based render helpers
- Support for a focused SVG subset used by current fixtures
- Support for inline
styleattributes - Support for simple shape and group transforms:
translate,scale,matrix - Support for translated clip paths used by representative fixtures
- Support for basic linear and radial gradient fills used by current fixtures
- Support for
fill-rule="evenodd" - Diagnostics for unsupported SVG features
- Example iOS app with inline, asset, and public remote SVG samples
- Regression tests for representative fixtures
- Baseline performance guardrails for render time and memory growth
- Placeholder
VectorImageAdvancedandVectorImageUImodules reserved for future work
- Real
VectorImageUImodule instead of a placeholder - SwiftUI async image view for SVG sources
- Core in-flight coalescing for identical source-based render requests
- Source-rendering configuration object
- Configurable completed-result cache policy
- Configurable in-flight request coalescing policy
- Configuration support in
VectorImageUI - Tests for cache and coalescing policy behavior
- SwiftUI environment configuration for
VectorImageAsyncImage - Explicit reload trigger through
reloadID - iOS example app using shared environment configuration
- Swift Testing coverage for
VectorImageUIenvironment behavior
- Focused SVG
<style>block support - Practical class selector support for exported SVGs using
.cls-*rules - CSS id and element selector support for presentation attributes
-
currentColorresolution from inherited SVGcolorattributes - Regression tests for stylesheet precedence and comments
- Supported-subset documentation updated for stylesheet behavior
-
stroke-linecap -
stroke-linejoin -
stroke-miterlimit -
stroke-dasharray -
stroke-dashoffset - Regression tests for parsed and rendered stroke presentation behavior
- Root
svgpresentation-attribute inheritance -
display="none"and stylesheet-backed hidden-node handling -
visibility="hidden"andvisibility="collapse"handling for supported nodes - Simple nested
svgcontainer support withx/yoffsets - Regression tests for hidden elements, hidden groups, root inheritance, and nested containers
-
rotate(angle)transform support -
rotate(angle cx cy)transform support -
skewX(angle)andskewY(angle)transform support - Regression tests for the new transform geometry
- Additional hardening releases between
0.8.0and1.0.0if the package needs them - Focused feature additions driven by real host-app needs
- Stable public API review
- Production adoption validation in a host app
- Decide the long-term role of
VectorImageAdvanced - Confidence that the documented supported SVG subset is stable enough for long-term maintenance
VectorImageCoreThe real implementation target. Contains SVG detection, parsing, diagnostics, and rasterization intoUIImageorNSImage.VectorImageAdvancedPlaceholder target for richer SVG feature support in later versions.VectorImageUISwiftUI integration target with async SVG image loading built onVectorImageCore.
The repository includes two example applications:
Example/VectorImageExampleiOS sample appExample/VectorImageMacExamplemacOS sample app
Current demo coverage:
- local package integration for
VectorImageCore,VectorImageAdvanced, andVectorImageUI - iOS demo coverage for
VectorImageAsyncImageusing inline SVGs and public remote URLs - iOS demo coverage for shared
VectorImageUIenvironment configuration - rendered sample SVG cards using bundled asset-catalog SVGs and public remote URLs
- diagnostics display for unsupported SVG features that are outside the documented subset
- startup toggle for cache behavior using
--vectorimage-disable-cache
Run the package tests with:
swift test