Skip to content
Merged
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: 2 additions & 1 deletion Examples/Sources/GalleryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ struct GalleryView: View {
LazyVStack(spacing: 20) {
ForEach(images, id: \.self) { image in
SVGView(image, bundle: .samples)
.aspectRatio(contentMode: .fit)
.resizable()
.scaledToFit()
.padding([.leading, .trailing], 10)
}
}
Expand Down
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,22 @@ imageView.image = svg.rasterize() // 240x200

### SwiftUI

Display an image within `SVGView`:
SVGs can be displayed within `SVGView` just like using SwiftUI's built-in `Image`:

```swift
var body: some View {
SVGView("sample.svg")
.aspectRatio(contentMode: .fit)
.padding()
}
SVGView("sample.svg")
```

By default, SVGs are rendered at their original (intrinsic) size. To make them flexible within layouts, mark them as resizable — exactly like `Image`:

```swift
SVGView("sample.svg")
.resizable()
.scaledToFit()
```

This allows the SVG to scale proportionally to fit within its container. Use `.scaledToFill()` to completely cover the container and use `.resizable(resizingMode: .tile)` to draw the SVG in repeating tiles filling the available space.

When you load by name, SVGView uses an internal cache so repeated lookups are efficient.
For more predictable performance (avoiding any cache lookup or parsing), you can pass an already-created SVG instance:

Expand Down
32 changes: 27 additions & 5 deletions SwiftDraw/Sources/SVG+CoreGraphics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,44 @@ public import Foundation

public extension CGContext {

func draw(_ image: SVG, in rect: CGRect? = nil) {
let defaultRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
func draw(_ svg: SVG, in rect: CGRect? = nil) {
let defaultRect = CGRect(x: 0, y: 0, width: svg.size.width, height: svg.size.height)
let renderer = CGRenderer(context: self)
saveGState()

if let rect = rect, rect != defaultRect {
translateBy(x: rect.origin.x, y: rect.origin.y)
scaleBy(
x: rect.width / image.size.width,
y: rect.height / image.size.height
x: rect.width / svg.size.width,
y: rect.height / svg.size.height
)
}
renderer.perform(image.commands)
renderer.perform(svg.commands)

restoreGState()
}

func draw(_ svg: SVG, in rect: CGRect, byTiling: Bool) {
guard byTiling else {
draw(svg, in: rect)
return
}

let cols = Int(ceil(rect.size.width / svg.size.width))
let rows = Int(ceil(rect.size.height / svg.size.height))

for r in 0..<rows {
for c in 0..<cols {
let tile = CGRect(
x: rect.minX + CGFloat(r) * svg.size.width,
y: rect.minY + CGFloat(c) * svg.size.height,
width: svg.size.width,
height: svg.size.height
)
draw(svg, in: tile)
}
}
}
}

public extension SVG {
Expand Down
86 changes: 76 additions & 10 deletions SwiftDraw/Sources/SVGView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,28 +44,94 @@ public struct SVGView: View {
}

private let svg: SVG?
private var resizingMode: ResizingMode?

public var body: some View {
if let svg {
Canvas(
opaque: false,
colorMode: .linear,
rendersAsynchronously: false
) { ctx, size in
ctx.draw(svg, in: CGRect(origin: .zero, size: size))
}
.frame(idealWidth: svg.size.width, idealHeight: svg.size.height)
if let resizingMode {
SVGView.makeCanvas(svg: svg, resizingMode: resizingMode)
.frame(idealWidth: svg.size.width, idealHeight: svg.size.height)
} else {
SVGView.makeCanvas(svg: svg, resizingMode: .stretch)
.frame(width: svg.size.width, height: svg.size.height)
}
}
}

public enum ResizingMode: Sendable, Hashable {
/// A mode to repeat the image at its original size, as many
/// times as necessary to fill the available space.
case tile

/// A mode to enlarge or reduce the size of an image so that it
/// fills the available space.
case stretch
}

/// Sets the mode by which SwiftUI resizes an SVG to fit its space.
/// - Parameters:
/// - resizingMode: The mode by which SwiftUI resizes the image.
/// - Returns: An SVGView, with the new resizing behavior set.
public func resizable(resizingMode: ResizingMode = .stretch) -> Self {
var copy = self
copy.resizingMode = resizingMode
return copy
}

private static func makeCanvas(svg: SVG, resizingMode: ResizingMode) -> some View {
Canvas(
opaque: false,
colorMode: .linear,
rendersAsynchronously: false
) { ctx, size in
switch resizingMode {
case .tile:
ctx.draw(svg, in: CGRect(origin: .zero, size: size), byTiling: true)
case .stretch:
ctx.draw(svg, in: CGRect(origin: .zero, size: size))
}
}
}
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public extension GraphicsContext {

func draw(_ image: SVG, in rect: CGRect? = nil) {
func draw(_ svg: SVG, in rect: CGRect? = nil) {
withCGContext {
$0.draw(svg, in: rect)
}
}

func draw(_ svg: SVG, in rect: CGRect, byTiling: Bool) {
withCGContext {
$0.draw(image, in: rect)
$0.draw(svg, in: rect, byTiling: byTiling)
}
}
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
#Preview {
SVGView(svg: .circle)

SVGView(svg: .circle)
.resizable(resizingMode: .stretch)

SVGView(svg: .circle)
.resizable(resizingMode: .tile)
}

#if DEBUG
private extension SVG {

static var circle: SVG {
SVG(xml: """
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="50" fill="orange" />
</svg>
""")!
}
}
#endif

#endif