Commit 2b7e6a87 authored by Geoff Pado's avatar Geoff Pado

Merge branch 'release/19.2' into 'master'

Release version 19.2

See merge request highlighter/app!54
parents 439f0c8f a3b97ac0
// Created by Geoff Pado on 7/3/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
import UIKit
class ActionEditingDismissalAlertController: UIAlertController {
init(completionHandler: @escaping ((Response) -> Void)) {
self.completionHandler = completionHandler
super.init(nibName: nil, bundle: nil)
addAction(saveAction)
addAction(deleteAction)
addAction(cancelAction)
}
var barButtonItem: UIBarButtonItem? {
get { return popoverPresentationController?.barButtonItem }
set(newButtonItem) {
popoverPresentationController?.barButtonItem = newButtonItem
}
}
private lazy var saveAction = UIAlertAction(title: ActionEditingDismissalAlertController.saveButtonTitle, style: .default, handler: { [weak self] _ in
self?.completionHandler(.save)
})
private lazy var deleteAction = UIAlertAction(title: ActionEditingDismissalAlertController.deleteButtonTitle, style: .destructive, handler: { [weak self] _ in
self?.completionHandler(.delete)
})
private let cancelAction = UIAlertAction(title: ActionEditingDismissalAlertController.cancelButtonTitle, style: .cancel, handler: nil)
enum Response {
case save, delete
}
// MARK: Boilerplate
override var preferredStyle: UIAlertController.Style { return .actionSheet }
private static let cancelButtonTitle = NSLocalizedString("ActionEditingDismissalAlertController.cancelButtonTitle", comment: "Title for the cancel button on the photo permissions denied alert")
private static let deleteButtonTitle = NSLocalizedString("ActionEditingDismissalAlertController.deleteButtonTitle", comment: "Title for the delete button on the photo permissions denied alert")
private static let saveButtonTitle = NSLocalizedString("ActionEditingDismissalAlertController.saveButtonTitle", comment: "Title for the save button on the photo permissions denied alert")
private let completionHandler: ((Response) -> Void)
@available(*, unavailable)
required init(coder: NSCoder) {
let className = String(describing: type(of: self))
fatalError("\(className) does not implement init(coder:)")
}
}
// Created by Geoff Pado on 6/26/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
import Editing
import MobileCoreServices
import Photos
import UIKit
class ActionEditingViewController: BasePhotoEditingViewController {
override func viewDidLoad() {
super.viewDidLoad()
loadImageFromExtensionContext()
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(ActionEditingViewController.done))
}
private func loadImageFromExtensionContext() {
let imageTypeIdentifier = (kUTTypeImage as String)
let imageProvider = extensionContext?
.inputItems
.compactMap { $0 as? NSExtensionItem }
.flatMap { $0.attachments ?? [] }
.first(where: { $0.hasItemConformingToTypeIdentifier(imageTypeIdentifier) })
imageProvider?.loadItem(forTypeIdentifier: imageTypeIdentifier, options: nil) { [weak self] item, error in
do {
guard let imageURL = (item as? URL) else { throw (error ?? ActionError.imageURLNotFound) }
let imageData = try Data(contentsOf: imageURL)
guard let image = UIImage(data: imageData) else { throw ActionError.invalidImageData }
DispatchQueue.main.async { [weak self] in
self?.load(image)
}
} catch {
dump(error)
}
}
}
@objc private func done(_ sender: UIBarButtonItem) {
let alertController = ActionEditingDismissalAlertController() { [weak self] response in
switch response {
case .delete:
self?.dismissActionExtension()
case .save:
guard let imageForExport = self?.imageForExport else { return }
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: imageForExport)
}, completionHandler: { [weak self] success, error in
assert(success, "an error occurred saving changes: \(error?.localizedDescription ?? "no error")")
DispatchQueue.main.async {
self?.dismissActionExtension()
}
})
}
}
alertController.barButtonItem = sender
present(alertController, animated: true)
}
private func dismissActionExtension() {
let items: [Any]
if let imageForExport = imageForExport {
let extensionItem = NSExtensionItem()
let itemProvider = NSItemProvider(item: imageForExport, typeIdentifier: (kUTTypeImage as String))
extensionItem.attachments = [itemProvider]
items = [extensionItem]
} else {
items = []
}
self.extensionContext?.completeRequest(returningItems: items, completionHandler: nil)
}
}
enum ActionError: Error {
case imageURLNotFound
case invalidImageData
}
// Created by Geoff Pado on 5/15/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
import Editing
import Photos
import UIKit
class ActionNavigationController: NavigationController {
init() {
super.init(rootViewController: ActionEditingViewController())
isToolbarHidden = false
}
// MARK: Boilerplate
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init(coder: NSCoder) {
let className = String(describing: type(of: self))
fatalError("\(className) does not implement init(coder:)")
}
}
// Created by Geoff Pado on 7/1/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
import Editing
import UIKit
class ActionViewController: UIViewController {
init() {
super.init(nibName: nil, bundle: nil)
let navigationController = ActionNavigationController()
embed(navigationController)
}
@available(*, unavailable)
required init(coder: NSCoder) {
let className = String(describing: type(of: self))
fatalError("\(className) does not implement init(coder:)")
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>CFBundleDisplayName</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>19.2</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionActionWantsFullScreenPresentation</key>
<true/>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>1</integer>
</dict>
<key>NSExtensionServiceAllowsFinderPreviewItem</key>
<true/>
<key>NSExtensionServiceAllowsTouchBarItem</key>
<true/>
<key>NSExtensionServiceFinderPreviewIconName</key>
<string>NSActionTemplate</string>
<key>NSExtensionServiceTouchBarBezelColorName</key>
<string>TouchBarBezel</string>
<key>NSExtensionServiceTouchBarIconName</key>
<string>NSActionTemplate</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.ui-services</string>
<key>NSExtensionPrincipalClass</key>
<string>Action.ActionViewController</string>
</dict>
<key>UIAppFonts</key>
<array>
<string>Aleo-Bold.otf</string>
<string>Aleo-Regular.otf</string>
</array>
</dict>
</plist>
/*
Localizable.strings
Highlighter
Created by Geoff Pado on 7/3/19.
Copyright © 2019 Cocoatype, LLC. All rights reserved.
*/
"ActionEditingDismissalAlertController.cancelButtonTitle" = "Cancel";
"ActionEditingDismissalAlertController.deleteButtonTitle" = "Delete Changes";
"ActionEditingDismissalAlertController.saveButtonTitle" = "Save to Photos";
"BasePhotoEditingViewController.undoKeyCommandDiscoverabilityTitle" = "Undo Redaction";
"BasePhotoEditingViewController.redoKeyCommandDiscoverabilityTitle" = "Redo Redaction";
/*
InfoPlist.strings
Highlighter
Created by Geoff Pado on 7/1/19.
Copyright © 2019 Cocoatype, LLC. All rights reserved.
*/
"CFBundleDisplayName" = "Hide Text";
// Created by Geoff Pado on 4/15/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
import Photos
import UIKit
open class BasePhotoEditingViewController: UIViewController, UIScrollViewDelegate {
public init(asset: PHAsset? = nil, image: UIImage? = nil, completionHandler: ((UIImage) -> Void)? = nil) {
self.asset = asset
self.image = image
self.completionHandler = completionHandler
super.init(nibName: nil, bundle: nil)
updateToolbarItems(animated: false)
redactionChangeObserver = NotificationCenter.default.addObserver(forName: PhotoEditingRedactionView.redactionsDidChange, object: nil, queue: .main, using: { [weak self] _ in
self?.updateToolbarItems()
})
}
open override func loadView() {
view = photoEditingView
}
open override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let options = PHImageRequestOptions()
options.version = .current
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
if image != nil {
updateScrollView()
} else if let asset = asset {
imageManager.requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { [weak self] image, info in
let isDegraded = (info?[PHImageResultIsDegradedKey] as? NSNumber)?.boolValue ?? false
guard let image = image, isDegraded == false else { return }
DispatchQueue.main.async {
self?.image = image
}
}
}
}
// MARK: Edit Protection
@objc open func markHasMadeEdits() {} // hook for responder chain
// MARK: Sharing
public var imageForExport: UIImage? {
guard let image = photoEditingView.image else { return nil }
let photoExporter = PhotoExporter(image: image, redactions: photoEditingView.redactions)
return photoExporter.exportedImage
}
// MARK: Highlighters
@objc func toggleHighlighterTool() {
let currentTool = photoEditingView.highlighterTool
let allTools = HighlighterTool.allCases
let currentToolIndex = allTools.firstIndex(of: currentTool) ?? allTools.startIndex
let nextToolIndex = (currentToolIndex + 1) % allTools.count
let nextTool = allTools[nextToolIndex]
photoEditingView.highlighterTool = nextTool
updateToolbarItems()
}
private func updateToolbarItems(animated: Bool = true) {
let undoToolItem = UIBarButtonItem(image: UIImage(named: "Undo"), style: .plain, target: self, action: #selector(BasePhotoEditingViewController.undo))
undoToolItem.isEnabled = editingUndoManager.canUndo
let redoToolItem = UIBarButtonItem(image: UIImage(named: "Redo"), style: .plain, target: self, action: #selector(BasePhotoEditingViewController.redo))
redoToolItem.isEnabled = editingUndoManager.canRedo
let spacerItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let highlighterToolIcon = photoEditingView.highlighterTool.image
let highlighterToolItem = UIBarButtonItem(image: highlighterToolIcon, style: .plain, target: self, action: #selector(toggleHighlighterTool))
setToolbarItems([undoToolItem, redoToolItem, spacerItem, highlighterToolItem], animated: animated)
}
// MARK: Undo/Redo
let editingUndoManager = UndoManager()
open override var undoManager: UndoManager? {
return editingUndoManager
}
@objc private func undo() {
editingUndoManager.undo()
updateToolbarItems()
}
@objc private func redo() {
editingUndoManager.redo()
updateToolbarItems()
}
// MARK: Key Commands
private let undoKeyCommand = UIKeyCommand(input: "z", modifierFlags: .command, action: #selector(BasePhotoEditingViewController.undo), discoverabilityTitle: BasePhotoEditingViewController.undoKeyCommandDiscoverabilityTitle)
private let redoKeyCommand = UIKeyCommand(input: "z", modifierFlags: [.command, .shift], action: #selector(BasePhotoEditingViewController.redo), discoverabilityTitle: BasePhotoEditingViewController.redoKeyCommandDiscoverabilityTitle)
open override var keyCommands: [UIKeyCommand]? {
return [undoKeyCommand, redoKeyCommand]
}
// MARK: Image
public func load(_ image: UIImage) {
guard self.image == nil else { return }
self.image = image
}
private(set) public var image: UIImage? {
didSet {
updateScrollView()
}
}
private func updateScrollView() {
photoEditingView.image = image
guard let image = image else { return }
textRectangleDetector.detectTextRectangles(in: image) { [weak self] textObservations in
DispatchQueue.main.async { [weak self] in
self?.photoEditingView.textObservations = textObservations
}
}
}
// MARK: Boilerplate
public let completionHandler: ((UIImage) -> Void)?
private static let undoKeyCommandDiscoverabilityTitle = NSLocalizedString("BasePhotoEditingViewController.undoKeyCommandDiscoverabilityTitle", comment: "Discovery title for the undo key command")
private static let redoKeyCommandDiscoverabilityTitle = NSLocalizedString("BasePhotoEditingViewController.redoKeyCommandDiscoverabilityTitle", comment: "Discovery title for the redo key command")
private let asset: PHAsset?
private let imageManager = PHImageManager()
private let textRectangleDetector = TextRectangleDetector()
private let photoEditingView = PhotoEditingView()
private var redactionChangeObserver: Any?
deinit {
redactionChangeObserver.map(NotificationCenter.default.removeObserver)
}
override convenience init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
self.init(asset: nil, image: nil, completionHandler: nil)
}
public required init(coder: NSCoder) {
let className = String(describing: type(of: self))
fatalError("\(className) does not implement init(coder:)")
}
}
......@@ -3,8 +3,8 @@
import UIKit
class PhotoEditingView: UIView, UIScrollViewDelegate {
init() {
public class PhotoEditingView: UIView, UIScrollViewDelegate {
public init() {
super.init(frame: .zero)
backgroundColor = .primary
......@@ -19,31 +19,31 @@ class PhotoEditingView: UIView, UIScrollViewDelegate {
])
}
var image: UIImage? {
public var image: UIImage? {
get { return photoScrollView.image }
set(newImage) { photoScrollView.image = newImage }
}
var textObservations: [TextObservation]? {
public var textObservations: [TextObservation]? {
didSet {
photoScrollView.textObservations = textObservations
}
}
var highlighterTool: HighlighterTool {
public var highlighterTool: HighlighterTool {
get { return workspaceView.highlighterTool }
set(newTool) {
workspaceView.highlighterTool = newTool
}
}
var redactions: [Redaction] {
public var redactions: [Redaction] {
return workspaceView.redactions
}
// MARK: UIScrollViewDelegate
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return workspaceView
}
......
......@@ -3,8 +3,8 @@
import UIKit
class PhotoEditingImageView: UIImageView {
init() {
public class PhotoEditingImageView: UIImageView {
public init() {
super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
contentMode = .center
......
......@@ -3,8 +3,8 @@
import UIKit
class PhotoEditingRedactionView: UIView {
init() {
public class PhotoEditingRedactionView: UIView {
public init() {
super.init(frame: .zero)
backgroundColor = .clear
......@@ -12,7 +12,7 @@ class PhotoEditingRedactionView: UIView {
translatesAutoresizingMaskIntoConstraints = false
}
override func draw(_ rect: CGRect) {
public override func draw(_ rect: CGRect) {
super.draw(rect)
redactions
......@@ -31,17 +31,17 @@ class PhotoEditingRedactionView: UIView {
}
}
func add(_ redaction: Redaction) {
public func add(_ redaction: Redaction) {
self.redactions.append(redaction)
setNeedsDisplay()
}
func add(_ redactions: [Redaction]) {
public func add(_ redactions: [Redaction]) {
self.redactions.append(contentsOf: redactions)
setNeedsDisplay()
}
func removeAllRedactions() {
public func removeAllRedactions() {
self.redactions = []
}
......@@ -74,12 +74,14 @@ class PhotoEditingRedactionView: UIView {
// MARK: Notifications
static let redactionsDidChange = Notification.Name("PhotoEditingRedactionView.redactionsDidChange")
public static let redactionsDidChange = Notification.Name("PhotoEditingRedactionView.redactionsDidChange")
// MARK: Boilerplate
private(set) var redactions = [Redaction]() {
didSet(existingRedactions) {
guard existingRedactions.count != redactions.count else { return }
registerUndo(with: existingRedactions)
NotificationCenter.default.post(name: PhotoEditingRedactionView.redactionsDidChange, object: self)
}
......
......@@ -3,7 +3,7 @@
import UIKit
class PhotoEditingWorkspaceView: UIView {
class PhotoEditingWorkspaceView: UIControl {
init() {
imageView = PhotoEditingImageView()
visualizationView = PhotoEditingObservationVisualizationView()
......@@ -75,7 +75,7 @@ class PhotoEditingWorkspaceView: UIView {
case .manual: handleManualStrokeCompletion()
}
UIApplication.shared.sendAction(#selector(PhotoEditingViewController.markHasMadeEdits), to: nil, from: self, for: nil)
sendAction(#selector(BasePhotoEditingViewController.markHasMadeEdits), to: nil, for: nil)
}
private func handleMagicStrokeCompletion() {
......@@ -86,7 +86,9 @@ class PhotoEditingWorkspaceView: UIView {
.flatMap { $0 }
.filter { strokeBorderPath.contains($0.bounds.center) }
redactionView.add(CharacterObservationRedaction(redactedCharacterObservations))
if let newRedaction = CharacterObservationRedaction(redactedCharacterObservations) {
redactionView.add(newRedaction)
}
}
private func handleManualStrokeCompletion() {
......
//
// Editing.h
// Editing
//
// Created by Geoff Pado on 7/3/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for Editing.
FOUNDATION_EXPORT double EditingVersionNumber;
//! Project version string for Editing.
FOUNDATION_EXPORT const unsigned char EditingVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <Editing/PublicHeader.h>
......@@ -3,19 +3,19 @@
import UIKit
extension CGSize {
public extension CGSize {
static func * (size: CGSize, multiplier: CGFloat) -> CGSize {
return CGSize(width: size.width * multiplier, height: size.height * multiplier)
}
}
extension CGPoint {
public extension CGPoint {
static func * (point: CGPoint, multiplier: CGFloat) -> CGPoint {
return CGPoint(x:point.x * multiplier, y: point.y * multiplier)
}
}
extension CGRect {
public extension CGRect {
static func * (rect: CGRect, multiplier: CGFloat) -> CGRect {
return CGRect(x: rect.origin.x * multiplier, y: rect.origin.y * multiplier, width: rect.size.width * multiplier, height: rect.size.height * multiplier)
}
......
......@@ -4,7 +4,7 @@
import UIKit
extension UIViewController {
func embed(_ newChild: UIViewController) {
public func embed(_ newChild: UIViewController) {
if let existingChild = children.first {
existingChild.willMove(toParent: nil)
existingChild.view.removeFromSuperview()
......@@ -26,7 +26,7 @@ extension UIViewController {
])
}
func transition(to child: UIViewController, completion: ((Bool) -> Void)? = nil) {
public func transition(to child: UIViewController, completion: ((Bool) -> Void)? = nil) {
let duration = 0.3
let current = children.last
......
......@@ -3,11 +3,11 @@
import UIKit
enum HighlighterTool: CaseIterable {
public enum HighlighterTool: CaseIterable {
case magic
case manual
var image: UIImage {
public var image: UIImage {
switch self {
case .magic: return #imageLiteral(resourceName: "Magic Highlighter")
case .manual: return #imageLiteral(resourceName: "Standard Highlighter.png")
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>