Skip to content
advanced Phase 14 · Advanced iOS

Advanced SwiftUI

Use Canvas, custom shapes, Path, matchedGeometryEffect, and advanced layout techniques.

1h
3 problems
Topic Progress 0%

Canvas and Shape

Canvas View

Canvas provides high-performance 2D drawing similar to UIKit CGContext but declarative:

struct GraphView: View {
    let dataPoints: [CGFloat]
    var body: some View {
        Canvas { context, size in
            let width = size.width / CGFloat(dataPoints.count - 1)
            var path = Path()
            for (index, value) in dataPoints.enumerated() {
                let point = CGPoint(x: CGFloat(index) * width, y: size.height * (1 - value))
                if index == 0 { path.move(to: point) }
                else { path.addLine(to: point) }
            }
            context.stroke(path, with: .color(.blue), lineWidth: 2)
        }
    }
}

Custom Shapes with Shape Protocol

Create reusable vector shapes:

struct StarShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let center = CGPoint(x: rect.midX, y: rect.midY)
        let outerRadius = min(rect.width, rect.height) / 2
        let innerRadius = outerRadius * 0.4
        for i in 0..<10 {
            let angle = Double(i) * .pi / 5 - .pi / 2
            let radius = i.isMultiple(of: 2) ? outerRadius : innerRadius
            let point = CGPoint(x: center.x + CGFloat(cos(angle)) * radius, y: center.y + CGFloat(sin(angle)) * radius)
            if i == 0 { path.move(to: point) } else { path.addLine(to: point) }
        }
        path.closeSubpath()
        return path
    }
}

Animatable Shapes

Use animatableData for smooth shape transitions:

struct AnimatedArc: Shape {
    var progress: CGFloat
    var animatableData: CGFloat {
        get { progress }
        set { progress = newValue }
    }
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width / 2, startAngle: .degrees(0), endAngle: .degrees(progress * 360), clockwise: false)
        return path
    }
}

Advanced Layout

Custom Layout Protocol

SwiftUI Layout protocol creates custom layout containers:

struct WaterfallLayout: Layout {
    var columns: Int
    var spacing: CGFloat
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let result = layout(proposal: proposal, subviews: subviews)
        return result.size
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let result = layout(proposal: proposal, subviews: subviews)
        for (index, subview) in subviews.enumerated() {
            subview.place(at: CGPoint(x: bounds.minX + result.positions[index].x, y: bounds.minY + result.positions[index].y), proposal: .unspecified)
        }
    }
    
    private func layout(proposal: ProposedViewSize, subviews: Subviews) -> (size: CGSize, positions: [CGPoint]) {
        let columnWidth = (proposal.width ?? 0) / CGFloat(columns)
        var heights = Array(repeating: CGFloat(0), count: columns)
        var positions: [CGPoint] = []
        for subview in subviews {
            let size = subview.sizeThatFits(.unspecified)
            let minHeight = heights.min() ?? 0
            let col = heights.firstIndex(of: minHeight) ?? 0
            positions.append(CGPoint(x: CGFloat(col) * columnWidth, y: heights[col]))
            heights[col] += size.height + spacing
        }
        return (CGSize(width: proposal.width ?? 0, height: heights.max() ?? 0), positions)
    }
}

Parallax Header

Create parallax scrolling effects:

struct ParallaxHeader: View {
    var body: some View {
        ScrollView {
            GeometryReader { geometry in
                let offset = geometry.frame(in: .global).minY
                Image("header")
                    .resizable()
                    .scaledToFill()
                    .frame(width: UIScreen.main.bounds.width, height: 200 + max(0, -offset))
                    .offset(y: min(0, offset))
                    .clipped()
            }
            .frame(height: 200)
        }
    }
}

Custom Animations

AnimatableModifier

Create custom animated transitions:

struct PulseModifier: ViewModifier, Animatable {
    var scale: CGFloat = 1.0
    var opacity: Double = 1.0
    
    var animatableData: CGFloat {
        get { scale }
        set { scale = newValue }
    }
    
    func body(content: Content) -> some View {
        content.scaleEffect(scale).opacity(opacity)
    }
}

GeometryEffect

Custom geometric transformations like 3D flip:

struct FlipEffect: GeometryEffect {
    var angle: Double
    var animatableData: Double {
        get { angle }
        set { angle = newValue }
    }
    
    func effectValue(size: CGSize) -> ProjectionTransform {
        let a = CGFloat(Angle(degrees: angle).radians)
        var t = CATransform3DIdentity
        t.m34 = 1 / -500
        t = CATransform3DRotate(t, a, 0, 1, 0)
        t = CATransform3DTranslate(t, -size.width / 2, -size.height / 2, 0)
        let aff = ProjectionTransform(CGAffineTransform(translationX: size.width / 2, y: size.height / 2))
        return ProjectionTransform(t).concatenating(aff)
    }
}

Custom Transitions

Define reusable transitions:

struct ScaleAndFade: ViewModifier {
    func body(content: Content) -> some View {
        content.scaleEffect(0.8).opacity(0)
    }
}

extension AnyTransition {
    static var scaleAndFade: AnyTransition {
        .modifier(active: ScaleAndFade(), identity: IdentityModifier())
    }
}

if showView {
    ContentView().transition(.scaleAndFade)
}

Quiz

1. What does the Canvas view provide in SwiftUI?

Question 1 options

2. What protocol enables animated shape morphing?

Question 2 options

3. What does the Layout protocol allow you to create?

Question 3 options

4. What is GeometryEffect used for?

Question 4 options

Flashcards

Question

What is the difference between Canvas and Shape?

Answer

Canvas provides low-level drawing commands for complex graphics. Shape provides a path-based approach for creating reusable vector shapes.

Question

How do you create a custom layout in SwiftUI?

Answer

Implement the Layout protocol with sizeThatFits and placeSubviews methods to define your custom layout algorithm.

Question

What is animatableData used for?

Answer

It tells SwiftUI which properties should be interpolated during animation, enabling smooth transitions between view states.

Revision Notes

Key Takeaways

  • 1. Canvas provides CGContext-like drawing in SwiftUI
  • 2. Shape protocol creates reusable vector shapes
  • 3. Layout protocol enables custom layout algorithms
  • 4. Animatable protocol enables smooth shape transitions
  • 5. GeometryEffect handles complex view transformations

Interview Tips

  • Explain when to use Canvas vs individual SwiftUI views
  • Describe creating a custom shape with the Shape protocol
  • Walk through building a custom Layout implementation
  • Discuss creating custom animations with Animatable

Cheat Sheet

Advanced SwiftUI Quick Reference

  • Canvas: high-performance 2D drawing
  • Shape: reusable vector shapes via path
  • Layout protocol: custom layout containers
  • Animatable: animatableData for transitions
  • GeometryEffect: 3D transformations
  • Custom transitions with ViewModifier