diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..2714f53 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.6.4 diff --git a/Cartfile.resolved b/Cartfile.resolved index 0d4ea38..d57c23f 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1,2 @@ -github "Quick/Nimble" "v8.0.5" +github "Quick/Nimble" "v8.0.7" github "TimOliver/TOCropViewController" "2.5.2" diff --git a/MediaEditor.podspec b/MediaEditor.podspec index f64a7be..4dc30fb 100644 --- a/MediaEditor.podspec +++ b/MediaEditor.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'MediaEditor' - s.version = '1.0.1' + s.version = '1.1.0' s.summary = 'An extensible Media Editor for iOS.' s.description = <<-DESC diff --git a/MediaEditor.xcodeproj/project.pbxproj b/MediaEditor.xcodeproj/project.pbxproj index 3dc6d6c..03dee92 100644 --- a/MediaEditor.xcodeproj/project.pbxproj +++ b/MediaEditor.xcodeproj/project.pbxproj @@ -7,6 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 17002A9D245C27400021216C /* MediaEditorDrawing.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17002A9C245C27400021216C /* MediaEditorDrawing.storyboard */; }; + 17002A9F245C54160021216C /* MediaEditorAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17002A9E245C54150021216C /* MediaEditorAnnotationView.swift */; }; + 178126E62460B25300253107 /* MediaEditorDrawingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178126E52460B25300253107 /* MediaEditorDrawingTests.swift */; }; + 178126E82461A2ED00253107 /* demo-drawing in Resources */ = {isa = PBXBuildFile; fileRef = 178126E72461A2DA00253107 /* demo-drawing */; }; + 17DBA238245B1507006CD67F /* MediaEditorDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DBA237245B1507006CD67F /* MediaEditorDrawing.swift */; }; 8B05570523E1BF5900C10787 /* DeviceLibraryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05570423E1BF5900C10787 /* DeviceLibraryViewController.swift */; }; 8B05570723E1C1D800C10787 /* ImageViewCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05570623E1C1D800C10787 /* ImageViewCollectionCell.swift */; }; 8B05570923E1CF2E00C10787 /* PlainUIImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05570823E1CF2E00C10787 /* PlainUIImageViewController.swift */; }; @@ -92,6 +97,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 17002A9C245C27400021216C /* MediaEditorDrawing.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MediaEditorDrawing.storyboard; sourceTree = ""; }; + 17002A9E245C54150021216C /* MediaEditorAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEditorAnnotationView.swift; sourceTree = ""; }; + 178126E52460B25300253107 /* MediaEditorDrawingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEditorDrawingTests.swift; sourceTree = ""; }; + 178126E72461A2DA00253107 /* demo-drawing */ = {isa = PBXFileReference; lastKnownFileType = file; path = "demo-drawing"; sourceTree = ""; }; + 17DBA237245B1507006CD67F /* MediaEditorDrawing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEditorDrawing.swift; sourceTree = ""; }; 8B05570423E1BF5900C10787 /* DeviceLibraryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLibraryViewController.swift; sourceTree = ""; }; 8B05570623E1C1D800C10787 /* ImageViewCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewCollectionCell.swift; sourceTree = ""; }; 8B05570823E1CF2E00C10787 /* PlainUIImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainUIImageViewController.swift; sourceTree = ""; }; @@ -170,6 +180,25 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 178126E42460B23700253107 /* Drawing */ = { + isa = PBXGroup; + children = ( + 178126E72461A2DA00253107 /* demo-drawing */, + 178126E52460B25300253107 /* MediaEditorDrawingTests.swift */, + ); + path = Drawing; + sourceTree = ""; + }; + 17DBA236245B14F5006CD67F /* Drawing */ = { + isa = PBXGroup; + children = ( + 17002A9C245C27400021216C /* MediaEditorDrawing.storyboard */, + 17DBA237245B1507006CD67F /* MediaEditorDrawing.swift */, + 17002A9E245C54150021216C /* MediaEditorAnnotationView.swift */, + ); + path = Drawing; + sourceTree = ""; + }; 8B05570E23E1F63A00C10787 /* BrightnessCapability */ = { isa = PBXGroup; children = ( @@ -336,6 +365,7 @@ 8B5046C823D7CE1600068F66 /* Capabilities */ = { isa = PBXGroup; children = ( + 17DBA236245B14F5006CD67F /* Drawing */, 8B5046C923D7CE1600068F66 /* Crop */, 8B062DC723E865F800488F80 /* Filters */, 8B5046CC23D7CE1600068F66 /* MediaEditorCapability.swift */, @@ -376,6 +406,7 @@ 8B50472323D7D36C00068F66 /* Capabilities */ = { isa = PBXGroup; children = ( + 178126E42460B23700253107 /* Drawing */, 8B50472423D7D36C00068F66 /* Crop */, 8B062DCE23E87A7900488F80 /* Filters */, ); @@ -521,6 +552,7 @@ 8B5046E023D7CE1600068F66 /* MediaEditorHub.storyboard in Resources */, 8B5046DF23D7CE1600068F66 /* Media.xcassets in Resources */, 8B062DCB23E8661400488F80 /* MediaEditorFilters.storyboard in Resources */, + 17002A9D245C27400021216C /* MediaEditorDrawing.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -539,6 +571,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 178126E82461A2ED00253107 /* demo-drawing in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -573,9 +606,11 @@ files = ( 8B5046E223D7CE1600068F66 /* MediaEditorThumbCell.swift in Sources */, 8B062DD423E8925100488F80 /* MediaEditorFilterCell.swift in Sources */, + 17DBA238245B1507006CD67F /* MediaEditorDrawing.swift in Sources */, 8B5046D623D7CE1600068F66 /* UIImage+AsyncImage.swift in Sources */, 8B5046DE23D7CE1600068F66 /* AsyncImage.swift in Sources */, 8B5046DC23D7CE1600068F66 /* MediaEditorCapability.swift in Sources */, + 17002A9F245C54160021216C /* MediaEditorAnnotationView.swift in Sources */, 8B062DCD23E8663C00488F80 /* UIImage+withSize.swift in Sources */, 8B062DD723E8937100488F80 /* MediaEditorFilters.swift in Sources */, 8B5046DB23D7CE1600068F66 /* MediaEditorCropZoomRotate.swift in Sources */, @@ -614,6 +649,7 @@ 8B50472F23D87DA400068F66 /* UIImage+color.swift in Sources */, 8B50472723D7D36C00068F66 /* MediaEditorHubTests.swift in Sources */, 8B50472923D7D36C00068F66 /* MediaEditorTests.swift in Sources */, + 178126E62460B25300253107 /* MediaEditorDrawingTests.swift in Sources */, 8B50472823D7D36C00068F66 /* MediaEditorCropZoomRotateTests.swift in Sources */, 8B062DD023E87A9400488F80 /* MediaEditorFilterTests.swift in Sources */, ); diff --git a/README.md b/README.md index aa0d860..72abf1d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ MediaEditor is an extendable library for iOS that allows you to quickly and easily add image editing features to your app. You can edit single or multiple images, from the device's library or any other source. It has been designed to feel natural and part of the OS.

- +

# Features @@ -18,6 +18,7 @@ MediaEditor is an extendable library for iOS that allows you to quickly and easi - [x] Editing in both portrait and landscape modes - [x] Cool filters - [x] Crop, zoom and rotate capability (thanks to [`TOCropViewController`](https://github.com/TimOliver/TOCropViewController)) +- [x] PencilKit support to annotate images - [x] Easily extendable - [x] Customizable UI diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index b20a9f5..98071a9 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,3 +1,7 @@ +1.1.0 +----- +* Add Drawing capability using PencilKit + 1.0.1 ----- * Expose the Hub diff --git a/Sources/Capabilities/Drawing/MediaEditorAnnotationView.swift b/Sources/Capabilities/Drawing/MediaEditorAnnotationView.swift new file mode 100644 index 0000000..1ac0377 --- /dev/null +++ b/Sources/Capabilities/Drawing/MediaEditorAnnotationView.swift @@ -0,0 +1,230 @@ +import UIKit +import AVFoundation +import PencilKit + +@available(iOS 13.0, *) +protocol MediaEditorAnnotationViewUndoObserver: NSObject { + func mediaEditorAnnotationView(_ annotationView: MediaEditorAnnotationView, isHidingUndoControls: Bool) + func mediaEditorAnnotationViewUndoStatusDidChange(_ view: MediaEditorAnnotationView) +} + +/// Wrapper view that contains an image view and a PencilKit canvas to allow +/// drawing on top of the image. +/// +@available(iOS 13.0, *) +class MediaEditorAnnotationView: UIView { + + private let imageView = UIImageView() + private let canvasView = PKCanvasView() + + private var bottomConstraint: NSLayoutConstraint! + + weak var undoObserver: MediaEditorAnnotationViewUndoObserver? + + var canUndo: Bool { + return canvasView.undoManager?.canUndo ?? false + } + + var canRedo: Bool { + return canvasView.undoManager?.canRedo ?? false + } + + var image: UIImage? { + set { + imageView.image = newValue + } + get { + return renderedImage + } + } + + // Primarily for testing purposes + var drawingData: Data { + set { + do { + canvasView.drawing = try PKDrawing(data: newValue) + } catch { + print("Error setting annotation view drawing data.") + } + } + get { + return canvasView.drawing.dataRepresentation() + } + } + + // MARK: - Initialization + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + deinit { + undoObserver = nil + + NotificationCenter.default.removeObserver(self, + name: NSNotification.Name.NSUndoManagerCheckpoint, + object: canvasView.undoManager) + } + + private func commonInit() { + configureImageView() + configureCanvasView() + } + + private func configureImageView() { + addSubview(imageView) + imageView.translatesAutoresizingMaskIntoConstraints = false + + imageView.contentMode = .scaleAspectFit + + bottomConstraint = imageView.bottomAnchor.constraint(equalTo: bottomAnchor) + + NSLayoutConstraint.activate([ + imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + imageView.trailingAnchor.constraint(equalTo: trailingAnchor), + imageView.topAnchor.constraint(equalTo: topAnchor), + bottomConstraint + ]) + } + + private func configureCanvasView() { + addSubview(canvasView) + + canvasView.backgroundColor = .clear + canvasView.isOpaque = false + + // Ensure ink remains the same color regardless of light / dark mode + canvasView.overrideUserInterfaceStyle = .light + + NotificationCenter.default.addObserver(forName: NSNotification.Name.NSUndoManagerCheckpoint, object: canvasView.undoManager, queue: nil) { [weak self] _ in + self?.notifyUndoObserver() + } + } + + fileprivate func notifyUndoObserver() { + undoObserver?.mediaEditorAnnotationViewUndoStatusDidChange(self) + } + + // MARK: - View Layout + + override func layoutSubviews() { + super.layoutSubviews() + + let currentFrame = canvasView.frame + let newFrame = calculateCanvasFrame() + canvasView.frame = newFrame + + // If the canvas has changed size (e.g. due to device rotation) apply a transform + // to the drawing so that it still fits the scaled imageview + let transform = CGAffineTransform(scaleX: newFrame.width / currentFrame.width, y: newFrame.height / currentFrame.height) + self.canvasView.drawing.transform(using: transform) + } + + private func calculateCanvasFrame() -> CGRect { + guard let image = imageView.image, + imageView.contentMode == .scaleAspectFit, + image.size.width > 0 && image.size.height > 0 else { + return imageView.bounds + } + + let size = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds) + + let x = (imageView.bounds.width - size.width) * 0.5 + let y = (imageView.bounds.height - size.height) * 0.5 + + return CGRect(x: x, y: y, width: size.width, height: size.height) + } + + // MARK: - Public methods + + /// Displays the system tool picker in the specified window + /// + func showTools(in window: UIWindow) { + if let toolPicker = PKToolPicker.shared(for: window) { + toolPicker.setVisible(true, forFirstResponder: canvasView) + toolPicker.addObserver(canvasView) + toolPicker.addObserver(self) + + canvasView.becomeFirstResponder() + updateLayout(for: toolPicker) + } + } + + /// Renders the initial image with the canvas's image overlaid on top + /// into a single UIImage instance. + /// + private var renderedImage: UIImage? { + guard let imageViewImage = imageView.image else { + return nil + } + + guard canvasView.bounds != .zero else { + return imageViewImage + } + + // Check we actually have some changes + if let undoManager = canvasView.undoManager, + undoManager.canUndo == false { + return imageViewImage + } + + let targetSize = imageViewImage.size + + let canvasViewImage = canvasView.drawing.image(from: canvasView.bounds, scale: UIScreen.main.scale) + + let renderer = UIGraphicsImageRenderer(size: targetSize, format: .default()) + let renderedImage = renderer.image { context in + imageViewImage.draw(at: .zero) + canvasViewImage.draw(in: CGRect(origin: .zero, size: targetSize)) + } + + return renderedImage + } +} + +// Note: Code in this extension reused from WWDC 2019 PencilKit example +// +@available(iOS 13.0, *) +extension MediaEditorAnnotationView: PKToolPickerObserver { + // MARK: Tool Picker Observer + + /// Delegate method: Note that the tool picker has changed which part of the canvas view + /// it obscures, if any. + internal func toolPickerFramesObscuredDidChange(_ toolPicker: PKToolPicker) { + updateLayout(for: toolPicker) + } + + /// Delegate method: Note that the tool picker has become visible or hidden. + internal func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) { + updateLayout(for: toolPicker) + } + + /// Helper method to adjust the canvas view size when the tool picker changes which part + /// of the canvas view it obscures, if any. + /// + /// Note that the tool picker floats over the canvas in regular size classes, but docks to + /// the canvas in compact size classes, occupying a part of the screen that the canvas + /// could otherwise use. + fileprivate func updateLayout(for toolPicker: PKToolPicker) { + let obscuredFrame = toolPicker.frameObscured(in: self) + + if obscuredFrame.isNull { + bottomConstraint.constant = 0 + undoObserver?.mediaEditorAnnotationView(self, isHidingUndoControls: false) + } else { + bottomConstraint.constant = -obscuredFrame.height + undoObserver?.mediaEditorAnnotationView(self, isHidingUndoControls: true) + } + + setNeedsLayout() + layoutIfNeeded() + + canvasView.scrollIndicatorInsets = canvasView.contentInset + } +} diff --git a/Sources/Capabilities/Drawing/MediaEditorDrawing.storyboard b/Sources/Capabilities/Drawing/MediaEditorDrawing.storyboard new file mode 100644 index 0000000..b881ad9 --- /dev/null +++ b/Sources/Capabilities/Drawing/MediaEditorDrawing.storyboard @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/Capabilities/Drawing/MediaEditorDrawing.swift b/Sources/Capabilities/Drawing/MediaEditorDrawing.swift new file mode 100644 index 0000000..dd5d643 --- /dev/null +++ b/Sources/Capabilities/Drawing/MediaEditorDrawing.swift @@ -0,0 +1,120 @@ +import UIKit + +@available(iOS 13.0, *) +class MediaEditorDrawing: UIViewController { + + @IBOutlet weak var annotationView: MediaEditorAnnotationView! + @IBOutlet weak var cancelButton: UIButton! + @IBOutlet weak var doneButton: UIButton! + @IBOutlet weak var undoButton: UIButton! + @IBOutlet weak var redoButton: UIButton! + + var image: UIImage! + + let context = MediaEditor.ciContext + + var onFinishEditing: ((UIImage, [MediaEditorOperation]) -> ())? + + var onCancel: (() -> ())? + + static func initialize() -> MediaEditorDrawing { + return UIStoryboard( + name: "MediaEditorDrawing", + bundle: Bundle(for: MediaEditorDrawing.self) + ).instantiateViewController(withIdentifier: "drawingViewController") as! MediaEditorDrawing + } + + override func viewDidLoad() { + super.viewDidLoad() + + // Setting images in code to avoid an 'iOS 13 only' warning in the storyboard + undoButton.setImage(UIImage(systemName: "arrow.uturn.left.circle"), for: .normal) + redoButton.setImage(UIImage(systemName: "arrow.uturn.right.circle"), for: .normal) + undoButton.setTitle(nil, for: .normal) + redoButton.setTitle(nil, for: .normal) + + annotationView.undoObserver = self + annotationView.image = image + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // Based on Apple's sample PencilKit code from WWDC 2019: https://developer.apple.com/documentation/pencilkit/drawing_with_pencilkit + // Set up the tool picker, using the window of our parent because our view has not + // been added to a window yet. + if let window = parent?.view.window { + annotationView.showTools(in: window) + } + } + + // MARK: - Actions + + @IBAction func cancel(_ sender: Any) { + onCancel?() + } + + @IBAction func done(_ sender: Any) { + guard annotationView.canUndo, + let image = annotationView.image else { + onCancel?() + return + } + + onFinishEditing?(image, [.draw]) + } +} + +@available(iOS 13.0, *) +extension MediaEditorDrawing: MediaEditorCapability { + static var name = "Drawing" + + static var icon = UIImage(systemName: "pencil.tip.crop.circle")! + + static func initialize(_ image: UIImage, onFinishEditing: @escaping (UIImage, [MediaEditorOperation]) -> (), onCancel: @escaping () -> ()) -> CapabilityViewController { + let viewController: MediaEditorDrawing = MediaEditorDrawing.initialize() + viewController.onFinishEditing = onFinishEditing + viewController.onCancel = onCancel + viewController.image = image + return viewController + } + + func apply(styles: MediaEditorStyles) { + if let doneLabel = styles[.doneLabel] as? String { + doneButton.setTitle(doneLabel, for: .normal) + } + + if let cancelLabel = styles[.cancelLabel] as? String { + cancelButton.setTitle(cancelLabel, for: .normal) + } + + if let cancelColor = styles[.cancelColor] as? UIColor { + cancelButton.tintColor = cancelColor + undoButton.tintColor = cancelColor + redoButton.tintColor = cancelColor + } + + if let undoIcon = styles[.undoIcon] as? UIImage { + undoButton.setImage(undoIcon, for: .normal) + } + + if let redoIcon = styles[.redoIcon] as? UIImage { + redoButton.setImage(redoIcon, for: .normal) + } + } +} + +@available(iOS 13.0, *) +extension MediaEditorDrawing: MediaEditorAnnotationViewUndoObserver { + func mediaEditorAnnotationView(_ annotationView: MediaEditorAnnotationView, isHidingUndoControls: Bool) { + let shouldShowCustomControls = !isHidingUndoControls + + undoButton.isHidden = shouldShowCustomControls + redoButton.isHidden = shouldShowCustomControls + } + + func mediaEditorAnnotationViewUndoStatusDidChange(_ view: MediaEditorAnnotationView) { + undoButton.isEnabled = view.canUndo + redoButton.isEnabled = view.canRedo + } +} diff --git a/Sources/Enums/MediaEditorOperation.swift b/Sources/Enums/MediaEditorOperation.swift index 9483fac..a4d75ed 100644 --- a/Sources/Enums/MediaEditorOperation.swift +++ b/Sources/Enums/MediaEditorOperation.swift @@ -5,4 +5,5 @@ public enum MediaEditorOperation { case rotate case filter case other + case draw } diff --git a/Sources/Enums/MediaEditorStyle.swift b/Sources/Enums/MediaEditorStyle.swift index 3f01a87..79553e7 100644 --- a/Sources/Enums/MediaEditorStyle.swift +++ b/Sources/Enums/MediaEditorStyle.swift @@ -17,4 +17,6 @@ public enum MediaEditorStyle { case selectedColor case errorLoadingImageMessage case retryIcon + case undoIcon + case redoIcon } diff --git a/Sources/MediaEditor.swift b/Sources/MediaEditor.swift index baa15c2..07f02a1 100644 --- a/Sources/MediaEditor.swift +++ b/Sources/MediaEditor.swift @@ -12,7 +12,15 @@ import UIKit */ open class MediaEditor: UINavigationController { /// The capabilities are displayed in the Media Editor. You can add your own capabilities here. - public static var capabilities: [MediaEditorCapability.Type] = [MediaEditorFilters.self, MediaEditorCropZoomRotate.self] + public static var capabilities: [MediaEditorCapability.Type] = { + var capabilities: [MediaEditorCapability.Type] = [MediaEditorFilters.self, MediaEditorCropZoomRotate.self] + + if #available(iOS 13.0, *) { + capabilities.insert(MediaEditorDrawing.self, at: 0) + } + + return capabilities + }() /// A CIContext to be shared among capabilities. If your app already has one, you can assign it here. public static var ciContext = CIContext() diff --git a/Tests/Capabilities/Drawing/MediaEditorDrawingTests.swift b/Tests/Capabilities/Drawing/MediaEditorDrawingTests.swift new file mode 100644 index 0000000..2370617 --- /dev/null +++ b/Tests/Capabilities/Drawing/MediaEditorDrawingTests.swift @@ -0,0 +1,109 @@ +import XCTest +import Nimble + +@testable import MediaEditor + +import XCTest + +@available(iOS 13.0, *) +class MediaEditorDrawingTests: XCTestCase { + + func testName() { + let name = MediaEditorDrawing.name + + expect(name).to(equal("Drawing")) + } + + func testIcon() { + let icon = MediaEditorDrawing.icon + + expect(icon).to(equal(UIImage(systemName: "pencil.tip.crop.circle")!)) + } + + func testApplyStyles() { + let mediaEditorDrawing = MediaEditorDrawing.initialize(UIImage(), onFinishEditing: { _, _ in }, onCancel: {}) + mediaEditorDrawing.loadView() + + let undoIcon = UIImage(systemName: "arrowshape.turn.up.left")! + let redoIcon = UIImage(systemName: "arrowshape.turn.up.right")! + + mediaEditorDrawing.apply(styles: [ + .doneLabel: "foo", + .cancelLabel: "bar", + .cancelColor: UIColor.red, + .undoIcon: undoIcon, + .redoIcon: redoIcon + ]) + + let viewController = mediaEditorDrawing as! MediaEditorDrawing + expect(viewController.doneButton.titleLabel?.text).to(equal("foo")) + expect(viewController.cancelButton.titleLabel?.text).to(equal("bar")) + expect(viewController.cancelButton.tintColor).to(equal(.red)) + expect(viewController.undoButton.tintColor).to(equal(.red)) + expect(viewController.redoButton.tintColor).to(equal(.red)) + expect(viewController.undoButton.image(for: .normal)).to(equal(undoIcon)) + expect(viewController.redoButton.image(for: .normal)).to(equal(redoIcon)) + } + + private let image = UIImage() + + func testIsAMediaEditorCapability() { + let mediaEditorDrawing = MediaEditorDrawing.initialize(image, onFinishEditing: { _, _ in }, onCancel: {}) + + expect(mediaEditorDrawing).to(beAKindOf(MediaEditorCapability.self)) + } + + func testOriginalImageIsReturnedIfNoChangesMade() { + let image = UIImage(systemName: "arrowshape.turn.up.left")! + + var result: UIImage? = nil + let mediaEditorDrawing = MediaEditorDrawing.initialize(image, onFinishEditing: { finishedImage, _ in + result = finishedImage + }, onCancel: {}) as! MediaEditorDrawing + let annotationViewMock = MediaEditorAnnotationViewMock() + + mediaEditorDrawing.loadView() + mediaEditorDrawing.annotationView = annotationViewMock + mediaEditorDrawing.viewDidLoad() + mediaEditorDrawing.done(self) + + expect(image).to(equal(result)) + } + + func testModifiedImageIsReturnedIfChangesAreMade() { + let image = UIImage(systemName: "arrowshape.turn.up.left")! + + var result: UIImage? = nil + let mediaEditorDrawing = MediaEditorDrawing.initialize(image, onFinishEditing: { finishedImage, _ in + result = finishedImage + }, onCancel: {}) as! MediaEditorDrawing + let annotationViewMock = MediaEditorAnnotationViewMock() + annotationViewMock.image = UIImage() + + mediaEditorDrawing.loadView() + mediaEditorDrawing.viewDidLoad() + mediaEditorDrawing.annotationView = annotationViewMock + mediaEditorDrawing.view.setNeedsLayout() + mediaEditorDrawing.view.layoutIfNeeded() + + mediaEditorDrawing.done(self) + + expect(image).notTo(equal(result)) + } + + func testNewEditorHasNoUndoActions() { + let mediaEditorDrawing = MediaEditorDrawing.initialize(UIImage(), onFinishEditing: { _, _ in }, onCancel: {}) as! MediaEditorDrawing + mediaEditorDrawing.loadView() + mediaEditorDrawing.viewDidLoad() + + expect(mediaEditorDrawing.undoButton.isEnabled).to(beFalse()) + expect(mediaEditorDrawing.redoButton.isEnabled).to(beFalse()) + } +} + +@available(iOS 13.0, *) +private class MediaEditorAnnotationViewMock: MediaEditorAnnotationView { + override var canUndo: Bool { + return true + } +} diff --git a/Tests/Capabilities/Drawing/demo-drawing b/Tests/Capabilities/Drawing/demo-drawing new file mode 100644 index 0000000..7438f35 Binary files /dev/null and b/Tests/Capabilities/Drawing/demo-drawing differ