-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #195 from cocoatype/185-save-photo-in-place-via-done
Save photo in place via done alert
- Loading branch information
Showing
39 changed files
with
530 additions
and
399 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
// Created by Geoff Pado on 7/1/19. | ||
// Copyright © 2019 Cocoatype, LLC. All rights reserved. | ||
|
||
import Editing | ||
import ErrorHandling | ||
import MobileCoreServices | ||
import SwiftUI | ||
import UIKit | ||
|
||
class ActionHostingController: UIHostingController<ActionView> { | ||
init() { | ||
super.init(rootView: ActionView()) | ||
} | ||
|
||
override func loadView() { | ||
view = UIView() | ||
view.backgroundColor = .clear | ||
view.isOpaque = false | ||
} | ||
|
||
override func viewDidAppear(_ animated: Bool) { | ||
super.viewDidAppear(animated) | ||
loadImageFromExtensionContext() | ||
} | ||
|
||
private func loadImageFromExtensionContext() { | ||
print(#function) | ||
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) | ||
let imageDataString = imageData.base64EncodedString() | ||
guard let callbackURL = URL(string: "highlighter://x-callback-url/open?imageData=\(imageDataString)") else { throw ActionError.callbackURLConstructionFailed } | ||
self?.chain(selector: #selector(Self.openURL(_:)), object: callbackURL) | ||
self?.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) | ||
} catch { | ||
ErrorHandler().log(error) | ||
} | ||
} | ||
} | ||
|
||
// just to provide the selector | ||
@objc private func openURL(_ url: URL) {} | ||
|
||
@available(*, unavailable) | ||
required init(coder: NSCoder) { | ||
let className = String(describing: type(of: self)) | ||
fatalError("\(className) does not implement init(coder:)") | ||
} | ||
} | ||
|
||
enum ActionError: Error { | ||
case callbackURLConstructionFailed | ||
case imageURLNotFound | ||
case invalidImageData | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,65 +1,18 @@ | ||
// Created by Geoff Pado on 7/1/19. | ||
// Copyright © 2019 Cocoatype, LLC. All rights reserved. | ||
// Created by Geoff Pado on 7/12/24. | ||
// Copyright © 2024 Cocoatype, LLC. All rights reserved. | ||
|
||
import Editing | ||
import ErrorHandling | ||
import MobileCoreServices | ||
import SwiftUI | ||
import DesignSystem | ||
import UIKit | ||
|
||
class ActionViewController: UIHostingController<ActionView> { | ||
init() { | ||
super.init(rootView: ActionView()) | ||
class ActionViewController: UIViewController { | ||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { | ||
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) | ||
embed(ActionHostingController()) | ||
} | ||
|
||
override func loadView() { | ||
view = UIView() | ||
view.backgroundColor = .clear | ||
view.isOpaque = false | ||
} | ||
|
||
override func viewDidAppear(_ animated: Bool) { | ||
super.viewDidAppear(animated) | ||
loadImageFromExtensionContext() | ||
} | ||
|
||
private func loadImageFromExtensionContext() { | ||
print(#function) | ||
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) | ||
let imageDataString = imageData.base64EncodedString() | ||
guard let callbackURL = URL(string: "highlighter://x-callback-url/open?imageData=\(imageDataString)") else { throw ActionError.callbackURLConstructionFailed } | ||
self?.chain(selector: #selector(ActionViewController.openURL(_:)), object: callbackURL) | ||
self?.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) | ||
} catch { | ||
ErrorHandler().log(error) | ||
} | ||
} | ||
} | ||
|
||
// just to provide the selector | ||
@objc private func openURL(_ url: URL) {} | ||
|
||
@available(*, unavailable) | ||
required init(coder: NSCoder) { | ||
let className = String(describing: type(of: self)) | ||
fatalError("\(className) does not implement init(coder:)") | ||
let typeName = NSStringFromClass(type(of: self)) | ||
fatalError("\(typeName) does not implement init(coder:)") | ||
} | ||
} | ||
|
||
enum ActionError: Error { | ||
case callbackURLConstructionFailed | ||
case imageURLNotFound | ||
case invalidImageData | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// Created by Geoff Pado on 7/12/24. | ||
// Copyright © 2024 Cocoatype, LLC. All rights reserved. | ||
|
||
import Foundation | ||
import Photos | ||
import Redactions | ||
|
||
public class CopyExporter: NSObject { | ||
private let preparedURL: URL | ||
|
||
public init(preparedURL: URL) { | ||
self.preparedURL = preparedURL | ||
} | ||
|
||
public func export() async throws { | ||
try await PHPhotoLibrary.shared().performChanges { [preparedURL] in | ||
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: preparedURL) | ||
} | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
Modules/Capabilities/Exporting/Sources/ExportingActivityController.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// Created by Geoff Pado on 7/1/24. | ||
// Copyright © 2024 Cocoatype, LLC. All rights reserved. | ||
|
||
import Photos | ||
import Redactions | ||
import Rendering | ||
import UIKit | ||
import UniformTypeIdentifiers | ||
|
||
public class ExportingActivityController: UIActivityViewController { | ||
public init(image: UIImage, asset: PHAsset?, redactions: [Redaction]) async throws { | ||
let preparer = ExportingPreparer(image: image, asset: asset, redactions: redactions) | ||
let temporaryURL = try await preparer.preparedURL | ||
super.init(activityItems: [temporaryURL], applicationActivities: Self.applicationActivities(asset: asset, redactions: redactions)) | ||
excludedActivityTypes = [.saveToCameraRoll] | ||
} | ||
|
||
static func applicationActivities(asset: PHAsset?, redactions: [Redaction]) -> [UIActivity] { | ||
if let asset { | ||
[SaveActivity(asset: asset, redactions: redactions), SaveCopyActivity()] | ||
} else { | ||
[SaveCopyActivity()] | ||
} | ||
} | ||
} |
3 changes: 2 additions & 1 deletion
3
...Exporting/Sources/SaveActivityError.swift → ...es/Exporting/Sources/ExportingError.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
Modules/Capabilities/Exporting/Sources/ExportingPreparer.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// Created by Geoff Pado on 7/12/24. | ||
// Copyright © 2024 Cocoatype, LLC. All rights reserved. | ||
|
||
import Photos | ||
import Redactions | ||
import Rendering | ||
import UIKit | ||
import UniformTypeIdentifiers | ||
|
||
public class ExportingPreparer: NSObject { | ||
private let image: UIImage | ||
private let asset: PHAsset? | ||
private let redactions: [Redaction] | ||
|
||
public init(image: UIImage, asset: PHAsset?, redactions: [Redaction]) { | ||
self.image = image | ||
self.asset = asset | ||
self.redactions = redactions | ||
} | ||
|
||
public var preparedURL: URL { | ||
get async throws { | ||
let exportedImage = try await PhotoRenderer.render(image, redactions: redactions) | ||
|
||
let representedURLName = "\(ExportingStrings.PhotoEditingExporter.defaultImageName).\(imageType.preferredFilenameExtension ?? "png")" | ||
let temporaryURL = URL(fileURLWithPath: NSTemporaryDirectory()) | ||
.appendingPathComponent(representedURLName) | ||
|
||
let data: Data? | ||
|
||
switch imageType { | ||
case .jpeg: | ||
data = exportedImage.jpegData(compressionQuality: 0.9) | ||
default: | ||
data = exportedImage.pngData() | ||
} | ||
|
||
guard let data else { throw ExportingError.noDataGenerated } | ||
try data.write(to: temporaryURL) | ||
|
||
return temporaryURL | ||
} | ||
} | ||
|
||
var imageType: UTType { | ||
asset?.imageType ?? image.imageType ?? .png | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
Modules/Capabilities/Exporting/Sources/Extensions/PHAssetExtensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Created by Geoff Pado on 7/12/24. | ||
// Copyright © 2024 Cocoatype, LLC. All rights reserved. | ||
|
||
import Photos | ||
|
||
extension PHAsset { | ||
var imageType: UTType? { | ||
let resources = PHAssetResource.assetResources(for: self) | ||
|
||
guard let resource = resources.first(where: { $0.type == .photo }) | ||
else { return nil } | ||
|
||
return UTType(resource.uniformTypeIdentifier) | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
Modules/Capabilities/Exporting/Sources/Extensions/PHContentEditingOutputExtensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// Created by Geoff Pado on 7/12/24. | ||
// Copyright © 2024 Cocoatype, LLC. All rights reserved. | ||
|
||
import Foundation | ||
import Photos | ||
import UniformTypeIdentifiers | ||
|
||
extension PHContentEditingOutput { | ||
var renderingInformation: (URL, UTType) { | ||
guard #available(iOS 17, *), | ||
let type = defaultRenderedContentType, | ||
let typeURL = try? renderedContentURL(for: type) | ||
else { return (renderedContentURL, .jpeg) } | ||
|
||
return (typeURL, type) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
Modules/Capabilities/Exporting/Sources/InPlaceExporter.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// Created by Geoff Pado on 7/12/24. | ||
// Copyright © 2024 Cocoatype, LLC. All rights reserved. | ||
|
||
import ErrorHandling | ||
import Foundation | ||
import Photos | ||
import Redactions | ||
import UIKit | ||
|
||
public class InPlaceExporter: NSObject { | ||
private let preparedURL: URL | ||
private let asset: PHAsset | ||
private let redactions: [Redaction] | ||
|
||
public init(preparedURL: URL, asset: PHAsset, redactions: [Redaction]) { | ||
self.preparedURL = preparedURL | ||
self.asset = asset | ||
self.redactions = redactions | ||
} | ||
|
||
public func export() async throws { | ||
return try await withCheckedThrowingContinuation { continuation in | ||
asset.requestContentEditingInput(with: nil) { [weak self] contentEditingInput, _ in | ||
Task { [weak self] in | ||
do { | ||
guard let self, | ||
let input = contentEditingInput | ||
else { throw ExportingError.noInputProvided } | ||
|
||
let factory = OutputFactory(preparedURL: preparedURL, redactions: redactions) | ||
let output = try factory.output(from: input) | ||
|
||
try await PHPhotoLibrary.shared().performChanges { [asset] in | ||
let changeRequest = PHAssetChangeRequest(for: asset) | ||
changeRequest.contentEditingOutput = output | ||
print("Change request created with contentEditingOutput: \(output)") | ||
} | ||
|
||
print("Changes successfully committed to the photo library.") | ||
continuation.resume() | ||
} catch { | ||
ErrorHandler().log(error) | ||
print("Error during performChanges: \(error)") | ||
continuation.resume(throwing: error) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
Modules/Capabilities/Exporting/Sources/OutputFactory.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// Created by Geoff Pado on 7/12/24. | ||
// Copyright © 2024 Cocoatype, LLC. All rights reserved. | ||
|
||
import Photos | ||
import Redactions | ||
import UIKit | ||
|
||
public class OutputFactory { | ||
private let preparedURL: URL | ||
private let redactions: [Redaction] | ||
|
||
public init(preparedURL: URL, redactions: [Redaction]) { | ||
self.preparedURL = preparedURL | ||
self.redactions = redactions | ||
} | ||
|
||
public func output(from input: PHContentEditingInput) throws -> PHContentEditingOutput { | ||
let output = PHContentEditingOutput(contentEditingInput: input) | ||
guard let formatVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as? String else { throw ExportingError.missingBundleVersion } | ||
let data = try JSONEncoder().encode(SaveActivityAdjustmentData(redactions: redactions)) | ||
output.adjustmentData = PHAdjustmentData(formatIdentifier: SaveActivityAdjustmentData.formatIdentifier, formatVersion: formatVersion, data: data) | ||
let (renderedContentURL, renderType) = output.renderingInformation | ||
|
||
guard let image = UIImage(contentsOfFile: preparedURL.path) else { throw ExportingError.failedImageDecode } | ||
guard let data = image.encoded(as: renderType) else { throw ExportingError.failedImageEncode } | ||
|
||
try data.write(to: renderedContentURL) | ||
return output | ||
} | ||
} |
Oops, something went wrong.