Skip to content

Commit 1b158af

Browse files
authored
Merge pull request #22 from wordpress-mobile/release/1.1
Release/1.1
2 parents 266c4fa + ec14721 commit 1b158af

File tree

14 files changed

+650
-4
lines changed

14 files changed

+650
-4
lines changed

.ruby-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2.6.4

Cartfile.resolved

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
github "Quick/Nimble" "v8.0.5"
1+
github "Quick/Nimble" "v8.0.7"
22
github "TimOliver/TOCropViewController" "2.5.2"

MediaEditor.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'MediaEditor'
3-
s.version = '1.0.1'
3+
s.version = '1.1.0'
44
s.summary = 'An extensible Media Editor for iOS.'
55

66
s.description = <<-DESC

MediaEditor.xcodeproj/project.pbxproj

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
17002A9D245C27400021216C /* MediaEditorDrawing.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 17002A9C245C27400021216C /* MediaEditorDrawing.storyboard */; };
11+
17002A9F245C54160021216C /* MediaEditorAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17002A9E245C54150021216C /* MediaEditorAnnotationView.swift */; };
12+
178126E62460B25300253107 /* MediaEditorDrawingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178126E52460B25300253107 /* MediaEditorDrawingTests.swift */; };
13+
178126E82461A2ED00253107 /* demo-drawing in Resources */ = {isa = PBXBuildFile; fileRef = 178126E72461A2DA00253107 /* demo-drawing */; };
14+
17DBA238245B1507006CD67F /* MediaEditorDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DBA237245B1507006CD67F /* MediaEditorDrawing.swift */; };
1015
8B05570523E1BF5900C10787 /* DeviceLibraryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05570423E1BF5900C10787 /* DeviceLibraryViewController.swift */; };
1116
8B05570723E1C1D800C10787 /* ImageViewCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05570623E1C1D800C10787 /* ImageViewCollectionCell.swift */; };
1217
8B05570923E1CF2E00C10787 /* PlainUIImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B05570823E1CF2E00C10787 /* PlainUIImageViewController.swift */; };
@@ -92,6 +97,11 @@
9297
/* End PBXCopyFilesBuildPhase section */
9398

9499
/* Begin PBXFileReference section */
100+
17002A9C245C27400021216C /* MediaEditorDrawing.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MediaEditorDrawing.storyboard; sourceTree = "<group>"; };
101+
17002A9E245C54150021216C /* MediaEditorAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEditorAnnotationView.swift; sourceTree = "<group>"; };
102+
178126E52460B25300253107 /* MediaEditorDrawingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEditorDrawingTests.swift; sourceTree = "<group>"; };
103+
178126E72461A2DA00253107 /* demo-drawing */ = {isa = PBXFileReference; lastKnownFileType = file; path = "demo-drawing"; sourceTree = "<group>"; };
104+
17DBA237245B1507006CD67F /* MediaEditorDrawing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEditorDrawing.swift; sourceTree = "<group>"; };
95105
8B05570423E1BF5900C10787 /* DeviceLibraryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLibraryViewController.swift; sourceTree = "<group>"; };
96106
8B05570623E1C1D800C10787 /* ImageViewCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewCollectionCell.swift; sourceTree = "<group>"; };
97107
8B05570823E1CF2E00C10787 /* PlainUIImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainUIImageViewController.swift; sourceTree = "<group>"; };
@@ -170,6 +180,25 @@
170180
/* End PBXFrameworksBuildPhase section */
171181

172182
/* Begin PBXGroup section */
183+
178126E42460B23700253107 /* Drawing */ = {
184+
isa = PBXGroup;
185+
children = (
186+
178126E72461A2DA00253107 /* demo-drawing */,
187+
178126E52460B25300253107 /* MediaEditorDrawingTests.swift */,
188+
);
189+
path = Drawing;
190+
sourceTree = "<group>";
191+
};
192+
17DBA236245B14F5006CD67F /* Drawing */ = {
193+
isa = PBXGroup;
194+
children = (
195+
17002A9C245C27400021216C /* MediaEditorDrawing.storyboard */,
196+
17DBA237245B1507006CD67F /* MediaEditorDrawing.swift */,
197+
17002A9E245C54150021216C /* MediaEditorAnnotationView.swift */,
198+
);
199+
path = Drawing;
200+
sourceTree = "<group>";
201+
};
173202
8B05570E23E1F63A00C10787 /* BrightnessCapability */ = {
174203
isa = PBXGroup;
175204
children = (
@@ -336,6 +365,7 @@
336365
8B5046C823D7CE1600068F66 /* Capabilities */ = {
337366
isa = PBXGroup;
338367
children = (
368+
17DBA236245B14F5006CD67F /* Drawing */,
339369
8B5046C923D7CE1600068F66 /* Crop */,
340370
8B062DC723E865F800488F80 /* Filters */,
341371
8B5046CC23D7CE1600068F66 /* MediaEditorCapability.swift */,
@@ -376,6 +406,7 @@
376406
8B50472323D7D36C00068F66 /* Capabilities */ = {
377407
isa = PBXGroup;
378408
children = (
409+
178126E42460B23700253107 /* Drawing */,
379410
8B50472423D7D36C00068F66 /* Crop */,
380411
8B062DCE23E87A7900488F80 /* Filters */,
381412
);
@@ -521,6 +552,7 @@
521552
8B5046E023D7CE1600068F66 /* MediaEditorHub.storyboard in Resources */,
522553
8B5046DF23D7CE1600068F66 /* Media.xcassets in Resources */,
523554
8B062DCB23E8661400488F80 /* MediaEditorFilters.storyboard in Resources */,
555+
17002A9D245C27400021216C /* MediaEditorDrawing.storyboard in Resources */,
524556
);
525557
runOnlyForDeploymentPostprocessing = 0;
526558
};
@@ -539,6 +571,7 @@
539571
isa = PBXResourcesBuildPhase;
540572
buildActionMask = 2147483647;
541573
files = (
574+
178126E82461A2ED00253107 /* demo-drawing in Resources */,
542575
);
543576
runOnlyForDeploymentPostprocessing = 0;
544577
};
@@ -573,9 +606,11 @@
573606
files = (
574607
8B5046E223D7CE1600068F66 /* MediaEditorThumbCell.swift in Sources */,
575608
8B062DD423E8925100488F80 /* MediaEditorFilterCell.swift in Sources */,
609+
17DBA238245B1507006CD67F /* MediaEditorDrawing.swift in Sources */,
576610
8B5046D623D7CE1600068F66 /* UIImage+AsyncImage.swift in Sources */,
577611
8B5046DE23D7CE1600068F66 /* AsyncImage.swift in Sources */,
578612
8B5046DC23D7CE1600068F66 /* MediaEditorCapability.swift in Sources */,
613+
17002A9F245C54160021216C /* MediaEditorAnnotationView.swift in Sources */,
579614
8B062DCD23E8663C00488F80 /* UIImage+withSize.swift in Sources */,
580615
8B062DD723E8937100488F80 /* MediaEditorFilters.swift in Sources */,
581616
8B5046DB23D7CE1600068F66 /* MediaEditorCropZoomRotate.swift in Sources */,
@@ -614,6 +649,7 @@
614649
8B50472F23D87DA400068F66 /* UIImage+color.swift in Sources */,
615650
8B50472723D7D36C00068F66 /* MediaEditorHubTests.swift in Sources */,
616651
8B50472923D7D36C00068F66 /* MediaEditorTests.swift in Sources */,
652+
178126E62460B25300253107 /* MediaEditorDrawingTests.swift in Sources */,
617653
8B50472823D7D36C00068F66 /* MediaEditorCropZoomRotateTests.swift in Sources */,
618654
8B062DD023E87A9400488F80 /* MediaEditorFilterTests.swift in Sources */,
619655
);

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
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.
66

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

1111
# Features
@@ -18,6 +18,7 @@ MediaEditor is an extendable library for iOS that allows you to quickly and easi
1818
- [x] Editing in both portrait and landscape modes
1919
- [x] Cool filters
2020
- [x] Crop, zoom and rotate capability (thanks to [`TOCropViewController`](https://github.com/TimOliver/TOCropViewController))
21+
- [x] PencilKit support to annotate images
2122
- [x] Easily extendable
2223
- [x] Customizable UI
2324

RELEASE-NOTES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
1.1.0
2+
-----
3+
* Add Drawing capability using PencilKit
4+
15
1.0.1
26
-----
37
* Expose the Hub
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import UIKit
2+
import AVFoundation
3+
import PencilKit
4+
5+
@available(iOS 13.0, *)
6+
protocol MediaEditorAnnotationViewUndoObserver: NSObject {
7+
func mediaEditorAnnotationView(_ annotationView: MediaEditorAnnotationView, isHidingUndoControls: Bool)
8+
func mediaEditorAnnotationViewUndoStatusDidChange(_ view: MediaEditorAnnotationView)
9+
}
10+
11+
/// Wrapper view that contains an image view and a PencilKit canvas to allow
12+
/// drawing on top of the image.
13+
///
14+
@available(iOS 13.0, *)
15+
class MediaEditorAnnotationView: UIView {
16+
17+
private let imageView = UIImageView()
18+
private let canvasView = PKCanvasView()
19+
20+
private var bottomConstraint: NSLayoutConstraint!
21+
22+
weak var undoObserver: MediaEditorAnnotationViewUndoObserver?
23+
24+
var canUndo: Bool {
25+
return canvasView.undoManager?.canUndo ?? false
26+
}
27+
28+
var canRedo: Bool {
29+
return canvasView.undoManager?.canRedo ?? false
30+
}
31+
32+
var image: UIImage? {
33+
set {
34+
imageView.image = newValue
35+
}
36+
get {
37+
return renderedImage
38+
}
39+
}
40+
41+
// Primarily for testing purposes
42+
var drawingData: Data {
43+
set {
44+
do {
45+
canvasView.drawing = try PKDrawing(data: newValue)
46+
} catch {
47+
print("Error setting annotation view drawing data.")
48+
}
49+
}
50+
get {
51+
return canvasView.drawing.dataRepresentation()
52+
}
53+
}
54+
55+
// MARK: - Initialization
56+
57+
override init(frame: CGRect) {
58+
super.init(frame: frame)
59+
commonInit()
60+
}
61+
62+
required init?(coder: NSCoder) {
63+
super.init(coder: coder)
64+
commonInit()
65+
}
66+
67+
deinit {
68+
undoObserver = nil
69+
70+
NotificationCenter.default.removeObserver(self,
71+
name: NSNotification.Name.NSUndoManagerCheckpoint,
72+
object: canvasView.undoManager)
73+
}
74+
75+
private func commonInit() {
76+
configureImageView()
77+
configureCanvasView()
78+
}
79+
80+
private func configureImageView() {
81+
addSubview(imageView)
82+
imageView.translatesAutoresizingMaskIntoConstraints = false
83+
84+
imageView.contentMode = .scaleAspectFit
85+
86+
bottomConstraint = imageView.bottomAnchor.constraint(equalTo: bottomAnchor)
87+
88+
NSLayoutConstraint.activate([
89+
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
90+
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
91+
imageView.topAnchor.constraint(equalTo: topAnchor),
92+
bottomConstraint
93+
])
94+
}
95+
96+
private func configureCanvasView() {
97+
addSubview(canvasView)
98+
99+
canvasView.backgroundColor = .clear
100+
canvasView.isOpaque = false
101+
102+
// Ensure ink remains the same color regardless of light / dark mode
103+
canvasView.overrideUserInterfaceStyle = .light
104+
105+
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSUndoManagerCheckpoint, object: canvasView.undoManager, queue: nil) { [weak self] _ in
106+
self?.notifyUndoObserver()
107+
}
108+
}
109+
110+
fileprivate func notifyUndoObserver() {
111+
undoObserver?.mediaEditorAnnotationViewUndoStatusDidChange(self)
112+
}
113+
114+
// MARK: - View Layout
115+
116+
override func layoutSubviews() {
117+
super.layoutSubviews()
118+
119+
let currentFrame = canvasView.frame
120+
let newFrame = calculateCanvasFrame()
121+
canvasView.frame = newFrame
122+
123+
// If the canvas has changed size (e.g. due to device rotation) apply a transform
124+
// to the drawing so that it still fits the scaled imageview
125+
let transform = CGAffineTransform(scaleX: newFrame.width / currentFrame.width, y: newFrame.height / currentFrame.height)
126+
self.canvasView.drawing.transform(using: transform)
127+
}
128+
129+
private func calculateCanvasFrame() -> CGRect {
130+
guard let image = imageView.image,
131+
imageView.contentMode == .scaleAspectFit,
132+
image.size.width > 0 && image.size.height > 0 else {
133+
return imageView.bounds
134+
}
135+
136+
let size = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)
137+
138+
let x = (imageView.bounds.width - size.width) * 0.5
139+
let y = (imageView.bounds.height - size.height) * 0.5
140+
141+
return CGRect(x: x, y: y, width: size.width, height: size.height)
142+
}
143+
144+
// MARK: - Public methods
145+
146+
/// Displays the system tool picker in the specified window
147+
///
148+
func showTools(in window: UIWindow) {
149+
if let toolPicker = PKToolPicker.shared(for: window) {
150+
toolPicker.setVisible(true, forFirstResponder: canvasView)
151+
toolPicker.addObserver(canvasView)
152+
toolPicker.addObserver(self)
153+
154+
canvasView.becomeFirstResponder()
155+
updateLayout(for: toolPicker)
156+
}
157+
}
158+
159+
/// Renders the initial image with the canvas's image overlaid on top
160+
/// into a single UIImage instance.
161+
///
162+
private var renderedImage: UIImage? {
163+
guard let imageViewImage = imageView.image else {
164+
return nil
165+
}
166+
167+
guard canvasView.bounds != .zero else {
168+
return imageViewImage
169+
}
170+
171+
// Check we actually have some changes
172+
if let undoManager = canvasView.undoManager,
173+
undoManager.canUndo == false {
174+
return imageViewImage
175+
}
176+
177+
let targetSize = imageViewImage.size
178+
179+
let canvasViewImage = canvasView.drawing.image(from: canvasView.bounds, scale: UIScreen.main.scale)
180+
181+
let renderer = UIGraphicsImageRenderer(size: targetSize, format: .default())
182+
let renderedImage = renderer.image { context in
183+
imageViewImage.draw(at: .zero)
184+
canvasViewImage.draw(in: CGRect(origin: .zero, size: targetSize))
185+
}
186+
187+
return renderedImage
188+
}
189+
}
190+
191+
// Note: Code in this extension reused from WWDC 2019 PencilKit example
192+
//
193+
@available(iOS 13.0, *)
194+
extension MediaEditorAnnotationView: PKToolPickerObserver {
195+
// MARK: Tool Picker Observer
196+
197+
/// Delegate method: Note that the tool picker has changed which part of the canvas view
198+
/// it obscures, if any.
199+
internal func toolPickerFramesObscuredDidChange(_ toolPicker: PKToolPicker) {
200+
updateLayout(for: toolPicker)
201+
}
202+
203+
/// Delegate method: Note that the tool picker has become visible or hidden.
204+
internal func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
205+
updateLayout(for: toolPicker)
206+
}
207+
208+
/// Helper method to adjust the canvas view size when the tool picker changes which part
209+
/// of the canvas view it obscures, if any.
210+
///
211+
/// Note that the tool picker floats over the canvas in regular size classes, but docks to
212+
/// the canvas in compact size classes, occupying a part of the screen that the canvas
213+
/// could otherwise use.
214+
fileprivate func updateLayout(for toolPicker: PKToolPicker) {
215+
let obscuredFrame = toolPicker.frameObscured(in: self)
216+
217+
if obscuredFrame.isNull {
218+
bottomConstraint.constant = 0
219+
undoObserver?.mediaEditorAnnotationView(self, isHidingUndoControls: false)
220+
} else {
221+
bottomConstraint.constant = -obscuredFrame.height
222+
undoObserver?.mediaEditorAnnotationView(self, isHidingUndoControls: true)
223+
}
224+
225+
setNeedsLayout()
226+
layoutIfNeeded()
227+
228+
canvasView.scrollIndicatorInsets = canvasView.contentInset
229+
}
230+
}

0 commit comments

Comments
 (0)