Skip to content

Commit

Permalink
Merge pull request #22 from wordpress-mobile/release/1.1
Browse files Browse the repository at this point in the history
Release/1.1
  • Loading branch information
loremattei authored May 18, 2020
2 parents 266c4fa + ec14721 commit 1b158af
Show file tree
Hide file tree
Showing 14 changed files with 650 additions and 4 deletions.
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.6.4
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github "Quick/Nimble" "v8.0.5"
github "Quick/Nimble" "v8.0.7"
github "TimOliver/TOCropViewController" "2.5.2"
2 changes: 1 addition & 1 deletion MediaEditor.podspec
Original file line number Diff line number Diff line change
@@ -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
Expand Down
36 changes: 36 additions & 0 deletions MediaEditor.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -92,6 +97,11 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
17002A9C245C27400021216C /* MediaEditorDrawing.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MediaEditorDrawing.storyboard; sourceTree = "<group>"; };
17002A9E245C54150021216C /* MediaEditorAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEditorAnnotationView.swift; sourceTree = "<group>"; };
178126E52460B25300253107 /* MediaEditorDrawingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEditorDrawingTests.swift; sourceTree = "<group>"; };
178126E72461A2DA00253107 /* demo-drawing */ = {isa = PBXFileReference; lastKnownFileType = file; path = "demo-drawing"; sourceTree = "<group>"; };
17DBA237245B1507006CD67F /* MediaEditorDrawing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEditorDrawing.swift; sourceTree = "<group>"; };
8B05570423E1BF5900C10787 /* DeviceLibraryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLibraryViewController.swift; sourceTree = "<group>"; };
8B05570623E1C1D800C10787 /* ImageViewCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewCollectionCell.swift; sourceTree = "<group>"; };
8B05570823E1CF2E00C10787 /* PlainUIImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainUIImageViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -170,6 +180,25 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
178126E42460B23700253107 /* Drawing */ = {
isa = PBXGroup;
children = (
178126E72461A2DA00253107 /* demo-drawing */,
178126E52460B25300253107 /* MediaEditorDrawingTests.swift */,
);
path = Drawing;
sourceTree = "<group>";
};
17DBA236245B14F5006CD67F /* Drawing */ = {
isa = PBXGroup;
children = (
17002A9C245C27400021216C /* MediaEditorDrawing.storyboard */,
17DBA237245B1507006CD67F /* MediaEditorDrawing.swift */,
17002A9E245C54150021216C /* MediaEditorAnnotationView.swift */,
);
path = Drawing;
sourceTree = "<group>";
};
8B05570E23E1F63A00C10787 /* BrightnessCapability */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -336,6 +365,7 @@
8B5046C823D7CE1600068F66 /* Capabilities */ = {
isa = PBXGroup;
children = (
17DBA236245B14F5006CD67F /* Drawing */,
8B5046C923D7CE1600068F66 /* Crop */,
8B062DC723E865F800488F80 /* Filters */,
8B5046CC23D7CE1600068F66 /* MediaEditorCapability.swift */,
Expand Down Expand Up @@ -376,6 +406,7 @@
8B50472323D7D36C00068F66 /* Capabilities */ = {
isa = PBXGroup;
children = (
178126E42460B23700253107 /* Drawing */,
8B50472423D7D36C00068F66 /* Crop */,
8B062DCE23E87A7900488F80 /* Filters */,
);
Expand Down Expand Up @@ -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;
};
Expand All @@ -539,6 +571,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
178126E82461A2ED00253107 /* demo-drawing in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
);
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<p align="center">
<img src="https://user-images.githubusercontent.com/7040243/74174047-0548c980-4c12-11ea-8cac-98ea739e8702.PNG" width="340">
<img src="https://user-images.githubusercontent.com/7040243/81301171-148fb580-904f-11ea-8f7e-00997401cece.PNG" width="340">
</p>

# Features
Expand All @@ -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

Expand Down
4 changes: 4 additions & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
1.1.0
-----
* Add Drawing capability using PencilKit

1.0.1
-----
* Expose the Hub
Expand Down
230 changes: 230 additions & 0 deletions Sources/Capabilities/Drawing/MediaEditorAnnotationView.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 1b158af

Please sign in to comment.