While it’s possible to use an SVG asset inside the Xcode project (since Xcode 12), we can only use it as a UIImage. So what about converting a SVG to a SwiftUI Shape?

Lot of ISBN

Preface

Directly drawing forms (versus using static assets) is becoming a common practice since SwiftUI is making our life much easier. Before that, we had to use the drawRect method. And while the performances and possibilities are endless, the coding is quite grumpy, making it our last resort with UIKit projects.

As stated above, you can import a SVG file inside your Xcode project and use Image(named: "my-file.svg") inside your SwiftUI project. But you may want to use the capabilities of a SwiftUI Shape too (animate points, fill it, etc.).

There is only one way, and it’s to manually convert your SVG to SwiftUI code. However, do not fear, a tool exists since ancient times to help us in the process.

Step 1: get your SVG and PaintCode

Let’s start by downloading PaintCode. Unfortunately the licence price sucks (currently at 200$ / user / year), I think there was a one-time purchase at some point.

PaintCode allows you to convert any custom drawing (or SVG) into static assets, AND code. While it’s currently Swift code, it’s not very difficult to convert it into SwiftUI code once most of the difficult work has been done by the software. As you can see below, the export options aren’t limited to iOS projects.

Lot of ISBN

Export options in PaintCode

And obviously, get your hand on the SVG you’re interested in.
In this article, we’ll use a cloud asset.

Lot of ISBN

The cloud we want to convert

Step 2: adjust the SVG within PaintCode

The “tricky” part happens here: to make it usable as a “path”, we need to have relative values (e.g. positions and sizes) for the exported code.

Let’s remind us how the Shape protocol works with SwiftUI. We have to implement the method func path(in rect: CGRect) -> Path, and so create a form within the rect parameter. Meaning that we should create lines and curves based on a dynamic value, and not based on a static frame (e.g. 100x200px square).

1
2
3
4
5
6
7
8
9
public protocol Shape : Animatable, View {

    /// Describes this shape as a path within a rectangular frame of reference.
    ///
    /// - Parameter rect: The frame of reference for describing this shape.
    ///
    /// - Returns: A path that describes this shape.
    func path(in rect: CGRect) -> Path
}

Said differently, we shouldn’t export code such as path.move(to: CGPoint(x: 10, y: 20)), but we should have instead something like path.move(to: CGPoint(x: rect.minX + rect.width * 0.1, y: rect.minY + rect.height * 0.2)). Yes yes, there might be a way (at least with UIKit) by calculating the difference between “from” frame and “real” frame and applying a scaling effect, but yes honestly… no.

a. Import the SVG file and clip it around the canvas.

Make sure the shape (here the blue cloud) fits inside the canvas (the white zone). You likely will have to resize the canvas, either with the mouse (click and resize from its bottom-right corner), or from the menu (inside the right column).

Use this occasion to properly name the canvas (here “cloud”) and the form (here “mainShape”). This may have an impact on exported code and asset.

Lot of ISBN

Cloud before adjusting the canvas size

Lot of ISBN

Cloud after adjusting the canvas size

If you export the code right now, you’ll get some hard-coded frame, which isn’t our desired goal. PaintCode includes for UIKit a ResizingBehavior logic to get around this problem. But honestly I highly recommend avoiding this kind of trick for SwiftUI. So let’s go on!

b. Add a “frame” around the shape.

Adding a frame around the shape will allow us to specify how it should behave in a dynamic way (like ping edge to edge).

  • Click the “frame” symbol in the navigation bar.
  • Add a frame with the same position and size as the canvas.

You should get something similar to this in the right column.

Lot of ISBN

The frame has been added

c. Adjust the “pin” conditions.

Since you have added the frame, you can now edit the “pin” conditions in the menu. Make sure to have them as in the image below, so the shape will keep its edges pinned to the parent view (the canvas).

Lot of ISBN

Adjust the constraints

Step 3: export the code and convert it to SwiftUI shape

If everything went smoothly, you’re ready to export the cloud to some UIKit code. We’re not done yet as we still need to convert it to some SwiftUI code as PaintCode hasn’t an export option for this (yet).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import UIKit

public class StyleKitName : NSObject {

    //// Drawing Methods

    @objc dynamic public class func drawCloud(frame: CGRect = CGRect(x: 0, y: 0, width: 618, height: 383)) {
        //// Color Declarations
        let fillColor = UIColor(red: 0.639, green: 0.831, blue: 0.969, alpha: 1.000)

        //// mainShape Drawing
        let mainShapePath = UIBezierPath()
        mainShapePath.move(to: CGPoint(x: frame.minX + 0.81028 * frame.width, y: frame.minY + 0.39009 * frame.height))
        mainShapePath.addCurve(to: CGPoint(x: frame.minX + 0.77271 * frame.width, y: frame.minY + 0.39612 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.79766 * frame.width, y: frame.minY + 0.39009 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.78507 * frame.width, y: frame.minY + 0.39210 * frame.height))
        mainShapePath.addCurve(to: CGPoint(x: frame.minX + 0.69093 * frame.width, y: frame.minY + 0.22300 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.76339 * frame.width, y: frame.minY + 0.32133 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.73325 * frame.width, y: frame.minY + 0.25753 * frame.height))
        mainShapePath.addCurve(to: CGPoint(x: frame.minX + 0.55572 * frame.width, y: frame.minY + 0.21906 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.64862 * frame.width, y: frame.minY + 0.18847 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.59879 * frame.width, y: frame.minY + 0.18702 * frame.height))
        mainShapePath.addCurve(to: CGPoint(x: frame.minX + 0.29076 * frame.width, y: frame.minY + 0.02004 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.51674 * frame.width, y: frame.minY + 0.04650 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.39812 * frame.width, y: frame.minY + -0.04260 * frame.height))
        mainShapePath.addCurve(to: CGPoint(x: frame.minX + 0.16694 * frame.width, y: frame.minY + 0.44594 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.18340 * frame.width, y: frame.minY + 0.08269 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.12797 * frame.width, y: frame.minY + 0.27339 * frame.height))
        mainShapePath.addCurve(to: CGPoint(x: frame.minX + 0.00002 * frame.width, y: frame.minY + 0.72706 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.07290 * frame.width, y: frame.minY + 0.45072 * frame.height), controlPoint2: CGPoint(x: frame.minX + -0.00139 * frame.width, y: frame.minY + 0.57584 * frame.height))
        mainShapePath.addCurve(to: CGPoint(x: frame.minX + 0.17212 * frame.width, y: frame.minY + 1.00000 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.00143 * frame.width, y: frame.minY + 0.87829 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.07803 * frame.width, y: frame.minY + 0.99976 * frame.height))
        mainShapePath.addLine(to: CGPoint(x: frame.minX + 0.81028 * frame.width, y: frame.minY + 1.00000 * frame.height))
        mainShapePath.addCurve(to: CGPoint(x: frame.minX + 1.00000 * frame.width, y: frame.minY + 0.69504 * frame.height), controlPoint1: CGPoint(x: frame.minX + 0.91505 * frame.width, y: frame.minY + 1.00000 * frame.height), controlPoint2: CGPoint(x: frame.minX + 1.00000 * frame.width, y: frame.minY + 0.86347 * frame.height))
        mainShapePath.addCurve(to: CGPoint(x: frame.minX + 0.81028 * frame.width, y: frame.minY + 0.39009 * frame.height), controlPoint1: CGPoint(x: frame.minX + 1.00000 * frame.width, y: frame.minY + 0.52662 * frame.height), controlPoint2: CGPoint(x: frame.minX + 0.91505 * frame.width, y: frame.minY + 0.39009 * frame.height))
        mainShapePath.close()
        fillColor.setFill()
        mainShapePath.fill()
    }

}

Final steps:

  • There are a few differences between UIBezierPath (UIKit) methods and Path (SwiftUI) methods. So you’ll have to open your preferred text editor and do some “find/replace”.
  • Remove any fill or similar method as we don’t really care to have them inside our custom Shape.

The final code should be similar to this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import SwiftUI

struct CloudShape: Shape {
        
    func path(in rect: CGRect) -> Path {

        var path = Path()
        path.move(to: CGPoint(x: rect.minX + 0.81028 * rect.width, y: rect.minY + 0.39009 * rect.height))
        path.addCurve(to: CGPoint(x: rect.minX + 0.77271 * rect.width, y: rect.minY + 0.39612 * rect.height), control1: CGPoint(x: rect.minX + 0.79766 * rect.width, y: rect.minY + 0.39009 * rect.height), control2: CGPoint(x: rect.minX + 0.78507 * rect.width, y: rect.minY + 0.39210 * rect.height))
        path.addCurve(to: CGPoint(x: rect.minX + 0.69093 * rect.width, y: rect.minY + 0.22300 * rect.height), control1: CGPoint(x: rect.minX + 0.76339 * rect.width, y: rect.minY + 0.32133 * rect.height), control2: CGPoint(x: rect.minX + 0.73325 * rect.width, y: rect.minY + 0.25753 * rect.height))
        path.addCurve(to: CGPoint(x: rect.minX + 0.55572 * rect.width, y: rect.minY + 0.21906 * rect.height), control1: CGPoint(x: rect.minX + 0.64862 * rect.width, y: rect.minY + 0.18847 * rect.height), control2: CGPoint(x: rect.minX + 0.59879 * rect.width, y: rect.minY + 0.18702 * rect.height))
        path.addCurve(to: CGPoint(x: rect.minX + 0.29076 * rect.width, y: rect.minY + 0.02004 * rect.height), control1: CGPoint(x: rect.minX + 0.51674 * rect.width, y: rect.minY + 0.04650 * rect.height), control2: CGPoint(x: rect.minX + 0.39812 * rect.width, y: rect.minY + -0.04260 * rect.height))
        path.addCurve(to: CGPoint(x: rect.minX + 0.16694 * rect.width, y: rect.minY + 0.44594 * rect.height), control1: CGPoint(x: rect.minX + 0.18340 * rect.width, y: rect.minY + 0.08269 * rect.height), control2: CGPoint(x: rect.minX + 0.12797 * rect.width, y: rect.minY + 0.27339 * rect.height))
        path.addCurve(to: CGPoint(x: rect.minX + 0.00002 * rect.width, y: rect.minY + 0.72706 * rect.height), control1: CGPoint(x: rect.minX + 0.07290 * rect.width, y: rect.minY + 0.45072 * rect.height), control2: CGPoint(x: rect.minX + -0.00139 * rect.width, y: rect.minY + 0.57584 * rect.height))
        path.addCurve(to: CGPoint(x: rect.minX + 0.17212 * rect.width, y: rect.minY + 1.00000 * rect.height), control1: CGPoint(x: rect.minX + 0.00143 * rect.width, y: rect.minY + 0.87829 * rect.height), control2: CGPoint(x: rect.minX + 0.07803 * rect.width, y: rect.minY + 0.99976 * rect.height))
        path.addLine(to: CGPoint(x: rect.minX + 0.81028 * rect.width, y: rect.minY + 1.00000 * rect.height))
        path.addCurve(to: CGPoint(x: rect.minX + 1.00000 * rect.width, y: rect.minY + 0.69504 * rect.height), control1: CGPoint(x: rect.minX + 0.91505 * rect.width, y: rect.minY + 1.00000 * rect.height), control2: CGPoint(x: rect.minX + 1.00000 * rect.width, y: rect.minY + 0.86347 * rect.height))
        path.addCurve(to: CGPoint(x: rect.minX + 0.81028 * rect.width, y: rect.minY + 0.39009 * rect.height), control1: CGPoint(x: rect.minX + 1.00000 * rect.width, y: rect.minY + 0.52662 * rect.height), control2: CGPoint(x: rect.minX + 0.91505 * rect.width, y: rect.minY + 0.39009 * rect.height))
        path.closeSubpath()
        return path
        
    }
    
}

Final words

  • Once you have done it one time, the process can be quickly duplicated for more assets.
  • This method streches the asset inside the frame you’ll set in SwiftUI. It’s up to you to determine the propre size (i.e. you may keep the real width/ratio as a property).