Photo Asset Entity:
@available(anyAppleOS 27.0, *)
@AppEntity(schema: .photos.asset)
public struct ImageEntity: AppEntity, Sendable {
public static let defaultQuery = PhotoEntityQuery()
public let id: FileEntityIdentifier
var creationDate: Date?
var location: GeoToolbox.PlaceDescriptor?
var assetType: ImageEntityAssetType?
var isFavorite: Bool
var isHidden: Bool
var hasSuggestedEdits: Bool
var aperture: Double?
var exposure: Double?
var saturation: Double?
var warmth: Double?
var filter: ImageEntityFilterType?
var isPortraitModeEnabled: Bool?
private var name: String?
public var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: LocalizedStringResource(stringLiteral: name ?? "Photo"),
image: .init(systemName: "photo")
)
}
init(
id: FileEntityIdentifier,
name: String?,
creationDate: Date?,
location: GeoToolbox.PlaceDescriptor? = nil,
assetType: ImageEntityAssetType? = .photo,
isFavorite: Bool = false,
isHidden: Bool = false,
hasSuggestedEdits: Bool = false,
aperture: Double? = nil,
exposure: Double? = nil,
saturation: Double? = nil,
warmth: Double? = nil,
filter: ImageEntityFilterType? = nil,
isPortraitModeEnabled: Bool? = nil
) {
self.id = id
self.name = name
self.creationDate = creationDate
self.location = location
self.assetType = assetType
self.isFavorite = isFavorite
self.isHidden = isHidden
self.hasSuggestedEdits = hasSuggestedEdits
self.aperture = aperture
self.exposure = exposure
self.saturation = saturation
self.warmth = warmth
self.filter = filter
self.isPortraitModeEnabled = isPortraitModeEnabled
}
var fileURL: URL? {
get async throws {
try await id.fileURL
}
}
}
@available(anyAppleOS 27.0, *)
public struct PhotoEntityQuery: EntityStringQuery {
enum Errors: Error {
case unableToRetrieveURL
}
public init() {}
public func entities(matching string: String) async throws -> [ImageEntity] {
[]
}
public func entities(for identifiers: [ImageEntity.ID]) async throws -> [ImageEntity] {
if identifiers.isEmpty { return [] }
var results: [(index: Int, entity: ImageEntity)] = []
results.reserveCapacity(identifiers.count)
await withTaskGroup(of: (Int, ImageEntity)?.self) { group in
for (index, id) in identifiers.enumerated() {
group.addTask {
do {
let entity = try await constructEntity(for: id)
return (index, entity)
} catch {
return nil
}
}
}
for await result in group {
if let result {
results.append(result)
}
}
}
results.sort { $0.index < $1.index }
return results.map(\.entity)
}
private func constructEntity(for id: ImageEntity.ID) async throws -> ImageEntity {
guard let url = try await id.fileURL else { throw Errors.unableToRetrieveURL }
let attributes = try? FileManager.default.attributesOfItem(atPath: url.path())
let creationDate = attributes?[.creationDate] as? Date
let assetType: ImageEntityAssetType = url.conforms(toAny: [.movie, .video]) ? .video : .photo
return ImageEntity(
id: id,
name: url.lastPathComponent,
creationDate: creationDate,
assetType: assetType
)
}
}
Add to album entity:
@available(anyAppleOS 27.0, *)
@AppIntent(schema: .photos.addAssetsToAlbum)
public struct CopyToAPPNAMEAppIntent: AppIntent {
public static let allowedExecutionTargets: IntentExecutionTargets = [.main]
public static let title: LocalizedStringResource = "Copy to APP NAME"
public static let description = IntentDescription("Copies an image to a specified gallery or inbox of APP NAME.")
public init() {}
@Parameter(title: "Images", description: "Images to copy", inputConnectionBehavior: .default)
public var assets: [ImageEntity]
@Parameter(title: "Gallery", optionsProvider: GalleryOptionsProvider())
public var album: GalleryEntity
@MainActor
public func perform() async throws -> some IntentResult {
guard !assets.isEmpty else {
throw $assets.needsValueError("Which assets do you want to copy?")
}
let services = APPServices.default
let sourceItemInterface = services.makeSourceItemInterface()
var transferDestination: TransferLocation? = nil
switch album.type {
case .gallery(let uri):
if let gallerySourceItem = sourceItemInterface.getSourceItemByManagedObjectURI(uri, context: services.coreDataController.viewContext),
let destination = TransferLocation(sourceItem: gallerySourceItem) {
transferDestination = destination
}
case .inbox(let url):
transferDestination = TransferLocation.url(url)
}
guard let transferDestination else {
throw $album.needsValueError("Gallery not valid. Choose another and try again.")
}
for asset in assets {
guard let assetURL = try await asset.fileURL else {
throw $assets.needsValueError("One of the selected assets is not valid. Choose another and try again.")
}
let transfer = FileURLTransfer(assetURL, to: transferDestination, transferType: .copy)
let _: Void = try await withCheckedThrowingContinuation { continuation in
transfer.transfer { possibleError in
if let error = possibleError {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}
return .result()
}
}