Image Playground Sheet in SwiftUI


Greetings, traveler!

When the Image Playground framework first appeared, it gave developers two different paths. You could present the system Image Playground interface, or you could use ImageCreator to generate images programmatically from your app.

That second path is going away.

Apple has deprecated the ImageCreator class. It will no longer work in iOS 27, iPadOS 27, macOS 27, and visionOS 27 or later. During beta OS releases, code may still compile, but Xcode will show warnings. Apps that use ImageCreator will not work in TestFlight builds and may hit runtime errors. In public OS releases, the code will not compile.

So if your app already has image generation built on top of ImageCreator, it is a migration point.

Apple’s recommended direction is to present the Image Playground sheet instead, or integrate another image generation service.

For SwiftUI apps, the main API for the system-managed path is imagePlaygroundSheet.

What imagePlaygroundSheet does

imagePlaygroundSheet is a SwiftUI view modifier that presents Apple’s Image Playground interface from your app.

Your app provides an initial idea. That idea can be a simple text concept, a list of structured concepts, and optionally a source image. The system then opens a familiar Image Playground experience where the user can create, adjust, and choose an image.

The app does not send a prompt and receive a final image without user interaction. Instead, the app starts the flow, the user works inside the sheet, and your app receives the selected result.

That design makes more sense after the ImageCreator deprecation. Apple is clearly moving image generation toward a consistent, system-managed user experience rather than letting every app build its own invisible generation flow.

Basic SwiftUI usage

The simplest version looks like this:

import SwiftUI
import ImagePlayground

struct ContentView: View {
    @State private var isPresented = false
    @State private var generatedImageURL: URL?

    var body: some View {
        VStack(spacing: 16) {
            if let generatedImageURL {
                AsyncImage(url: generatedImageURL) { image in
                    image
                        .resizable()
                        .scaledToFit()
                } placeholder: {
                    ProgressView()
                }
            }

            Button("Create image") {
                isPresented = true
            }
        }
        .imagePlaygroundSheet(
            isPresented: $isPresented,
            concept: "A small robot reading a Swift book",
            sourceImage: nil,
            onCompletion: { url in
                generatedImageURL = url
            },
            onCancellation: {
                print("The user cancelled image creation")
            }
        )
    }
}

The modifier behaves like other SwiftUI presentation APIs. You control it through a binding, and SwiftUI presents the sheet when the binding becomes true.

The concept is the starting idea for the image. It can be something simple, such as “a fox wearing glasses” or “a cozy cabin in winter”. The system uses it to seed Image Playground, but the user can still modify the result inside the sheet.

The sourceImage parameter lets you pass an optional image as visual context. This is useful when the user starts from an existing photo, sketch, avatar, or design reference.

When the user finishes, onCompletion gives your app a file URL for the generated image. When the user cancels, onCancellation is called instead.

The completion URL is temporary

There is one detail that is easy to miss: the URL returned in onCompletion points to a temporary file.

That means this code is fine for a quick preview:

onCompletion: { url in
    generatedImageURL = url
}

But it is not enough if the generated image becomes part of your app data.

If the user creates a profile image, journal illustration, sticker, cover image, or project asset, you should copy the file into persistent storage.

private func persistGeneratedImage(from temporaryURL: URL) throws -> URL {
    let fileName = "\(UUID().uuidString).png"
    let destinationURL = URL.documentsDirectory.appending(path: fileName)

    try FileManager.default.copyItem(
        at: temporaryURL,
        to: destinationURL
    )

    return destinationURL
}

Then use it from the completion handler:

.imagePlaygroundSheet(
    isPresented: $isPresented,
    concept: "A small robot reading a Swift book",
    sourceImage: nil,
    onCompletion: { temporaryURL in
        do {
            generatedImageURL = try persistGeneratedImage(
                from: temporaryURL
            )
        } catch {
            print("Failed to save generated image:", error)
        }
    },
    onCancellation: {
        print("Cancelled")
    }
)

I would keep this persistence logic outside the view in real projects. The view should present the sheet and react to the result. A separate object should decide where the image is saved, how it is named, and how it becomes part of the user’s data model.

Checking availability

Image Playground is not available everywhere.

It depends on OS version, device support, Apple Intelligence availability, and user settings. Do not hardcode a device list. SwiftUI gives you an environment value for this:

@Environment(\.supportsImagePlayground)
private var supportsImagePlayground

You can use it to hide or disable the feature:

if supportsImagePlayground {
    Button("Create image") {
        isPresented = true
    }
} else {
    Text("Image Playground is not available on this device.")
}

Passing a source image

A source image changes the flow from “create something from text” to “create something based on this visual input”.

.imagePlaygroundSheet(
    isPresented: $isPresented,
    concept: "Turn this into a soft illustrated avatar",
    sourceImage: selectedImage,
    onCompletion: { url in
        generatedImageURL = url
    }
)

The main thing to remember is that Image Playground is still an interactive system UI. The source image gives the user a starting point, not a strict transformation pipeline.

Multiple concepts are better for richer context

A single String works well for simple cases, but Image Playground also supports structured concepts.

That matters when the image idea comes from multiple parts of your app. For example, you might combine a theme, a user message, and extracted text from a note.

let concepts: [ImagePlaygroundConcept] = [
    .text("Birthday postcard"),
    .text("Warm evening light"),
    .extracted(
        from: messageText,
        title: "Message"
    )
]

Then you can present the sheet with those concepts instead of a single prompt:

.imagePlaygroundSheet(
    isPresented: $isPresented,
    concepts: concepts,
    sourceImage: nil,
    onCompletion: { url in
        generatedImageURL = url
    },
    onCancellation: {
        print("Cancelled")
    }
)

Final thoughts

imagePlaygroundSheet is not just a convenience modifier anymore. After the ImageCreator deprecation, it becomes the main Apple-recommended path for apps that want to keep using Image Playground inside their own interfaces.

Your app provides context, the system presents the creation experience, and the user chooses the final image.

If your app used ImageCreator, this is the moment to revisit the feature. Some products can move to imagePlaygroundSheet with a fairly small UI change. Others may need a different image generation service.