Search Docs
Loading...
Skip to content

Photo Editor in iOS

Professional photo editing for your iOS app—crop, filter, adjust, and remove backgrounds. Runs entirely on the mobile device with no server dependencies.

Photo Editor starter kit screenshot

10 mins
estimated time
Download
GitHub

Pre-Requisites#

This guide assumes basic familiarity with iOS and Swift. You will need:

  • Latest Xcode
  • Swift 5.9 or later
  • iOS 16.0+

Get Started#

Start with a complete, runnable iOS starter kit project.

Step 1: Clone the Repository#

Terminal window
git clone -b v1.73.1 https://github.com/imgly/starterkit-photo-editor-ios.git
cd starterkit-photo-editor-ios

Step 2: Open and Run#

Open the project in Xcode and run on a simulator or connected device:

  1. Open starterkit-photo-editor-ios.xcodeproj in Xcode
  2. Select your target device or simulator
  3. Press ⌘R to build and run

The sample app shows a “Launch Editor” button. Tapping it presents the photo editor:

var body: some View {
Editor(settings)
.imgly.configuration {
PhotoEditorConfiguration()
}
}

The full implementation of the starter kit lives in the starter-kit/ folder:

starter-kit/
├── PhotoEditorStarterKit.swift # SwiftUI view that launches the editor
├── PhotoEditorConfiguration.swift # Editor configuration (callbacks + UI components)
├── callbacks/
│ ├── OnCreate+Photo.swift # Editor initialization logic
│ ├── OnExport+Photo.swift # Export flow and handling
│ ├── OnLoaded+Photo.swift # Post-load setup (crop behavior)
│ └── OnChanged+Photo.swift # Edit mode change handling
└── components/
├── CanvasMenu+Photo.swift # Canvas menu configuration
├── Dock+Photo.swift # Dock configuration
├── InspectorBar+Photo.swift # Inspector bar configuration
└── NavigationBar+Photo.swift # Navigation bar configuration

Configuring the Starter Kit#

The starter kit provides a generic structure and behavior that you can customize to match your needs. Since the implementation is part of your codebase, you can add, remove, or modify functionality as you wish.

The entry point is PhotoEditorStarterKit.swift, which creates the Editor view and applies the configuration:

Editor(settings)
.imgly.configuration {
PhotoEditorConfiguration()
}

You can configure the editor based on your business logic — for example, loading a scene from a previous editing session or displaying different UI for different users. Add properties to PhotoEditorStarterKit and use them in the .imgly.onCreate callback to customize scene loading or other behavior.

Set Up a Scene#

The scene setup logic is located in OnCreate+Photo.swift as part of the defaultCreateScene callback:

let imageURL = Bundle(for: PhotoEditorConfiguration.self).url(forResource: "photo-ui-empty", withExtension: "png")!
try await engine.scene.create(fromImage: imageURL)

CE.SDK offers multiple ways to load a scene into the editor — from an image URL, a template archive, a blank canvas, or a .scene file.

Force Crop#

Require users to crop images to specific dimensions before saving. This is useful for profile pictures, social media posts, or any workflow requiring consistent image sizes. Send the applyForceCrop event in an onLoaded callback:

/// Example: Apply force crop in the `onLoaded` callback.
/// Add this logic before or after the existing `onLoaded` handler in `PhotoEditorConfiguration`.
extension PhotoEditorConfiguration {
static var forceCropOnLoadedHandler: OnLoaded.Handler {
{ context, existing in
// Apply force crop: allow 1:1, 16:9, or 9:16
if let page = try context.engine.scene.getPages().first {
context.eventHandler.send(
.applyForceCrop(
to: page,
with: [
ForceCropPreset(sourceID: "ly.img.crop.presets", presetID: "aspect-ratio-1-1"),
ForceCropPreset(sourceID: "ly.img.crop.presets", presetID: "aspect-ratio-16-9"),
ForceCropPreset(sourceID: "ly.img.crop.presets", presetID: "aspect-ratio-9-16"),
],
mode: .ifNeeded,
),
)
}
// Continue with the existing onLoaded logic
try await existing()
}
}
}

See Force Crop for more details.

Customize Assets#

The asset source setup is located in OnCreate+Photo.swift as part of the defaultLoadAssetSources callback. Enable or disable individual sources:

let assetSources: [String: URL] = [
Engine.DefaultAssetSource.sticker.rawValue: Engine.assetBaseURL,
Engine.DefaultAssetSource.vectorPath.rawValue: Engine.assetBaseURL,
Engine.DefaultAssetSource.filterLut.rawValue: Engine.assetBaseURL,
Engine.DefaultAssetSource.filterDuotone.rawValue: Engine.assetBaseURL,
Engine.DefaultAssetSource.colorsDefaultPalette.rawValue: Engine.assetBaseURL,
Engine.DefaultAssetSource.effect.rawValue: Engine.assetBaseURL,
Engine.DefaultAssetSource.blur.rawValue: Engine.assetBaseURL,
Engine.DefaultAssetSource.typeface.rawValue: Engine.assetBaseURL,
Engine.DefaultAssetSource.cropPresets.rawValue: Engine.assetBaseURL,
Engine.DefaultAssetSource.pagePresets.rawValue: Engine.assetBaseURL,
Engine.DemoAssetSource.textComponents.rawValue: Engine.assetBaseURL,
]
try await withThrowingTaskGroup(of: Void.self) { group in
for assetSource in assetSources {
group.addTask {
try await engine.populateAssetSource(id: assetSource.key, baseURL: assetSource.value)
}
}
try await group.waitForAll()
}
try await engine.asset.addSource(TextAssetSource(engine: engine))
try engine.asset.addSource(PhotoRollAssetSource(engine: engine))

For production deployments, self-hosting assets is required—the IMG.LY CDN is intended for development only. See Serve Assets for downloading assets, configuring baseURL and excluding unused sources to optimize load times.

Customize Export Functionality#

Export handling logic is located in OnExport+Photo.swift as part of the onExport callback.

The default implementation exports the scene as PNG and opens the system share sheet:

guard let scene = try engine.scene.get() else {
throw EditorError("No scene was found.")
}
let data = try await engine.block.export(scene, mimeType: .png) { engine in
try engine.scene.getPages().forEach {
try engine.block.setScopeEnabled($0, key: "layer/visibility", enabled: true)
try engine.block.setVisible($0, visible: true)
}
}
let url = FileManager.default.temporaryDirectory.appendingPathComponent("Export", conformingTo: .png)
try data.write(to: url, options: [.atomic])
eventHandler.send(.shareFile(url))

Customize (Optional)#

Base URL#

The starter kit uses Engine.assetBaseURL by default, which points to https://cdn.img.ly/packages/imgly/cesdk-engine/1.73.1/assets. To self-host assets, download the asset zip file and pass a custom baseURL to your EngineSettings.

Color Scheme#

CE.SDK supports light and dark modes out of the box, plus automatic system preference detection. Apply SwiftUI’s .preferredColorScheme modifier to the Editor view to switch themes.

See Theming for more details.

Localization#

See Localization for supported languages, adding support for new languages, and replacing existing keys.

UI Layout#

All configurable components are located in the components/ folder:

  • CanvasMenu+Photo.swift — see Canvas Menu for full configuration options
  • Dock+Photo.swift — see Dock for full configuration options
  • InspectorBar+Photo.swift — see Inspector Bar for full configuration options
  • NavigationBar+Photo.swift — see Navigation Bar for full configuration options

Troubleshooting#

Editor doesn’t load#

  • Check onCreate: Ensure the onCreate callback loads a scene successfully
  • Verify the baseURL: Assets must be accessible from the CDN or your self-hosted location
  • Check Xcode console: Look for errors in the Xcode debug console

Assets don’t appear#

  • Check network requests: Make sure the device or simulator is connected to the internet
  • Self-host assets for production: See Serve Assets to host assets on your infrastructure
  • Check Xcode console: Look for errors in the Xcode debug console

Export fails or produces blank images#

  • Wait for content to load: Ensure images are fully loaded before exporting
  • Check Xcode console: Look for errors in the Xcode debug console

Watermark appears in production#

  • Add your license key: Set the license property in your EngineSettings
  • Sign up for a trial: Get a free trial license at img.ly/forms/free-trial

Next Steps#