Skip to content

Commit

Permalink
Merge pull request #232 from cocoatype/224-exported-image-telemetry
Browse files Browse the repository at this point in the history
Add telemetry for exporting
  • Loading branch information
Arclite authored Dec 8, 2024
2 parents 0067aa1 + c138ae3 commit d36cc3d
Show file tree
Hide file tree
Showing 17 changed files with 276 additions and 72 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Created by Geoff Pado on 12/5/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import Photos

protocol ExportableAsset {
func requestContentEditingInput(with options: PHContentEditingInputRequestOptions?) async -> (PHContentEditingInput?, [AnyHashable: Any])

var changeRequest: PHAssetChangeRequest { get }
}

extension PHAsset: ExportableAsset {
public func requestContentEditingInput(with options: PHContentEditingInputRequestOptions?) async -> (PHContentEditingInput?, [AnyHashable: Any]) {
await withCheckedContinuation { continuation in
requestContentEditingInput(with: options) { input, info in
continuation.resume(returning: (input, info))
}
}
}

public var changeRequest: PHAssetChangeRequest {
PHAssetChangeRequest(for: self)
}
}

enum ExportableAssetError: Error {
case unexpectedNil
}
20 changes: 18 additions & 2 deletions Modules/Capabilities/Exporting/Sources/CopyExporter.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
// Created by Geoff Pado on 7/12/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import Defaults
import Foundation
import Logging
import Photos
import Redactions

public class CopyExporter: NSObject {
private let preparedURL: URL
public convenience init(preparedURL: URL) {
self.init(preparedURL: preparedURL, logger: Logging.logger, library: PHPhotoLibrary.shared())
}

public init(preparedURL: URL) {
private let logger: any Logger
private let library: any PhotoLibrary
init(
preparedURL: URL,
logger: any Logger,
library: any PhotoLibrary
) {
self.preparedURL = preparedURL
self.logger = logger
self.library = library
}

public func export() async throws {
try await PHPhotoLibrary.shared().performChanges { [preparedURL] in
try await library.performChanges { [preparedURL] in
PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: preparedURL)
}

Defaults.numberOfSaves = Defaults.numberOfSaves + 1
logger.log(ExportingEventFactory().event(style: .copy))
}
}
11 changes: 11 additions & 0 deletions Modules/Capabilities/Exporting/Sources/Exporting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Created by Geoff Pado on 12/8/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import Foundation
import Redactions

public enum Exporting {
public static func outputFactory(preparedURL: URL, redactions: [Redaction]) -> any OutputFactory {
return PhotoOutputFactory(preparedURL: preparedURL, redactions: redactions)
}
}
30 changes: 30 additions & 0 deletions Modules/Capabilities/Exporting/Sources/ExportingEventFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Created by Geoff Pado on 12/5/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import Defaults
import Logging

struct ExportingEventFactory {
enum OutputStyle {
case inPlace
case copy
}

private static let eventName: Event.Name = "Exporting.successfulExport"
private static let styleKey = "style"
private static let exportCountKey = "exportCount"
func event(style: OutputStyle) -> Event {
let styleValue = switch style {
case .inPlace: "inPlace"
case .copy: "copy"
}

return Event(
name: Self.eventName,
info: [
Self.styleKey: styleValue,
Self.exportCountKey: String(Defaults.numberOfSaves),
]
)
}
}
67 changes: 38 additions & 29 deletions Modules/Capabilities/Exporting/Sources/InPlaceExporter.swift
Original file line number Diff line number Diff line change
@@ -1,50 +1,59 @@
// Created by Geoff Pado on 7/12/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import Defaults
import ErrorHandling
import Foundation
import Logging
import Photos
import Redactions
import UIKit

public class InPlaceExporter: NSObject {
private let preparedURL: URL
private let asset: PHAsset
private let redactions: [Redaction]
public convenience init(
preparedURL: URL,
asset: PHAsset,
redactions: [Redaction]
) {
self.init(
asset: asset,
outputFactory: PhotoOutputFactory(preparedURL: preparedURL, redactions: redactions),
library: PHPhotoLibrary.shared()
)
}

public init(preparedURL: URL, asset: PHAsset, redactions: [Redaction]) {
self.preparedURL = preparedURL
private let asset: any ExportableAsset
private let outputFactory: any OutputFactory
private let logger: any Logger
private let library: any PhotoLibrary
init(
asset: any ExportableAsset,
outputFactory: any OutputFactory,
logger: any Logger = Logging.logger,
library: any PhotoLibrary
) {
self.asset = asset
self.redactions = redactions
self.outputFactory = outputFactory
self.logger = logger
self.library = library
}

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)
let (contentEditingInput, _) = await asset.requestContentEditingInput(with: nil)
do {
guard let contentEditingInput else { throw ExportingError.noInputProvided }

try await PHPhotoLibrary.shared().performChanges { [asset] in
let changeRequest = PHAssetChangeRequest(for: asset)
changeRequest.contentEditingOutput = output
print("Change request created with contentEditingOutput: \(output)")
}
let output = try outputFactory.output(from: contentEditingInput)

print("Changes successfully committed to the photo library.")
continuation.resume()
} catch {
ErrorHandler().log(error)
print("Error during performChanges: \(error)")
continuation.resume(throwing: error)
}
}
try await library.performChanges { [asset] in
asset.changeRequest.contentEditingOutput = output
}

Defaults.numberOfSaves = Defaults.numberOfSaves + 1
logger.log(ExportingEventFactory().event(style: .inPlace))
} catch {
ErrorHandler().log(error)
throw error
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Created by Geoff Pado on 12/8/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import Photos

protocol PhotoLibrary {
func performChanges(
_ changeBlock: @escaping () -> Void) async throws
}

extension PHPhotoLibrary: PhotoLibrary {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Created by Geoff Pado on 12/5/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import Photos

public protocol OutputFactory {
func output(from input: PHContentEditingInput) throws -> PHContentEditingOutput
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import Photos
import Redactions
import UIKit

public class OutputFactory {
class PhotoOutputFactory: OutputFactory {
private let preparedURL: URL
private let redactions: [Redaction]

public init(preparedURL: URL, redactions: [Redaction]) {
init(preparedURL: URL, redactions: [Redaction]) {
self.preparedURL = preparedURL
self.redactions = redactions
}

public func output(from input: PHContentEditingInput) throws -> PHContentEditingOutput {
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))
Expand Down
28 changes: 28 additions & 0 deletions Modules/Capabilities/Exporting/Tests/CopyExporterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Created by Geoff Pado on 12/8/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import Defaults
import LoggingDoubles
import XCTest

@testable import Exporting
@testable import Logging

class CopyExporterTests: XCTestCase {
func testWhenExporterSucceedsThenEventLogged() async throws {
let logger = SpyLogger()
let exporter = CopyExporter(
preparedURL: URL(fileURLWithPath: "/"),
logger: logger,
library: StubPhotoLibrary()
)
Defaults.numberOfSaves = 0

try await exporter.export()

let loggedEvent = try XCTUnwrap(logger.loggedEvents.first)
XCTAssertEqual(loggedEvent.name, "Exporting.successfulExport")
XCTAssertEqual(loggedEvent.info["style"], "copy")
XCTAssertEqual(loggedEvent.info["exportCount"], "1")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Created by Geoff Pado on 12/5/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import Photos

@testable import Exporting

struct StubExportableAsset: ExportableAsset {
func requestContentEditingInput(with options: PHContentEditingInputRequestOptions?) async -> (PHContentEditingInput?, [AnyHashable: Any]) {
return (PHContentEditingInput(), [:])
}

var changeRequest: PHAssetChangeRequest {
let asset = PHAsset()
return PHAssetChangeRequest(for: asset)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Created by Geoff Pado on 12/5/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import Photos

@testable import Exporting

struct StubOutputFactory: OutputFactory {
func output(from input: PHContentEditingInput) throws -> PHContentEditingOutput {
PHContentEditingOutput()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Created by Geoff Pado on 12/8/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

@testable import Exporting

struct StubPhotoLibrary: PhotoLibrary {
func performChanges(_ changeBlock: @escaping () -> Void) async throws {}
}
7 changes: 0 additions & 7 deletions Modules/Capabilities/Exporting/Tests/ExportingTests.swift

This file was deleted.

31 changes: 31 additions & 0 deletions Modules/Capabilities/Exporting/Tests/InPlaceExporterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Created by Geoff Pado on 12/5/24.
// Copyright © 2024 Cocoatype, LLC. All rights reserved.

import Defaults
import LoggingDoubles
import XCTest

@testable import Exporting
@testable import Logging

class InPlaceExporterTests: XCTestCase {
func testWhenExporterSucceedsThenEventLogged() async throws {
let asset = StubExportableAsset()
let outputFactory = StubOutputFactory()
let logger = SpyLogger()
let exporter = InPlaceExporter(
asset: asset,
outputFactory: outputFactory,
logger: logger,
library: StubPhotoLibrary()
)
Defaults.numberOfSaves = 0

try await exporter.export()

let loggedEvent = try XCTUnwrap(logger.loggedEvents.first)
XCTAssertEqual(loggedEvent.name, "Exporting.successfulExport")
XCTAssertEqual(loggedEvent.info["style"], "inPlace")
XCTAssertEqual(loggedEvent.info["exportCount"], "1")
}
}
Loading

0 comments on commit d36cc3d

Please sign in to comment.