Mantis

guoyingtaoMIT活跃维护

iOS 图片裁剪库,支持 UIKit 和 SwiftUI,仿系统相册的裁剪体验,提供裁剪形状、旋转、翻转、透视校正和撤销重做。

An iOS image cropping library for UIKit and SwiftUI, mimicking the Photos app — crop shapes, rotation, flip, perspective correction, undo/redo. iOS & Mac Catalyst.

v3.1.1发布于 2026-10-23 21:25 UTC1,211star245fork同步于 2026-11-10 17:52 UTC

怎么上手

由 README 提炼,执行前请确认
在 Xcode 中 File → Add Package Dependencies… 输入 https://github.com/guoyingtao/Mantis-spm.git,版本规则选 Up to Next Major 3.1.1;或在 Package.swift 写 .package(url: "https://github.com/guoyingtao/Mantis-spm.git", from: "3.1.1")
硬件要求按 README 说明

README 未说明;仅要求 iOS 17.0+、Mac Catalyst 17.0+(macOS 14.0+),MantisCore 需 macOS 14.0+,Xcode 16.0+(Swift 6.0+)

下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

iOS 图片裁剪库,用 UIKit 或 SwiftUI 复刻系统相册的裁剪编辑体验

iOS 图片裁剪库,支持 UIKit 和 SwiftUI,仿系统相册的裁剪体验,提供裁剪形状、旋转、翻转、透视校正和撤销重做。

核心能力

  • 提供 UIKit 与 SwiftUI 两套 API,SwiftUI 有声明式 ImageCropper 视图
  • 支持旋转、翻转、旋转拨盘拉直与自由或固定宽高比裁剪
  • 内置圆形、椭圆、圆角矩形、多边形、心形及自定义路径裁剪形状
  • 支持仿系统相册的透视校正,带实时 3D 预览
  • CropInfo 可 Codable 持久化,支持撤销重做与恢复原图

容易踩的坑

4.0 起只通过 Swift Package Manager 分发,不再支持其他包管理器;应依赖轻量的 Mantis-spm 仓库而非本仓库,后者完整历史约 70 MB 会被 SwiftPM 全量下载;用 UIKit 弹出裁剪控制器时必须设置 modalPresentationStyle = .fullScreen

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本v3.1.1
  1. v3.1.1release-please-action v3.1.1最新正式版2026-10-23 · 2026-10-23 21:25 UTC

    Bug Fixes

    • adapt crop layout to foldable iPhone displays (#551) (#552) (8dc58d6)
    • confine crop box resizing within the content bounds (#543) (3cd2b51)
    • finish VoiceOver support on the rotation dials (#555) (deb9ce8)
    • keep the crop box visible when a view transition is skipped (#553) (b25ffea)
    • make VoiceOver decrement turn the slide dial the other way (#554) (f435c1f)
  2. v3.1.0release-please-action v3.1.02026-07-09 · 2026-07-09 07:40 UTC
    查看更新说明

    Features

    • make CropInfo conform to Codable for cross-session crop persistence (#538) (8b3737c), closes #536
  3. v3.0.0release-please-action v3.0.02026-07-06 · 2026-07-06 06:57 UTC
    查看更新说明

    ⚠ BREAKING CHANGES

    • targeted at the next major version.
      • build!: raise minimum deployment target to iOS 15 / Mac Catalyst 15

    Features

    • add PrivacyInfo.xcprivacy privacy manifest (#525) (b999649)
    • observable CropSession + declarative ImageCropper; per-session TransformStack (#535) (1d51c1f)
    • Support large images (#498) (82dea03)

    Bug Fixes

    • avoid double safe area inset offset in embedded mode (#487) (#521) (f60bb84)
    • avoid initial-display zoom flicker in fixed-ratio crop (#487) (#523) (d613bdc)
    • fix the UI jumping issue when rotateCropBoxFor90DegreeRotation = false (#490) (3c0f520)
    • guard iOS 17 trait-change API for the declared iOS 15 minimum (#533) (ddda42e)

    Performance Improvements

    • reuse a shared CIContext for perspective cropping (#524) (cabefe3)

    Build System

    • raise minimum deployment target to iOS 15 (#528) (379382d)

    Code Refactoring

    • remove deprecated CropViewConfig props; slim public CropInfo (#534) (30b9e7a)
  4. v2.31.2release-please-action v2.31.22026-05-31 · 2026-05-31 13:09 UTC
    查看更新说明

    Bug Fixes

    • avoid contentOffset snap on first skew frame after rotation/resize (#519) (ced9c75)
  5. v2.31.1release-please-action v2.31.12026-03-26 · 2026-03-26 14:13 UTC
    查看更新说明

    Bug Fixes

    • guard against NaN/Infinite values to prevent crashes during crop and rotation (#518) (294e44b)
    • wrap iOS 26 Liquid Glass API usage with #if compiler(>=6.2) for Xcode 16 compatibility (#513) (662383e)

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    Mantis — Image Cropping Library for iOS (Swift · UIKit · SwiftUI)

    Mantis is an open-source iOS image cropping library written in Swift, with both UIKit and SwiftUI APIs. It gives your app an Apple Photos–style photo crop and edit experience: crop with rotation, flip, free or fixed aspect ratios, rich crop shapes (circle, ellipse, rounded rectangle, polygon, heart, arbitrary paths), perspective correction (skew), and full undo/redo — on iOS and Mac Catalyst.

    Features

    • ✂️ Photos-app-style cropping UI — pan, zoom, resize the crop box, rotate 90°, flip, and straighten with a rotation dial or slide dial
    • 🖼 Free or fixed aspect ratios — built-in presets (original, square, 16:9, …) plus your own custom ratios
    • 🟣 Rich crop shapes — rectangle, square, circle, ellipse, rounded rectangle, diamond, polygon, heart, or any custom path (with optional mask-only mode)
    • 📐 Perspective correction (skew) — Apple Photos–style vertical/horizontal perspective adjustment with real-time 3D preview
    • ↩️ Undo / Redo / Revert to original — independent history per cropper, with localized Mac Catalyst menus
    • 🧩 Two SwiftUI APIs — a modern declarative ImageCropper view with modifiers and an observable CropSession, plus a binding-based ImageCropperView
    • 💾 Persist and restore crops — CropInfo is Codable (3.1+); re-crop the original image offline in a later session, or reopen the editor at the saved state
    • 🖥 Crop engine for macOS — the UIKit-free MantisCore product (4.0+) applies a saved CropInfo to a CGImage or NSImage in a native macOS app
    • 🌗 Light / dark / system appearance modes
    • 🌍 Localized into 14 languages (Arabic, Chinese, English, French, German, Italian, Japanese, Korean, Dutch, Portuguese, Russian, Spanish, Turkish, …) with custom localization support
    • 🧱 Highly customizable — embed the cropper in your own view controller, build a custom toolbar, subclass CropViewController, tweak colors, borders, dial styles, and more
    • 🐘 Large-image support — async crop mode and pixel-count limiting for very large photos
    • 🔒 Privacy manifest included (PrivacyInfo.xcprivacy)

    Demos

    Mantis provides rich crop shapes, from basic circle/square to polygons to arbitrary paths (we even provide a heart shape ❤️ 😏).

    Table of Contents

    Quick Start

    SwiftUI

    import Mantis
    import SwiftUI
    
    struct AvatarCropView: View {
        let image: UIImage
        @State private var croppedImage: UIImage?
    
        var body: some View {
            ImageCropper(image: image)
                .cropShape(.circle)
                .aspectRatio(.fixed(1))
                .onCrop { result in
                    croppedImage = result.croppedImage
                }
        }
    }
    

    UIKit

    import Mantis
    
    let cropViewController = Mantis.cropViewController(image: yourImage)
    cropViewController.delegate = self
    cropViewController.modalPresentationStyle = .fullScreen // required when presenting
    present(cropViewController, animated: true)
    
    // Receive the result:
    extension YourViewController: CropViewControllerDelegate {
        func cropViewControllerDidCrop(_ cropViewController: CropViewController,
                                       cropped: UIImage,
                                       transformation: Transformation,
                                       cropInfo: CropInfo) {
            // Use the cropped image
        }
    
        func cropViewControllerDidCancel(_ cropViewController: CropViewController,
                                         original: UIImage) {
            // Handle cancel
        }
    }
    

    See Usage below for aspect ratios, crop shapes, undo/redo, persistence, and full customization.

    Requirements

    • iOS 17.0+
    • Mac Catalyst 17.0+ (macOS 14.0+)
    • macOS 14.0+ for the MantisCore product only (offline cropping, no crop UI)
    • Xcode 16.0+ (Swift 6.0+)

    Installation

    Starting with 4.0, Mantis is distributed through Swift Package Manager only.

    Swift Package Manager

    Use the lightweight Mantis-spm distribution. It contains only the sources and is under 1 MB, whereas this repository's full history (example apps, README media) is about 70 MB and SwiftPM downloads all of it for every project that depends on it.

    In Xcode: File → Add Package Dependencies… and enter:

    https://github.com/guoyingtao/Mantis-spm.git
    

    Rule: Version — Up to Next Major — 3.1.1

    Or in Package.swift:

    .package(url: "https://github.com/guoyingtao/Mantis-spm.git", from: "3.1.1")
    

    and in your target's dependencies: .product(name: "Mantis", package: "Mantis-spm"). The product and module are unchanged, so import Mantis works as before.

    A native macOS app, or any target that only replays saved crops, depends on .product(name: "MantisCore", package: "Mantis-spm") instead. MantisCore is available from Mantis 4.0. See Cropping on macOS with MantisCore.

    Depending on https://github.com/guoyingtao/Mantis.git directly still works, but downloads far more. Don't use both URLs in the same project: SwiftPM derives the package identity from the URL (mantis-spm vs mantis) and would treat them as two packages exporting the same product.

    CocoaPods and Carthage

    Mantis 4.0 drops CocoaPods and Carthage. Projects that still use them can stay on Mantis 3.x:

    pod 'Mantis', '~> 3.1'
    
    github "guoyingtao/Mantis" ~> 3.1
    

    What's New in Mantis 3.x

    For the complete history see the CHANGELOG.

    Declarative SwiftUI API (3.0)

    Mantis 3.0 introduced a modern, declarative SwiftUI API:

    • ImageCropper — a SwiftUI view configured with modifiers:
    ImageCropper(image: myImage)
        .cropShape(.circle)
        .aspectRatio(.fixed(16/9))
        .onCrop { result in croppedImage = result.croppedImage }
    
    • CropSession — an observable handle to a live crop session. It exposes canUndo, canRedo, isResettable and the live transformation as observable state, and drives the cropper with plain methods (rotate(), flip(), crop(), undo(), redo(), reset(), setAspectRatio()) instead of the old enum-binding action pattern. It participates in the Observation framework for fine-grained, per-property updates and is also an ObservableObject.
    • Independent undo history per cropper — the internal transform stack is no longer a global singleton, so two croppers shown at the same time (e.g. iPad multi-window) keep separate undo/redo histories.

    The binding-based ImageCropperView API remains fully supported. See the SwiftUI usage section for the full API and the MantisSwiftUIExample project for working demos.

    Codable crop persistence (3.1)

    CropInfo now conforms to Codable, so you can save a user's exact crop and reproduce it offline in a later app session — including rotated, fixed-ratio, perspective-skewed, and large-image crops. See Persisting and restoring crops.

    Perspective correction (skew)

    Mantis supports Apple Photos–style perspective correction, letting users adjust horizontal and vertical skew in addition to straightening. When enabled, the slide dial displays three circular icon buttons — Straighten, Vertical, and Horizontal — so users can switch adjustment modes with a single tap.

    • Real-time 3D perspective preview powered by CATransform3D
    • Accurate image export using CIPerspectiveCorrection
    • Full integration with existing features: undo/redo, flip, 90° rotation, and preset transformations

    Appearance mode

    Mantis supports light, dark, and system appearance modes. By default Mantis uses a dark appearance (backward compatible).

    var config = Mantis.Config()
    config.appearanceMode = .forceLight   // or .forceDark (default), .system
    let cropViewController = Mantis.cropViewController(image: yourImage, config: config)
    

    Usage

    Important: set modalPresentationStyle = .fullScreen on the crop view controller (or its navigation controller) when presenting it.

    UIKit

    let cropViewController = Mantis.cropViewController(image: yourImage)
    cropViewController.delegate = self
    cropViewController.modalPresentationStyle = .fullScreen
    present(cropViewController, animated: true)
    

    The caller conforms to CropViewControllerDelegate:

    public protocol CropViewControllerDelegate: AnyObject {
        func cropViewControllerDidCrop(_ cropViewController: CropViewController, cropped: UIImage, transformation: Transformation, cropInfo: CropInfo)
        func cropViewControllerDidCancel(_ cropViewController: CropViewController, original: UIImage)
        
        // The implementation of the following functions are optional
        func cropViewControllerDidFailToCrop(_ cropViewController: CropViewController, original: UIImage)     
        func cropViewControllerDidBeginResize(_ cropViewController: CropViewController)
        func cropViewControllerDidEndResize(_ cropViewController: CropViewController, original: UIImage, cropInfo: CropInfo)    
    }
    

    SwiftUI

    Declarative API (Mantis 3.0+)

    ImageCropper is a declarative SwiftUI view configured with modifiers:

    struct MyView: View {
        @State private var croppedImage: UIImage?
    
        var body: some View {
            ImageCropper(image: myImage)
                .cropShape(.circle)
                .aspectRatio(.fixed(16/9))
                .onCrop { result in
                    croppedImage = result.croppedImage
                }
        }
    }
    

    To build your own controls, attach a CropSession. It exposes canUndo, canRedo, isResettable and the live transformation as observable state (fine-grained Observation tracking, or ObservableObject with @StateObject), and drives the cropper with plain methods instead of enum bindings:

    struct MyCropperView: View {
        @StateObject private var session = CropSession()
        @State private var croppedImage: UIImage?
    
        var body: some View {
            VStack {
                ImageCropper(image: myImage, session: session)
                    .builtInToolbarVisible(false)
                    .onCrop { result in croppedImage = result.croppedImage }
    
                HStack {
                    Button("Undo") { session.undo() }
                        .disabled(!session.canUndo)
                    Button("Redo") { session.redo() }
                        .disabled(!session.canRedo)
                    Button("Reset") { session.reset() }
                        .disabled(!session.isResettable)
                    Button("Rotate") { session.rotate(.clockwise) }
                    Button("Flip") { session.flip(.horizontal) }
                    Button("Done") { session.crop() }
                }
            }
        }
    }
    

    Available modifiers: .cropShape(_:), .aspectRatio(_:), .builtInToolbarVisible(_:), .appearance(_:), .configure { $0... } (escape hatch to the full Mantis.Config), .onCrop(_:), .onCancel(_:), .onCropFailed(_:).

    Binding-based API

    Kept for backward compatibility and fully supported; new code should prefer the declarative ImageCropper above.

    struct MyView: View {
        @State private var image: UIImage?
        @State private var transformation: Transformation?
        @State private var cropInfo: CropInfo?
    
        var body: some View {
            ImageCropperView(
                image: $image,
                transformation: $transformation,
                cropInfo: $cropInfo
            )
        }
    }
    

    Note:

    • To start a crop operation programmatically, use the action binding (for ImageCropperView):
      action = .crop
      
    • To receive the result of the crop (success or failure), use the onCropCompleted callback.
      This is especially useful because cropping may not complete instantly in all cases, so relying on this callback ensures you update your UI only after the operation finishes.

    CropToolbar has two modes:

    • normal mode

      In normal mode, you can use a set of standard CropViewController photo editing features with "Cancel" and "Done" buttons.

    let cropViewController = Mantis.cropViewController(image: yourImage)
    
    • embedded mode

      This mode does not include "Cancel" and "Done" buttons, so you can embed CropViewController into another view controller and build your own surrounding UI.

    var config = Mantis.Config()
    config.cropToolbarConfig.mode = .embedded
    let cropViewController = Mantis.cropViewController(image: yourImage, config: config)
    
    // Add a custom ratio 1:2 for portrait orientation
    var config = Mantis.Config()
    config.addCustomRatio(byVerticalWidth: 1, andVerticalHeight: 2)            
    let cropViewController = Mantis.cropViewController(image: yourImage, config: config)
    
    // Set the ratioOptions of the config if you don't want to keep all default ratios
    var config = Mantis.Config() 
    //config.ratioOptions = [.original, .square, .custom]
    config.ratioOptions = [.custom]
    config.addCustomRatio(byVerticalWidth: 1, andVerticalHeight: 2)            
    let cropViewController = Mantis.cropViewController(image: yourImage, config: config)
    
    • If you always want to use only one fixed ratio, set presetFixedRatioType to alwaysUsingOnePresetFixedRatio:
    config.presetFixedRatioType = .alwaysUsingOnePresetFixedRatio(ratio: 16.0 / 9.0)
    

    When choosing alwaysUsingOnePresetFixedRatio, the fixed-ratio setting button does not show. (In the declarative SwiftUI API this is .aspectRatio(.fixed(16/9)).)

    • If you want to hide the rotation control view, set config.cropViewConfig.showAttachedRotationControlView = false
    • If you want to use a ratio list instead of a presenter, set config.cropToolbarConfig.ratioCandidatesShowType = .alwaysShowRatioList
    public enum RatioCandidatesShowType {
        case presentRatioList
        case alwaysShowRatioList
    }
    
    • If you build your own custom toolbar you can add your own fixed ratio buttons:
    // set a custom fixed ratio
    cropToolbarDelegate?.didSelectRatio(ratio: 9 / 16)
    

    To use a different crop shape — for example a circular avatar crop — set config.cropViewConfig.cropShapeType:

    public enum CropShapeType {
        case rect
        case square
        case ellipse(maskOnly: Bool = false)
        case circle(maskOnly: Bool = false)
        case roundedRect(radiusToShortSide: CGFloat, maskOnly: Bool = false)
        case diamond(maskOnly: Bool = false)
        case heart(maskOnly: Bool = false)
        case polygon(sides: Int, offset: CGFloat = 0, maskOnly: Bool = false)
        case path(points: [CGPoint], maskOnly: Bool = false)
    }
    

    With maskOnly: true, the shape is used only as a visual mask and the exported image stays rectangular; with the default maskOnly: false, the exported image is cut to the shape with a transparent background.

    In the declarative SwiftUI API the same type is used: .cropShape(.circle) (the cases whose associated values all have defaults can be written without parentheses).

    If you want to apply transformations when showing an image (for example to reopen the editor at a previously saved state), set config.cropViewConfig.presetTransformationType:

    public enum PresetTransformationType {
        case none
        case presetInfo(info: Transformation)
        case presetNormalizedInfo(normalizedInfo: CGRect)
    }
    

    Please use the transformation information obtained previously from the delegate method cropViewControllerDidCrop(_:cropped:transformation:cropInfo:), or — in the declarative SwiftUI API — from CropResult.transformation delivered by .onCrop.

    As of Mantis 3.1.0, CropInfo conforms to Codable, so you can save a user's exact crop and reproduce it offline in a later session — including rotated, fixed-ratio, perspective-skewed, and large-image crops.

    // Save the CropInfo delivered by the delegate (or CropResult.cropInfo in SwiftUI).
    func cropViewControllerDidCrop(_ cropViewController: CropViewController,
                                   cropped: UIImage,
                                   transformation: Transformation,
                                   cropInfo: CropInfo) {
        let data = try? JSONEncoder().encode(cropInfo)
        // ...persist `data` (UserDefaults, a file, a database, etc.)
    }
    
    // Later — even in a new app session — re-crop the original image offline, no UI:
    let cropInfo = try JSONDecoder().decode(CropInfo.self, from: data)
    let recropped = Mantis.crop(image: originalImage, by: cropInfo)
    
    • A decoded CropInfo crops identically to the same-session value: the opaque view-reconstruction state needed by the perspective-skew and maxImagePixelCount (large-image) paths is encoded alongside the public fields.
    • Persist a CropInfo that Mantis produced (from the delegate or getCropInfo()). A CropInfo you build by hand via the public init carries no view-reconstruction state, so the perspective and large-image paths still return nil for it — exactly as before.
    • If you only need to reopen the editor at the previous state (rather than crop offline), persisting the Transformation and restoring it via presetTransformationType = .presetInfo(info:) remains the lighter option.

    Compatibility: adding Codable is purely additive — existing code needs no changes. The one exception: if you previously wrote your own extension CropInfo: Codable as a workaround, remove it after upgrading to avoid a duplicate-conformance error. The encoded JSON mirrors CropInfo's fields, so treat versioning of your own persisted data as your app's responsibility.

    As of Mantis 4.0, the crop engine lives in a separate, UIKit-free module, MantisCore, which also builds for native macOS (AppKit) apps. It has no crop UI: it applies a CropInfo that the Mantis UI produced on iOS, iPadOS or Mac Catalyst — for example one your iOS app saved and synced — to the full-resolution original.

    import Foundation
    import ImageIO
    import MantisCore
    
    let cropInfo = try JSONDecoder().decode(CropInfo.self, from: data)
    
    // NSImage
    let cropped = MantisCore.crop(nsImage, by: cropInfo)
    
    // Or a CGImage read straight from the file, with its EXIF orientation
    let source = CGImageSourceCreateWithURL(url as CFURL, nil)!
    let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil)!
    let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any]
    let orientation = (properties?[kCGImagePropertyOrientation] as? UInt32)
        .flatMap(CGImagePropertyOrientation.init) ?? .up
    let croppedCGImage = MantisCore.crop(cgImage, orientation: orientation, by: cropInfo)
    
    • The result matches what Mantis.crop(image:by:) produces on iOS for the same image and CropInfo.
    • Mantis re-exports MantisCore, so on iOS import Mantis is enough to use the CGImage overload too.
    • The same rule as on iOS applies: a CropInfo you build by hand crops on the standard path, but the perspective-skew and maxImagePixelCount paths need a CropInfo that Mantis produced.
    • Mantis offers full support for Undo, Redo, and Revert to Original on both iOS and Mac Catalyst.

    • To enable this feature, set config.enableUndoRedo = true.

    • In the declarative SwiftUI API, attaching a CropSession enables undo/redo automatically and exposes canUndo / canRedo / isResettable as observable state.

    • Each crop view controller keeps its own undo history, so multiple croppers can be shown at the same time (Mantis 3.0+).

    • Catalyst menus for this feature are localized.

    Enable perspective correction to let users adjust horizontal and vertical skew, similar to the Apple Photos app.

    var config = Mantis.Config()
    config.cropViewConfig.enablePerspectiveCorrection = true
    let cropViewController = Mantis.cropViewController(image: yourImage, config: config)
    

    When enablePerspectiveCorrection is true, the slide dial is used by default (no need to set builtInRotationControlViewType explicitly) and automatically switches to withTypeSelector mode, showing three circular icon buttons (Straighten / Vertical / Horizontal) above the ruler. Users can tap each button to switch adjustment modes.

    • The skew values are included in the Transformation and CropInfo returned by the delegate, so you can persist and restore them via presetTransformationType.

    • You can optionally customize the appearance of the type selector buttons through SlideDialConfig:

      • typeButtonSize — diameter of each circular button (default: 48)
      • typeButtonSpacing — spacing between buttons (default: 16)
      • activeColor — color for the selected button ring and value text
      • inactiveColor — color for unselected buttons
      • pointerColor — color of the center pointer on the ruler
      • skewLimitation — maximum skew angle in degrees (default: 30)

    Set the appearance mode to control the overall look of the crop UI:

    var config = Mantis.Config()
    config.appearanceMode = .forceLight   // or .forceDark (default), .system
    let cropViewController = Mantis.cropViewController(image: yourImage, config: config)
    
    public enum AppearanceMode {
        /// Always use dark appearance (default, backward compatible)
        case forceDark
        /// Always use light appearance
        case forceLight
        /// Follow system light/dark mode setting
        case system
    }
    
    • .forceDark is the default, keeping the existing dark-themed behavior.
    • .forceLight uses a light color scheme similar to Apple Photos in light mode.
    • .system dynamically adapts to the user's system-wide light/dark mode setting.

    The appearance mode affects all UI components including the toolbar, dimming overlay, rotation dial, type selector, and ratio item views.

    Mantis ships with built-in localizations for Arabic, Chinese (Simplified and Traditional), Dutch, English, French, German, Italian, Japanese, Korean, Portuguese, Russian, Spanish, and Turkish.

    • UIKit project: add the languages you support to the Localizations section of the Project Info tab.
    • SwiftUI project: please check this discussion.

    • Custom localization tables and bundle

    By default Mantis uses its built-in localization tables, and not every language is supported out of the box (see fig 1).

    If your app supports languages that are not built in, you can define your own strings table, localize it in your application target or framework, and point Mantis at it.

    Important: first create a strings file with these keys:

    "Mantis.Done" = "";
    "Mantis.Cancel" = "";
    "Mantis.Reset" = "";
    "Mantis.Original" = "";
    "Mantis.Square" = "";
    "Mantis.Straighten" = "";
    "Mantis.Horizontal" = "";
    "Mantis.Vertical" = "";
    

    Then configure Mantis:

    let config = Mantis.Config()
    config.localizationConfig.bundle = // a bundle where the strings file is located
    config.localizationConfig.tableName = // the localized strings file name within the bundle
    

    If needed you can subclass CropViewController:

    class CustomViewController: CropViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // Do your custom logic here.
            // The MantisExample project also has a showcase for a CustomViewController.
        }
    }
    

    To get an instance, Mantis provides a factory method:

    let cropViewController: CustomViewController = Mantis.cropViewController(image: image, config: config)
    

    Migrating from 3.x

    Mantis 4.0 changes how Mantis is installed and which systems it runs on, removes one function, and isolates the UI API to the main actor:

    • Swift Package Manager only. CocoaPods and Carthage are no longer supported; switch to the Swift Package Manager or stay on Mantis 3.x.
    • Mantis.locateResourceBundle(by:) is removed. It only existed so CocoaPods static frameworks could find the resource bundle; delete the call. To use your own strings, set config.localizationConfig.bundle as described in Localization.
    • The UI API is @MainActor. CropToolbarProtocol, CropToolbarDelegate, CropToolbarIconProvider, RotationControlViewProtocol, ActivityIndicatorProtocol, CropSession, Orientation, Mantis.cropViewController(...) and Mantis.setupCropViewController(...) are now main-actor isolated, as CropViewControllerDelegate already was. In the Swift 6 language mode, custom toolbars, rotation controls, activity indicators and icon providers now conform without isolation errors. Calls from view controllers and SwiftUI views need no change; move calls made from a nonisolated context, such as a detached task or a plain helper function, onto the main actor (for example by marking the caller @MainActor). Mantis.crop(image:by:) and Mantis.downsample(image:maxPixelCount:sourceData:) stay nonisolated, so offline cropping can still run in the background.
    • Custom rotation controls can rely on the default implementations. The default implementations of RotationControlViewProtocol (setupUI(withAllowableFrame:), getTouchTarget(with:), handleDeviceRotation(), getLengthRatio(), setAccessibilities() and setAccessibilityValue()) are now public. In 3.x they were internal, so a rotation control outside Mantis had to implement all of them. The default setupUI(withAllowableFrame:) sets the control's frame to the allowable frame, as the built-in dials do; override it if an attached control needs a different size or lays out subviews there.
    • import Mantis no longer brings in UIKit. The CocoaPods framework re-exported UIKit through its umbrella header; the Swift package does not. Add import UIKit to files that use UIKit types with only import Mantis.
    • CropInfo and CropRegion moved to the new MantisCore module. Mantis re-exports MantisCore, so code that uses CropInfo or Mantis.CropInfo compiles unchanged.
    • Crops of images with scale > 1 keep their full pixel resolution. Mantis 3.x cropped such an image (for example one drawn with UIGraphicsImageRenderer at screen scale) at its point size and discarded the extra pixels, unless maxImagePixelCount sent it through the large-image path. Both paths now return the source's pixel resolution at scale 1, so the cropped image's size in points is scale times what 3.x returned; getExpectedCropImageSize() reports the new size. Images with scale 1, such as photos from the camera or photo library, are unaffected.
    • Xcode 16 or later. The package manifest now uses swift-tools-version:6.0. Mantis itself still compiles in the Swift 5 language mode, and apps can use either language mode.
    • iOS 17 / Mac Catalyst 17 or later. Apps that still support iOS 15 or 16 can stay on Mantis 3.x.

    Migrating from 2.x

    Mantis 3.0 contains a small set of breaking changes; most apps only need the renames below. Everything else — including the binding-based SwiftUI ImageCropperView — is source compatible.

    These four properties had been deprecated forwarding shims; use the replacements:

    Removed in 3.0Use instead
    cropViewConfig.cropBoxHotAreaUnitcropViewConfig.cropAuxiliaryIndicatorConfig.cropBoxHotAreaUnit
    cropViewConfig.disableCropBoxDeformationcropViewConfig.cropAuxiliaryIndicatorConfig.disableCropBoxDeformation
    cropViewConfig.cropAuxiliaryIndicatorStylecropViewConfig.cropAuxiliaryIndicatorConfig.style
    cropViewConfig.showRotationDialcropViewConfig.showAttachedRotationControlView

    Five view-reconstruction fields (skewSublayerTransform, scrollContentOffset, scrollBoundsSize, imageContainerFrame, scrollViewTransform) moved out of the public struct and its init; they were pure view-hierarchy plumbing for the perspective and large-image crop paths.

    • Passing a Mantis-provided CropInfo back into Mantis.crop(image:by:) (from the delegate or getCropInfo()): no change, including across value copies.
    • Rebuilding a CropInfo from persisted semantic fields via the public init for offline re-cropping: no change for the standard path. The perspective-skew and maxImagePixelCount overflow paths return nil for a rebuilt CropInfo — the same outcome the previous zero-value guards produced. As of 3.1, prefer persisting the Codable CropInfo Mantis produced — see Persisting and restoring crops.
    • Tip: for save-and-restore flows, persist the Transformation and feed it back via presetTransformationType = .presetInfo(info:) — that path is unchanged.
    • Undo/redo bookkeeping is now per crop view controller instead of shared globally. If your app shows two croppers at once, each keeps its own undo history (previously they polluted each other's).
    • The Mantis.cropViewController(image:config:) factory now correctly wires up undo/redo when config.enableUndoRedo = true; previously only the setupCropViewController path did.

    Demo Projects

    Mantis provides two demo projects:

    • MantisExample (UIKit, using Storyboard)
    • MantisSwiftUIExample (SwiftUI)
      • Demonstrates the declarative ImageCropper + CropSession API (Mantis 3.0+): normal crop with restore, crop shapes, fixed ratio, slide dial, perspective correction, and a custom toolbar driven by session state.
      • Also includes a "Legacy Binding API" entry showing the binding-based ImageCropperView, kept as a 2.x migration reference.

    Open Example/MantisExample.xcodeproj or SwiftUIExample/MantisSwiftUIExample.xcodeproj directly; both build Mantis from the package at the repository root.

    FAQ

    Set modalPresentationStyle = .fullScreen on the crop view controller (or its navigation controller) before presenting it. On iOS 13+ the default sheet presentation style does not work with the crop UI.

    Use config.cropViewConfig.cropShapeType = .circle() (UIKit) or .cropShape(.circle) (SwiftUI). The exported UIImage is cut to the circle with a transparent background. If you want the exported image to stay rectangular and only show a circular mask in the UI, use .circle(maskOnly: true).

    Two options:

    • Re-crop offline (no UI): persist the Codable CropInfo (Mantis 3.1+) and pass it to Mantis.crop(image:by:) later.
    • Reopen the editor at the saved state: persist the Transformation and restore it via config.cropViewConfig.presetTransformationType = .presetInfo(info:).

    Yes — the crop UI supports iOS and Mac Catalyst, including localized Catalyst menus for undo/redo and independent undo histories for multiple croppers (e.g. iPad multi-window). Native macOS (AppKit) apps can use the MantisCore product to apply saved crops; it has no crop UI. See Cropping on macOS with MantisCore.

    Set config.cropMode = .async to crop off the main thread, and/or set config.cropViewConfig.maxImagePixelCount to cap the output pixel count for very large photos.

    Apps Using Mantis

    Below are apps that use the Mantis framework. If your app also uses Mantis and you'd like it showcased here, please submit a PR following the existing format.

    PictopoemWhere Images Whisper PoemsText Behind MeAdd Depth to Your Photos

    Backers & Sponsors

    If Mantis saves you time, consider becoming a sponsor through GitHub Sponsors.

    Credits

    License

    Mantis is released under the MIT License.