Swift and SwiftUI tutorials for iOS and Swift Developers - Swift Programming

SwiftUI with Core Graphics

Interface design has evolved drastically, but the need to create custom, high-performance visual elements remains a constant for any iOS Developer. While Apple’s declarative framework offers very useful primitives like Circle, Rectangle, or Capsule, sometimes the design demands complex geometric shapes, detailed data charts, or custom animations. This is where the combination of SwiftUI and Core Graphics truly shines.

In this comprehensive tutorial, we will explore what Core Graphics is, how it perfectly integrates into the current declarative paradigm, and how you can use Swift programming in Xcode to draw advanced 2D graphics that work natively on iOS, macOS, and watchOS.


1. What is Core Graphics?

Core Graphics (also known as Quartz 2D) is a powerful two-dimensional drawing framework based on C that has been Apple’s rendering engine for decades. Traditionally associated with UIKit and AppKit, it provides low-level drawing primitives: paths, geometric transformations, gradients, color management, and text rendering.

Although SwiftUI has simplified interface creation, under the hood it still relies on lower-level rendering engines. The beauty of modern Swift programming is that we do not have to abandon the power of Core Graphics; Apple has created elegant bridges to use its concepts within our declarative views.


2. The Fundamental Bridge: Path in SwiftUI

To integrate SwiftUI and Core Graphics, the main tool you will use is the Path structure. A Path in SwiftUI is the direct equivalent and wrapper of a Core Graphics CGPath.

Drawing Basic Shapes with Path

To understand how it works, let’s create a custom triangle. Open Xcode, create a new view in Swift, and use the following code:

import SwiftUI

struct TriangleView: View {
    var body: some View {
        Path { path in
            // 1. Move the "pencil" to the starting point (top center)
            path.move(to: CGPoint(x: 100, y: 10))
            
            // 2. Draw line to the bottom right corner
            path.addLine(to: CGPoint(x: 190, y: 190))
            
            // 3. Draw line to the bottom left corner
            path.addLine(to: CGPoint(x: 10, y: 190))
            
            // 4. Close the path (returns to the starting point)
            path.closeSubpath()
        }
        .fill(Color.blue)
        .frame(width: 200, height: 200)
    }
}

In this Swift block, the Path closure receives a mutable path object. We use classic Core Graphics methods (like move and addLine) using CGPoint coordinates.

Implementing the Shape Protocol

If you want your path to be reusable and adapt to the size of its container (like a native Rectangle does), the best practice for an iOS Developer is to conform to the Shape protocol.

import SwiftUI

struct DiamondShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        // Calculate points based on the available rectangle (rect)
        let top = CGPoint(x: rect.midX, y: rect.minY)
        let right = CGPoint(x: rect.maxX, y: rect.midY)
        let bottom = CGPoint(x: rect.midX, y: rect.maxY)
        let left = CGPoint(x: rect.minX, y: rect.midY)
        
        path.move(to: top)
        path.addLine(to: right)
        path.addLine(to: bottom)
        path.addLine(to: left)
        path.closeSubpath()
        
        return path
    }
}

// Usage in the view:
// DiamondShape().fill(Color.purple).frame(width: 100, height: 150)

3. Bézier Curves and Complex Paths

The true power of Core Graphics lies in creating smooth curves. Bézier curves allow you to create everything from fluid waves to custom icons.

Quadratic vs Cubic Curve

  • Quadratic (addQuadCurve): Uses a single control point to pull the line towards it.
  • Cubic (addCurve): Uses two control points, allowing for “S” shaped curves.

Let’s see how to create a wave shape using Bézier curves in SwiftUI:

import SwiftUI

struct WaveShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        path.move(to: CGPoint(x: rect.minX, y: rect.midY))
        
        // Cubic Bézier Curve
        path.addCurve(
            to: CGPoint(x: rect.maxX, y: rect.midY),
            control1: CGPoint(x: rect.width * 0.25, y: rect.minY),
            control2: CGPoint(x: rect.width * 0.75, y: rect.maxY)
        )
        
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
        path.closeSubpath()
        
        return path
    }
}

4. The Next Level: The Canvas View (Immediate Mode)

Starting with iOS 15 and macOS 12, Apple introduced the Canvas view. If you have ever used draw(_:) in UIKit with CGContext, the Canvas will feel very familiar.

While building views by stacking shapes with ZStack works well for static interfaces, when you need to draw thousands of particles, complex data charts, or perform high-performance rendering, Canvas is the ultimate tool.

The Canvas provides a GraphicsContext, which is a modern, value-safe, and Swift-oriented wrapper of what was classically the CGContext in Core Graphics.

import SwiftUI

struct DataChartView: View {
    let dataPoints: [CGFloat] = [20, 50, 30, 80, 60, 100, 40]
    
    var body: some View {
        Canvas { context, size in
            // 1. Define the drawing space
            let width = size.width / CGFloat(dataPoints.count - 1)
            var path = Path()
            
            // 2. Calculate the points
            for (index, data) in dataPoints.enumerated() {
                let x = width * CGFloat(index)
                // Invert the Y axis because 0,0 is top left
                let y = size.height - (data / 100) * size.height 
                
                if index == 0 {
                    path.move(to: CGPoint(x: x, y: y))
                } else {
                    path.addLine(to: CGPoint(x: x, y: y))
                }
            }
            
            // 3. Render the path in the context
            context.stroke(
                path,
                with: .color(.green),
                style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round)
            )
        }
        .frame(height: 200)
        .padding()
    }
}

Advantages of using Canvas

  • Performance: Avoids creating multiple view nodes in the SwiftUI structural tree. Everything is drawn in a single pass (Immediate Mode).
  • Filters and Effects: The GraphicsContext allows applying blurs, opacities, and blend modes directly to specific elements within the canvas.
  • Text and Image Support: You can draw SF Symbols, text, and Image objects directly at specific coordinates.

5. Cross-Platform Development: iOS, macOS, and watchOS

One of the greatest joys of Swift programming today is the “Learn once, apply everywhere” philosophy.

By using SwiftUI and Core Graphics through Path, Shape, or Canvas, your code inherently becomes cross-platform. Structures like CGPoint, CGSize, and CGRect (which belong to CoreFoundation/CoreGraphics) are available identically in Xcode regardless of the compiler destination.

  • iOS/iPadOS: Excellent for interactive animations, finance charts, and custom UI elements.
  • macOS: Ideal for graphic design tools, large-screen data visualizers, and menu bar utilities.
  • watchOS: Perfect for custom activity rings, Watch Faces, or mini health charts, where performance and low battery consumption are critical. (Note: Canvas is especially useful on watchOS to maintain stable FPS).

6. Integrating CGPath Directly

In case you are migrating an older application or need to use pure Core Graphics algorithms (e.g., path intersection or advanced mathematical calculations provided by C), you can inject a CGPath directly into a SwiftUI Path.

import SwiftUI
import CoreGraphics

struct LegacyGraphicsView: View {
    var body: some View {
        GeometryReader { geometry in
            Path { path in
                // Create a pure mutable CGPath
                let cgPath = CGMutablePath()
                let rect = CGRect(x: 0, y: 0, width: geometry.size.width, height: geometry.size.height)
                
                // Use Core Graphics functions
                cgPath.addEllipse(in: rect)
                
                // Integrate it into SwiftUI
                path.addPath(Path(cgPath))
            }
            .stroke(Color.red, lineWidth: 5)
        }
    }
}

7. Best Practices for the iOS Developer

To get the most out of these technologies, keep the following recommendations in mind:

  • Use Shape instead of static Path: If your graphic needs to adapt to different screen sizes or devices (iPhone vs iPad), wrap your mathematical logic inside the path(in rect: CGRect) function of the Shape protocol.
  • Reserve Canvas for high performance: Do not use Canvas to draw a simple circle or a button. Reserve it for line charts with hundreds of points, particle systems, or drawings where performance is an obvious bottleneck.
  • Geometric Animation: SwiftUI can animate paths automatically if you implement the animatableData property. This allows you to create amazing morphs between two different shapes (e.g., animating a “Play” button transforming into a “Pause” button).

Conclusion

Mastery of SwiftUI and Core Graphics is what separates a junior developer from an advanced iOS Developer. While the standard component catalog will only take you so far, the ability to manipulate CGPoint, Bézier curves, and the rendering Canvas in Swift programming gives you absolute control over the pixels on the screen.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Integrating SwiftUI into UIKit

Related Posts