Igor Skvortsov/Guide - Render Swipe Actions in SwiftUI

Created Sat, 01 Feb 2025 00:00:00 +0000 Modified Tue, 08 Jul 2025 22:46:24 +0000

The short guide on how to render custom view in swipe action.

Task

Use SwiftUI views inside swipeActions by prerendering them into Image objects, overcoming UIKit limitations.

Result illustration

Solution

In SwiftUI, swipeActions only accept Image/UIImage as button labels. This restricts the ability to use rich SwiftUI layouts like CircleRoundedRectangle, or even combinations of views with SF Symbols and colors. To overcome this, we can render SwiftUI views into images.

Meet ImageRenderer

ImageRenderer is a powerful API available from iOS 16+, allowing you to convert any SwiftUI view into a UIImage. It’s especially useful when you’re working with UIKit-bound APIs or places like swipeActions that only accept image-based content.

Here’s how it works:

  • You wrap your SwiftUI content in a fixed-size container.
  • You pass it to ImageRenderer.
  • You retrieve a UIImage (or Image for SwiftUI) that visually matches your SwiftUI view.

This allows full creative control with shapes, text, gradients, SF Symbols, and more — and then reuse that output anywhere UIImage is expected.

Implementation

Here’s a reusable function that does the rendering:

func render(size: CGSize, @ViewBuilder content: () -> some View) -> Image? {
    let renderer = ImageRenderer(
        content: content()
            .frame(width: size.width, height: size.height)
    )
    renderer.scale = UIScreen.main.scale
    renderer.proposedSize = .init(size)

    return renderer.uiImage.map { Image(uiImage: $0) }
}
  • size defines the exact output size of the image.
  • .proposedSize ensures the renderer respects layout.
  • The SwiftUI view is framed and passed into the renderer.

Benefits

  • Use rich SwiftUI layout (shapes, overlays, icons) where only Image is allowed
  • Retain visual consistency with the rest of your UI
  • Customize light/dark mode rendering via SwiftUI environment if needed
  • Combine this with caching for performance

Notes

  • Works on iOS 16 and later .
  • You can wrap this rendering in a cache layer for efficiency.
  • .tint(.clear) is important to avoid system tint from affecting your rendered image.

Demo Project

import SwiftUI

struct ContentView: View {
    @Environment(\.displayScale) var displayScale
    @State private var items = Array(repeating: "0", count: 10)

    var body: some View {
        List {
            ForEach(items.indices, id: \.self) { index in
                Text("Row \(index)")
                    .swipeActions(edge: .trailing) {
                        Button {
                            print("Tapped action on row \(index)")
                        } label: {
                            render(size: CGSize(width: 36, height: 36)) {
                                Circle()
                                    .fill(Color.blue)
                                    .overlay {
                                        Image(systemName: "star.fill")
                                            .foregroundColor(.white)
                                    }
                            }
                        }
                        .tint(.clear)

                        Button {
                            print("Tapped action on row \(index)")
                        } label: {
                            render(size: CGSize(width: 36, height: 36)) {
                                RoundedRectangle(cornerRadius: 6)
                                    .fill(Color.blue)
                                    .overlay {
                                        Image(systemName: "document.fill")
                                            .foregroundColor(.white)
                                    }
                            }
                        }
                        .tint(.clear)
                    }
            }
        }
    }

    // MARK: - Renderer

    func render(size: CGSize, @ViewBuilder content: () -> some View) -> Image? {
        let renderer = ImageRenderer(
            content: content()
                .frame(width: size.width, height: size.height)
        )
        renderer.scale = UIScreen.main.scale
        renderer.proposedSize = .init(size)

        return renderer.uiImage.map { Image(uiImage: $0) }
    }
}