Skip to main content
Language:

Custom Smart Stickers

VideoEditor SDK supports implementation of custom smart stickers which gives you a full control on how sticker is rendered. If input from the user is needed, smart sticker API allows presentation of custom view controller, that you can use to prompt the user to enter needed data.

In this example we will show how you can implement a custom smart sticker that holds the URL that is entered by the user. We will implement LinkSmartSticker and LinkSmartStickerViewController.

Define LinkSmartSticker#

We need to define our LinkSmartSticker here. This class is used to describe how the sticker is rendered. It has access to the metadata that is provided by LinkSmartStickerViewController, which is defined below in this example.

We will create a link sticker that allows us to enter a link that will then be rendered next to an icon in a prettyfied form. Full link that was entered or copied to the prompt will be available in the field metadata on StickerSpriteModel when the sticker is placed so it can be used in a later stage for processing along with other data.

Define sticker properties#

We will define multiple properties that will be used when rendering the sticker. Some are constant and are not changing between variations.

Set sticker prompt#

We need to override prompt with our custom view controller defined below. This view controller will be presented when our LinkSmartSticker is selected in the sticker selection menu. Make sure that you pass the instance of self to the prompt, so the renderer knows what sticker to render.

Implement rendering methods#

There are 2 methods that need to be implemented. First we need to implement size(for metadata: [String: String]? = nil) -> CGSize which will tell the renderer what size is requested based on the given metadata. Then we need to implement draw(with metadata: [String: String]?, context: CGContext, size: CGSize, scale: CGFloat) which is responsible for rendering.

Implement LinkSmartStickerViewController#

Here is the implementation of the LinkSmartStickerViewController which will be shown to enter data. Most of this is our user interface code, but note that there are 2 important methods that need to be called. These are done and cancel. done is used to pass the metadata to the sticker renderer so it can be placed on the canvas, and cancel method is used to dismiss the prompt.

Define variations of LinkSmartSticker#

This class begins by creating a Video object from an image in the app bundle, and then creating an AssetCatalog object with default items. Next, it defines several variations of a LinkSmartSticker object, which is a subclass of SmartSticker that can be used to open a URL when tapped. These variations are defined by different colors for the background box, text, and icon.

Create a StickerCategory with the MultiLinkSmartSticker#

The LinkSmartSticker variations are then added to a MultiImageSticker object, which allows users to switch between the variations by tapping on the sticker on the canvas. The MultiImageSticker is then added to a StickerCategory object, which holds all the stickers that will be available to the user.

Create a Configuration object#

Finally, a Configuration object is created and assigned the AssetCatalog object that was created earlier. This configuration is then passed to a VideoEditViewController object, which is presented to the user. The current class is also set as the delegate of the VideoEditViewController to handle export and cancelation.

Query the URLs#

When the image is exported, we can access the entered metadata from our custom stickers.

File:
import UIKit
import VideoEditorSDK
extension VideoLinkSmartSticker {
/// Custom `SmartSticker` that will present a prompt when sticker is selected.
///
/// In this example we will create a link sticker that allows us to enter a link, that will then be rendered next to an icon
/// in a prettyfied form. Full link that was entered or copied to the prompt will be available in the field `metadata` on
/// `StickerSpriteModel` when the sticker is placed so it can be used in a later stage for processing along with other data.
@objc(VLinkSmartSticker) class LinkSmartSticker: SmartSticker {
// We will define multiple properties that will be used when rendering
// the sticker. Some are constant and are not changing between variations.
// Corner radius of the background box.
private let cornerRadius = 10
// Font size used for link text.
private let fontSize = 17.0
// Content padding.
private let padding: UIEdgeInsets = .init(top: 8, left: 16, bottom: 8, right: 16)
private let iconSize = CGSize(width: 24, height: 24)
// A text that will be shown when no metadata is provided which is when
// preview sticker is being rendered.
private let previewText: String = "Link".uppercased()
// Three variable properties that are different between variations.
// We pass these through the constructor.
let textColor: UIColor
let boxColor: UIColor
let iconColor: UIColor
// A prompt that will be presented when sticker is selected in the sticker
// selection view. It must implement `SmartSticker.PromptViewController`, which
// is in our example implemented below the LinkSmartSticker. We pass the instance
// of our sticker so we later know which sticker is used, so a certain PromptViewController
// could be reused.
override var prompt: SmartSticker.PromptViewController? {
LinkSmartStickerViewController(sticker: self)
}
var textAttributes: [NSAttributedString.Key: Any] {
[
.font: UIFont.boldSystemFont(ofSize: fontSize),
.foregroundColor: textColor
]
}
init(identifier: String, textColor: UIColor, boxColor: UIColor, iconColor: UIColor) {
self.textColor = textColor
self.boxColor = boxColor
self.iconColor = iconColor
super.init(identifier: identifier)
}
private func linkText(from metadata: [String: String]? = nil) -> String {
guard let metadata = metadata else { return previewText }
let url = URL(string: metadata["url"]!)!
return url.host?.lowercased() ?? previewText
}
// We will implement `size` method here, that will return the size we are
// requesting to draw sticker on.
override func size(for metadata: [String: String]? = nil) -> CGSize {
// First we extract link text from metadata. If metadata is nil, we
// will return preview text which is "LINK" in our case
let text = linkText(from: metadata)
// We calculate text screen size with our helper method.
let textSize = text.imgly.bounds(textAttributes).size
// Calculate width, which consists of icon width, padding, and text screen width.
let stickerWidth = iconSize.width + 8 + textSize.width
// Calculate height, which consists of icon height, and text screen height.
let stickerHeight = max(iconSize.height, textSize.height)
// We return the calculated size which we also add paddding onto.
return CGRect(x: 0, y: 0, width: stickerWidth + padding.left + padding.right, height: stickerHeight + padding.top + padding.bottom).size
}
// We will implement `draw` method here, that will render the sticker in graphic
// context given by renderer.
override func draw(with metadata: [String: String]?, context: CGContext, size: CGSize, scale: CGFloat) {
// We create rectangle of size that is passed to us. We need to make
// sure, our content is scaled correctly to this size.
let rect = CGRect(origin: .zero, size: size)
// Here we extract link text from metadata. If metadata is nil, we
// will return preview text which is "LINK" in our case
let linkString = linkText(from: metadata)
// We calculate text screen size with our helper method.
let textSize = linkString.imgly.bounds(textAttributes).size
// We calculate our requested size so we can calculate scaling factor.
let stickerRect = CGRect(origin: .zero, size: self.size(for: metadata))
// Calculate icon and text positions
let iconRect = CGRect(origin: .zero, size: iconSize).offsetBy(dx: padding.left, dy: padding.top)
let dy = (iconRect.height - textSize.height) / 2 + padding.top
let textRect = CGRect(origin: .zero, size: textSize).offsetBy(dx: iconRect.maxX + 8, dy: dy)
// We fit our requested rectangle into the given rectangle
let fittedRect = stickerRect.imgly.fitted(into: rect, with: .scaleAspectFit)
// Calculate scale ratio
let fittedScale = fittedRect.width / stickerRect.width
// Apply scaling and translation of our context
context.translateBy(x: fittedRect.minX, y: fittedRect.minY)
context.scaleBy(x: fittedScale, y: fittedScale)
// Render background rounded rectangle
context.setFillColor(boxColor.cgColor)
context.imgly.addRoundedRect(of: stickerRect.size, cornerRadius: CGSize(width: cornerRadius, height: cornerRadius))
context.fillPath()
// Render link icon
let image = UIImage(systemName: "link")?.withTintColor(iconColor)
image?.draw(in: iconRect)
// Render link text
linkString.imgly.draw(in: textRect, context: context, withAttributes: textAttributes)
}
}
/// Custom` UIViewController` that will get presented when a certain smart sticker is selected.
/// It is used to feed arbitrary data to the sticker. You can customize it in any way, just make sure you
/// call either `done(metadata: [String: String])` to render the sticker with `metadata` or
/// `cancel()` to dismiss this `SmartSticker.PromptViewController`.
///
/// In this example we will configure it in a way it will show two buttons for cancelling and applying in the navigation bar,
/// and a text box below that will be used to enter URL that will be used when rendering `LinkSmartSticker` .
@objc class LinkSmartStickerViewController: SmartSticker.PromptViewController {
// MARK: - Properties
private let linkTextField: UITextField = {
let textField = UITextField()
textField.borderStyle = .none
textField.placeholder = "https://example.com"
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
// MARK: - View creation
private func createNavigationBar() -> UINavigationBar {
let navigationBar = UINavigationBar()
navigationBar.translatesAutoresizingMaskIntoConstraints = false
let navigationItem = UINavigationItem(title: "Add Link")
let doneItem = UIBarButtonItem(barButtonSystemItem: .done, target: nil, action: #selector(addLinkTapped))
let cancelItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: nil, action: #selector(cancelTapped))
navigationItem.rightBarButtonItem = doneItem
navigationItem.leftBarButtonItem = cancelItem
navigationBar.setItems([navigationItem], animated: false)
return navigationBar
}
private func createURLLabel() -> UILabel {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 14)
label.textColor = UIColor.lightGray
label.text = "URL"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}
private func setupView() {
let navigationBar = createNavigationBar()
let urlLabel = createURLLabel()
view.addSubview(navigationBar)
view.addSubview(urlLabel)
view.addSubview(linkTextField)
NSLayoutConstraint.activate(
[
navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
navigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
navigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
urlLabel.topAnchor.constraint(equalTo: navigationBar.safeAreaLayoutGuide.bottomAnchor, constant: 16),
urlLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
linkTextField.topAnchor.constraint(equalTo: urlLabel.bottomAnchor, constant: 8),
linkTextField.leadingAnchor.constraint(equalTo: urlLabel.leadingAnchor)
]
)
}
// MARK: - UIViewController
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupView()
}
override func viewWillAppear(_ animated: Bool) {
linkTextField.becomeFirstResponder()
}
// MARK: - Actions
@objc func addLinkTapped() {
guard var text = linkTextField.text?.trimmingCharacters(in: .whitespaces) else { return }
if !text.starts(with: "http://"), !text.starts(with: "https://") {
text = "https://\(text)"
}
if URL(string: text) != nil {
// We call `done` here with desired metadata,
// that will be passed to `SmartSticker.draw` method
// when sticker instance is rendered.
done(metadata: ["url": text])
}
}
@objc func cancelTapped() {
// In case we don't want to proceed with sticker
// we selected, calling `cancel` will dismiss this
// `SmartSticker.PromptViewController`.
cancel()
}
}
}
class VideoLinkSmartSticker: Example, VideoEditViewControllerDelegate {
override func invokeExample() {
// Create a `Video` from a URL to a video in the app bundle.
let video = Video(url: Bundle.main.url(forResource: "Skater", withExtension: "mp4")!)
// Create an asset catalog with default items that we will use to add
// our smart sticker.
let assetCatalog = AssetCatalog.defaultItems
// For this example we will create a smart sticker with multiple
// variations so we will define few colors for background box, text
// and icon color.
let boxColors = [
UIColor(red: 246, green: 246, blue: 246).withAlphaComponent(0.55),
UIColor(red: 246, green: 246, blue: 246),
UIColor(red: 51, green: 51, blue: 55).withAlphaComponent(0.55),
UIColor(red: 51, green: 51, blue: 55)
]
let textColors = [
UIColor(red: 51, green: 51, blue: 55),
UIColor(red: 51, green: 51, blue: 55),
UIColor(red: 246, green: 246, blue: 246),
UIColor(red: 246, green: 246, blue: 246)
]
let iconColors = [
UIColor(red: 51, green: 51, blue: 55),
UIColor(red: 38, green: 119, blue: 253),
UIColor(red: 246, green: 246, blue: 246),
UIColor(red: 255, green: 92, blue: 0)
]
// Create 4 instances of LinkSmartSticker that is defined below in
// the code.
let multiLinkStickers = (0 ..< 4).map {
LinkSmartSticker(identifier: "imgly_link_smart_sticker_\($0 + 1)", textColor: textColors[$0], boxColor: boxColors[$0], iconColor: iconColors[$0])
}
// When we have stickers generated we will add them to a MultiImageSticker
// that will allow us to change between variations with tapping on the
// sticker on the canvas.
let multiLinkSticker = MultiImageSticker(identifier: "imgly_link_smart_sticker", imageURL: nil, stickers: multiLinkStickers)
// For this example we will create a sticker category that will only hold
// our Multi LinkSmartSticker.
let stickerCategory = StickerCategory(identifier: "smart_stickers", title: "Smart Stickers", imageURL: Bundle.imgly.resourceBundle.url(forResource: "imgly_sticker_shapes_badge_28", withExtension: "png")!, stickers: [multiLinkSticker])
assetCatalog.stickers = [stickerCategory]
// Create a `Configuration` object.
let configuration = Configuration { builder in
// Assign asset catalog we defined before to the configuration builder.
builder.assetCatalog = assetCatalog
}
// Create and present the Video editor. Make this class the delegate of it to handle export and cancelation.
let VideoEditViewController = VideoEditViewController(videoAsset: video, configuration: configuration)
VideoEditViewController.delegate = self
VideoEditViewController.modalPresentationStyle = .fullScreen
presentingViewController?.present(VideoEditViewController, animated: true, completion: nil)
}
// MARK: - VideoEditViewControllerDelegate
func videoEditViewControllerShouldStart(_ VideoEditViewController: VideoEditViewController, task: VideoEditorTask) -> Bool {
// Implementing this method is optional. You can perform additional validation and interrupt the process by returning `false`.
true
}
func videoEditViewControllerDidFinish(_ VideoEditViewController: VideoEditViewController, result: VideoEditorResult) {
// 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.
// Here we can query what URLs were entered by the user.
let stickerLinks = result.task.model.spriteModels.compactMap { ($0 as? StickerSpriteModel)?.metadata?["url"] }
print(stickerLinks)
presentingViewController?.dismiss(animated: true, completion: nil)
}
func videoEditViewControllerDidFail(_ VideoEditViewController: VideoEditViewController, error: VideoEditorError) {
// There was an error generating the Video.
print(error.localizedDescription)
// Dismissing the editor.
presentingViewController?.dismiss(animated: true, completion: nil)
}
func videoEditViewControllerDidCancel(_ VideoEditViewController: VideoEditViewController) {
// The user tapped on the cancel button within the editor. Dismissing the editor.
presentingViewController?.dismiss(animated: true, completion: nil)
}
}