Verified Commit 6b8aa76a authored by Geoff Pado's avatar Geoff Pado

Save edited photo in place

parent 49f71a18
......@@ -30,6 +30,9 @@
041EFF72225C3DC00058D8EE /* PhotoLibraryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041EFF71225C3DC00058D8EE /* PhotoLibraryDataSource.swift */; };
041EFF74225C3DD20058D8EE /* PhotoLibraryViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041EFF73225C3DD20058D8EE /* PhotoLibraryViewCell.swift */; };
041EFF76225C3DF10058D8EE /* PhotoLibraryViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041EFF75225C3DF10058D8EE /* PhotoLibraryViewLayout.swift */; };
042A2B78228A5B6F00353414 /* PhotoExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 042A2B77228A5B6F00353414 /* PhotoExporter.swift */; };
042A2B7A228A642100353414 /* PhotoEditingProtectionAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 042A2B79228A642100353414 /* PhotoEditingProtectionAlertController.swift */; };
042A2B7C228A68C900353414 /* PhotoPermissionsRestrictedAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 042A2B7B228A68C900353414 /* PhotoPermissionsRestrictedAlertController.swift */; };
042A39AD22811F87009C1C07 /* Redaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 042A39AC22811F87009C1C07 /* Redaction.swift */; };
042A39AF228125FA009C1C07 /* PhotoEditingRedactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 042A39AE228125FA009C1C07 /* PhotoEditingRedactionView.swift */; };
043CD9C9226EB0400012F5AE /* TextRectangleDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043CD9C8226EB0400012F5AE /* TextRectangleDetector.swift */; };
......@@ -103,6 +106,9 @@
041EFF71225C3DC00058D8EE /* PhotoLibraryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryDataSource.swift; sourceTree = "<group>"; };
041EFF73225C3DD20058D8EE /* PhotoLibraryViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryViewCell.swift; sourceTree = "<group>"; };
041EFF75225C3DF10058D8EE /* PhotoLibraryViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryViewLayout.swift; sourceTree = "<group>"; };
042A2B77228A5B6F00353414 /* PhotoExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoExporter.swift; sourceTree = "<group>"; };
042A2B79228A642100353414 /* PhotoEditingProtectionAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoEditingProtectionAlertController.swift; sourceTree = "<group>"; };
042A2B7B228A68C900353414 /* PhotoPermissionsRestrictedAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPermissionsRestrictedAlertController.swift; sourceTree = "<group>"; };
042A39AC22811F87009C1C07 /* Redaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Redaction.swift; sourceTree = "<group>"; };
042A39AE228125FA009C1C07 /* PhotoEditingRedactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoEditingRedactionView.swift; sourceTree = "<group>"; };
043CD9C8226EB0400012F5AE /* TextRectangleDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRectangleDetector.swift; sourceTree = "<group>"; };
......@@ -282,6 +288,7 @@
children = (
042A39AB22811F7A009C1C07 /* Redactions */,
048909F1226571AB0048E203 /* PhotoEditingViewController.swift */,
042A2B77228A5B6F00353414 /* PhotoExporter.swift */,
043CD9CD226EB0B20012F5AE /* Text Detection */,
047072922268134500FF20B6 /* PhotoEditingView.swift */,
043CD9E42277EC910012F5AE /* PhotoEditingBrushStrokeView.swift */,
......@@ -290,6 +297,7 @@
048909F3226573750048E203 /* PhotoEditorPresenting.swift */,
043CD9D52275316E0012F5AE /* PhotoEditingScrollView.swift */,
043CD9D7227531DF0012F5AE /* PhotoEditingObservationVisualizationView.swift */,
042A2B79228A642100353414 /* PhotoEditingProtectionAlertController.swift */,
);
path = "Photo Editing";
sourceTree = "<group>";
......@@ -334,6 +342,7 @@
children = (
04D68BC82262B0C400D09BBD /* PhotoPermissionsRequester.swift */,
04D68BCB2262B35700D09BBD /* PhotoPermissionsDeniedAlertController.swift */,
042A2B7B228A68C900353414 /* PhotoPermissionsRestrictedAlertController.swift */,
);
path = "Photo Permissions";
sourceTree = "<group>";
......@@ -478,6 +487,7 @@
buildActionMask = 2147483647;
files = (
043CD9D8227531DF0012F5AE /* PhotoEditingObservationVisualizationView.swift in Sources */,
042A2B7A228A642100353414 /* PhotoEditingProtectionAlertController.swift in Sources */,
043CD9E3227549330012F5AE /* SettingsTableViewDataSource.swift in Sources */,
049F292D22875AFA00A335BE /* AboutViewController.swift in Sources */,
049F293122875D5B00A335BE /* AcknowledgementsViewController.swift in Sources */,
......@@ -510,10 +520,12 @@
041EFF52225303430058D8EE /* PromptButton.swift in Sources */,
041EFEF92251A9F30058D8EE /* AppDelegate.swift in Sources */,
043CD9DD227548C20012F5AE /* SettingsTableView.swift in Sources */,
042A2B78228A5B6F00353414 /* PhotoExporter.swift in Sources */,
041EFF76225C3DF10058D8EE /* PhotoLibraryViewLayout.swift in Sources */,
049F292F22875B1300A335BE /* PrivacyViewController.swift in Sources */,
04D68BC7225EE2FD00D09BBD /* PhotoLibraryViewCellImageView.swift in Sources */,
041EFF5B2255A39D0058D8EE /* Fonts.swift in Sources */,
042A2B7C228A68C900353414 /* PhotoPermissionsRestrictedAlertController.swift in Sources */,
041EFF50225303120058D8EE /* PromptLabel.swift in Sources */,
043CD9D1226EB0F60012F5AE /* UIImageExtensions.swift in Sources */,
043CD9DF227548F30012F5AE /* SettingsTableViewCell.swift in Sources */,
......
......@@ -20,6 +20,17 @@ class AppViewController: UIViewController, PhotoEditorPresenting {
}
@objc func dismissPhotoEditingViewController() {
guard let presentedNavigationController = (presentedViewController as? NavigationController),
let editingViewController = (presentedNavigationController.viewControllers.first as? PhotoEditingViewController)
else { return }
guard editingViewController.hasMadeEdits else { dismiss(animated: true); return }
let alertController = PhotoEditingProtectionAlertController(appViewController: self)
editingViewController.present(alertController, animated: true)
}
@objc func destructivelyDismissPhotoEditingViewController() {
if let presentedNavigationController = (presentedViewController as? NavigationController),
let rootViewController = presentedNavigationController.viewControllers.first,
rootViewController is PhotoEditingViewController {
......
......@@ -9,6 +9,11 @@
"IntroView.promptLabelText" = "Black Highlighter needs permission to edit your photos.";
"IntroView.promptButtonTitle" = "Grant Access";
"PhotoEditingProtectionAlertController.alertTitle" = "Finished Editing?";
"PhotoEditingProtectionAlertController.alertMessage" = "Are you sure you're finished editing? You will lose any changes that haven't been saved.";
"PhotoEditingProtectionAlertController.actionButtonTitle" = "Finish Editing";
"PhotoEditingProtectionAlertController.cancelButtonTitle" = "Don't Finish";
"PhotoPermissionsDeniedAlertController.alertTitle" = "No Access to Your Photos";
"PhotoPermissionsDeniedAlertController.alertMessage" = "Black Highlighter needs permission to edit your photos. Open Settings to grant access.";
"PhotoPermissionsDeniedAlertController.actionButtonTitle" = "Open Settings";
......
// Created by Geoff Pado on 5/13/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
import UIKit
class PhotoEditingProtectionAlertController: UIAlertController {
init(appViewController: AppViewController) {
self.appViewController = appViewController
super.init(nibName: nil, bundle: nil)
title = PhotoEditingProtectionAlertController.alertTitle
message = PhotoEditingProtectionAlertController.alertMessage
addAction(closeAction)
addAction(cancelAction)
}
private lazy var closeAction = UIAlertAction(title: PhotoEditingProtectionAlertController.actionButtonTitle, style: .destructive, handler: { [weak self] _ in
self?.appViewController?.destructivelyDismissPhotoEditingViewController()
})
private let cancelAction = UIAlertAction(title: PhotoEditingProtectionAlertController.cancelButtonTitle, style: .cancel, handler: nil)
// MARK: Boilerplate
override var preferredStyle: UIAlertController.Style { return .alert }
weak var appViewController: AppViewController?
private static let alertTitle = NSLocalizedString("PhotoEditingProtectionAlertController.alertTitle", comment: "Title for the photo permissions denied alert")
private static let alertMessage = NSLocalizedString("PhotoEditingProtectionAlertController.alertMessage", comment: "Message for the photo permissions denied alert")
private static let actionButtonTitle = NSLocalizedString("PhotoEditingProtectionAlertController.actionButtonTitle", comment: "Title for the settings button on the photo permissions denied alert")
private static let cancelButtonTitle = NSLocalizedString("PhotoEditingProtectionAlertController.cancelButtonTitle", comment: "Title for the cancel button on the photo permissions denied alert")
@available(*, unavailable)
required init(coder: NSCoder) {
let className = String(describing: type(of: self))
fatalError("\(className) does not implement init(coder:)")
}
}
......@@ -34,7 +34,11 @@ class PhotoEditingRedactionView: UIView {
}
func add(_ redaction: Redaction) {
redactions.append(redaction)
add([redaction])
}
func add(_ redactions: [Redaction]) {
redactions.forEach { [unowned self] in self.redactions.append($0) }
setNeedsDisplay()
}
......@@ -58,7 +62,7 @@ class PhotoEditingRedactionView: UIView {
// MARK: Boilerplate
private var redactions = [Redaction]()
private(set) var redactions = [Redaction]()
@available(*, unavailable)
required init(coder: NSCoder) {
......
......@@ -48,6 +48,10 @@ class PhotoEditingView: UIView {
}
}
var redactions: [Redaction] {
return redactionView.redactions
}
var textObservations: [TextObservation]? {
get { return visualizationView.textObservations }
set(newTextObservations) {
......@@ -66,6 +70,8 @@ class PhotoEditingView: UIView {
.filter { strokeBorderPath.contains($0.bounds.center) }
redactionView.add(CharacterObservationRedaction(redactedCharacterObservations))
UIApplication.shared.sendAction(#selector(PhotoEditingViewController.markHasMadeEdits), to: nil, from: self, for: nil)
}
// MARK: Boilerplate
......
......@@ -10,6 +10,7 @@ class PhotoEditingViewController: UIViewController, UIScrollViewDelegate {
super.init(nibName: nil, bundle: nil)
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: nil, action: #selector(AppViewController.dismissPhotoEditingViewController))
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(PhotoEditingViewController.sharePhoto))
}
override func loadView() {
......@@ -41,6 +42,28 @@ class PhotoEditingViewController: UIViewController, UIScrollViewDelegate {
}
}
// MARK: Edit Protection
private(set) var hasMadeEdits = false
@objc func markHasMadeEdits() {
hasMadeEdits = true
}
// MARK: Sharing
@objc func sharePhoto() {
guard let editingView = photoScrollView?.photoEditingView, let image = editingView.image else { return }
let photoExporter = PhotoExporter(image: image, redactions: editingView.redactions)
guard let exportedImage = photoExporter.exportedImage else { return }
let activityController = UIActivityViewController(activityItems: [exportedImage], applicationActivities: nil)
activityController.completionWithItemsHandler = { [weak self] _, completed, _, _ in
self?.hasMadeEdits = false
}
present(activityController, animated: true)
}
// MARK: UIScrollViewDelegate
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
......
// Created by Geoff Pado on 5/13/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
import UIKit
class PhotoExporter: NSObject {
init(image: UIImage, redactions: [Redaction]) {
let imageSize = image.size * image.scale
let bounds = CGRect(origin: .zero, size: imageSize)
imageView.frame = bounds
redactionView.frame = bounds
imageView.image = image
redactionView.add(redactions)
}
var exportedImage: UIImage? {
let imageBounds = imageView.bounds
UIGraphicsBeginImageContextWithOptions(imageBounds.size, true, 1)
defer { UIGraphicsEndImageContext() }
imageView.drawHierarchy(in: imageBounds, afterScreenUpdates: true)
redactionView.drawHierarchy(in: imageBounds, afterScreenUpdates: true)
return UIGraphicsGetImageFromCurrentImageContext()
}
// MARK: Boilerplate
private let imageView = PhotoEditingImageView()
private let redactionView = PhotoEditingRedactionView()
}
......@@ -35,30 +35,3 @@ class PhotoPermissionsDeniedAlertController: UIAlertController {
fatalError("\(className) does not implement init(coder:)")
}
}
class PhotoPermissionsRestrictedAlertController: UIAlertController {
init() {
super.init(nibName: nil, bundle: nil)
title = PhotoPermissionsRestrictedAlertController.alertTitle
message = PhotoPermissionsRestrictedAlertController.alertMessage
addAction(dismissAction)
}
private let dismissAction = UIAlertAction(title: PhotoPermissionsRestrictedAlertController.dismissButtonTitle, style: .cancel, handler: nil)
// MARK: Boilerplate
override var preferredStyle: UIAlertController.Style { return .alert }
private static let alertTitle = NSLocalizedString("PhotoPermissionsRestrictedAlertController.alertTitle", comment: "Title for the photo permissions restricted alert")
private static let alertMessage = NSLocalizedString("PhotoPermissionsRestrictedAlertController.alertMessage", comment: "Message for the photo permissions restricted alert")
private static let dismissButtonTitle = NSLocalizedString("PhotoPermissionsRestrictedAlertController.dismissButtonTitle", comment: "Title for the cancel button on the photo permissions restricted alert")
@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 5/13/19.
// Copyright © 2019 Cocoatype, LLC. All rights reserved.
import UIKit
class PhotoPermissionsRestrictedAlertController: UIAlertController {
init() {
super.init(nibName: nil, bundle: nil)
title = PhotoPermissionsRestrictedAlertController.alertTitle
message = PhotoPermissionsRestrictedAlertController.alertMessage
addAction(dismissAction)
}
private let dismissAction = UIAlertAction(title: PhotoPermissionsRestrictedAlertController.dismissButtonTitle, style: .cancel, handler: nil)
// MARK: Boilerplate
override var preferredStyle: UIAlertController.Style { return .alert }
private static let alertTitle = NSLocalizedString("PhotoPermissionsRestrictedAlertController.alertTitle", comment: "Title for the photo permissions restricted alert")
private static let alertMessage = NSLocalizedString("PhotoPermissionsRestrictedAlertController.alertMessage", comment: "Message for the photo permissions restricted alert")
private static let dismissButtonTitle = NSLocalizedString("PhotoPermissionsRestrictedAlertController.dismissButtonTitle", comment: "Title for the cancel button on the photo permissions restricted alert")
@available(*, unavailable)
required init(coder: NSCoder) {
let className = String(describing: type(of: self))
fatalError("\(className) does not implement init(coder:)")
}
}
......@@ -21,4 +21,3 @@ extension UIColor {
alpha: 1.0)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment