Skip to main content
PESDK/iOS/Guides/User Interface
Language:

Add Custom Tools

If your customization needs go beyond styling menu items and replacing icons, you define custom tool controls by overwriting the tool controllers. In this example, we are adding a custom control for an annotation tool which extends the brush tool to offer several pre-defined pens of different color, size and hardness to allow quick selection of an optimal pen for annotation.

Create custom menu item#

We first create a custom menu item to be displayed alongside other tool icons in the toolbar. To that end we create a UIImage from an icon in our bundle and pass it to a ToolMenuItem constructor along with the tool title and the tool controller class for our custom tool.

Configure menuItems#

When we configure the editor we have to be sure to include this custom menu item to the array of menu items that is assigned to the menuItems configuration option.

Implementing the custom annotation tool#

Define menu items for the annotation tool#

Before we dive into implementing the custom annotation controller, we need to set up the menu items to be displayed inside the tool. Since each menu item corresponds to a pen of a specific color. size and hardness the AnnotationMenuItem needs to store that data as instance variables. Implementing isEqual allows operations on collections of menu items such as diffing. Finally, we define AnnotationListSectionController as section controller responsible for rendering the menu of the custom tool.

Set up the menu item section controller#

The AnnotationListSectionController subclasses the MenuListSectionController. The method cellForItem defines what is to be displayed for a given menu items. In our example, we create an image with the background color of the annotation pen and the title as label. UIImage is extended in our example to generate an image of a given color, size and border radius. The method didUpdate updates the current annotation menu item when one is selected.

Define the annotation tool controller#

Now we can finally turn to our custom annotation controller which extends the BrushToolController. The method viewDidLoad is called when the tool screen loads and is used to set up the tool. In our example, we add the annotation menu items we want to make available to the menuViewController's (see below) menu items and initialize the slider values of the BrushEditController. The method configureToolbarItem allows us to control the appearance of our tools toolbar item such as label text and font.

Implementing the menuViewController method allows us to define what happens when a menu item is selected. Since the last AnnotationMenuItem we added in the CustomToolController should allow the user to define their own annotation pen freely, we delegate to the BrushToolController if the menu item's title equals "Custom". In all other cases, we retrieve the pen properties from the menu item and assign them to the respective BrushEditController's properties.

File:
import PhotoEditorSDK
import UIKit
class PhotoAddCustomToolSwift: Example, PhotoEditViewControllerDelegate {
override func invokeExample() {
// Create a `Photo` from a URL to an image in the app bundle.
let photo = Photo(url: Bundle.main.url(forResource: "LA", withExtension: "jpg")!)
// Create a `Configuration` object.
// highlight-configure-menu-items
let configuration = Configuration { builder in
// Configure the `PhotoEditViewController`.
builder.configurePhotoEditViewController { options in
// Add the custom menu item.
options.menuItems = [createCustomToolMenuItem()] + PhotoEditMenuItem.defaultItems
}
}
// highlight-configure-menu-items
// Create and present the photo editor. Make this class the delegate of it to handle export and cancelation.
let photoEditViewController = PhotoEditViewController(photoAsset: photo, configuration: configuration)
photoEditViewController.delegate = self
photoEditViewController.modalPresentationStyle = .fullScreen
presentingViewController?.present(photoEditViewController, animated: true, completion: nil)
}
// Create a custom `PhotoEditMenuItem` for the custom annotation tool.
// highlight-menu-items
private func createCustomToolMenuItem() -> PhotoEditMenuItem {
let iconURL = Bundle.imgly.resourceBundle.url(forResource: "imgly_icon_tool_brush_48pt", withExtension: "png")!
let iconData = try? Data(contentsOf: iconURL)
let icon = UIImage(data: iconData!)
return .tool(ToolMenuItem(title: "Annotation", icon: icon!, toolControllerClass: CustomToolController.self, supportsPhoto: true, supportsVideo: false)!)
}
// highlight-menu-items
// MARK: - PhotoEditViewControllerDelegate
func photoEditViewControllerShouldStart(_ photoEditViewController: PhotoEditViewController, task: PhotoEditorTask) -> Bool {
// Implementing this method is optional. You can perform additional validation and interrupt the process by returning `false`.
true
}
func photoEditViewControllerDidFinish(_ photoEditViewController: PhotoEditViewController, result: PhotoEditorResult) {
// The image has been exported successfully and is passed as an `Data` object in the `result.output.data`.
// To create an `UIImage` from the output, use `UIImage(data:)`.
// See other examples about how to save the resulting image.
presentingViewController?.dismiss(animated: true, completion: nil)
}
func photoEditViewControllerDidFail(_ photoEditViewController: PhotoEditViewController, error: PhotoEditorError) {
// There was an error generating the photo.
print(error.localizedDescription)
// Dismissing the editor.
presentingViewController?.dismiss(animated: true, completion: nil)
}
func photoEditViewControllerDidCancel(_ photoEditViewController: PhotoEditViewController) {
// The user tapped on the cancel button within the editor. Dismissing the editor.
presentingViewController?.dismiss(animated: true, completion: nil)
}
}
// A custom subclass of `MenuListSectionController` which controls
// the `UICollectionView` menu of the custom tool.
private class AnnotationListSectionController: MenuListSectionController {
// The contained `AnnotationMenuItem`.
private var annotationMenuItem: AnnotationMenuItem?
// Return a dequeued cell for a given index.
open override func cellForItem(at index: Int) -> UICollectionViewCell {
guard let cell = super.cellForItem(at: index) as? MenuCollectionViewCell,
let annotationMenuItem = annotationMenuItem else {
fatalError()
}
cell.iconImageView.image = UIImage(color: annotationMenuItem.color, size: CGSize(width: 44, height: 44), cornerRadius: 22)
cell.captionTextLabel.text = annotationMenuItem.title
return cell
}
// Updates the section controller to a new object.
open override func didUpdate(to object: Any) {
super.didUpdate(to: object)
if let annotationMenuItem = object as? AnnotationMenuItem {
self.annotationMenuItem = annotationMenuItem
}
}
}
// A custom `MenuItem` used to display the various annotation
// pens in the menu of the `CustomToolController`.
private class AnnotationMenuItem: NSObject, MenuItem {
// The title of the pen.
let title: String
// The color of the pen.
let color: UIColor
// The hardness of the pen.
let hardness: CGFloat
// The size of the pen stroke.
let size: CGFloat
// Initializes a new `AnnotationMenuItem` with all available
// properties.
init(title: String, color: UIColor, hardness: CGFloat, size: CGFloat) {
self.title = title
self.color = color
self.hardness = hardness
self.size = size
super.init()
}
// The unique identifier.
var diffIdentifier: NSObjectProtocol {
self
}
// Determines whether the instance is equal to another `Diffable`.
func isEqual(toDiffableObject object: Diffable?) -> Bool {
guard self !== object else { return true }
guard let object = object as? AnnotationMenuItem else { return false }
return title == object.title
&& color == object.color
&& hardness == object.hardness
&& size == object.size
}
// Determines the `MenuListSectionController` type for the menu.
static var sectionControllerType: MenuListSectionController.Type {
AnnotationListSectionController.self
}
}
// A custom tool controller used for annotation.
private class CustomToolController: BrushToolController {
override func viewDidLoad() {
// Add custom menu items to the menu.
menuViewController.menuItems = [
AnnotationMenuItem(title: "Highlight", color: UIColor.yellow, hardness: 0.5, size: 20),
AnnotationMenuItem(title: "White Out", color: UIColor.white, hardness: 0.6, size: 5),
AnnotationMenuItem(title: "Black Pen", color: UIColor.black, hardness: 0.7, size: 10),
AnnotationMenuItem(title: "Blue Pen", color: UIColor.blue, hardness: 0.8, size: 50),
AnnotationMenuItem(title: "Red Pen", color: UIColor.red, hardness: 0.9, size: 1),
AnnotationMenuItem(title: "Custom", color: UIColor.white, hardness: 1, size: 1)
]
menuViewController.reloadData(completion: nil)
brushEditController.sliderEditController.slider.neutralValue = 1
brushEditController.sliderEditController.slider.maximumValue = 100
brushEditController.sliderEditController.slider.minimumValue = 1
brushEditController.activeBrushTool = .size
super.viewDidLoad()
}
// Configure the toolbar to display the tool name.
override func configureToolbarItem() {
super.configureToolbarItem()
guard let toolbarItem = toolbarItem as? DefaultToolbarItem else {
return
}
toolbarItem.titleLabel.attributedText = NSAttributedString(
string: "ANNOTATION",
attributes: [
.kern: 1.2,
.font: UIFont.systemFont(ofSize: 12, weight: .medium)
]
)
}
// Define what the tool should do if one of the menu items has been selected.
override func menuViewController(_ menuViewController: MenuViewController, didSelect menuItem: MenuItem) {
guard let menuItem = menuItem as? AnnotationMenuItem else {
return
}
if menuItem.title == "Custom" {
let brushToolController = BrushToolController(configuration: configuration, productType: .pesdk)!
notifySubscribers { $0.photoEditToolController(self, wantsToPresent: brushToolController) }
} else {
brushEditController.hardness = menuItem.hardness
brushEditController.color = menuItem.color
brushEditController.size = menuItem.size
brushEditController.sliderEditController.slider.value = menuItem.size
}
super.menuViewController(menuViewController, didSelect: menuItem)
}
}
private extension UIImage {
// Generates an `UIImage` with a given color, size and corner radius.
convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1), cornerRadius: CGFloat = 0.0) {
let rect = CGRect(origin: .zero, size: size)
let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
color.setFill()
path.fill()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let cgImage = image?.cgImage else { return nil }
self.init(cgImage: cgImage, scale: UIScreen.main.scale, orientation: .up)
}
}