Can I import images into my app with App Intent Schemas?

I'm trying to create an App Intent that takes in an image and copies it to a destination within my app.

Sure, this can be done with shortcuts, but that's lame, I want siri to grab an image off the screen and invoke the action!

Without an app schema, it doesn't seem like siri can tell what's possible in the app, so I'm trying to find a schema that fits?

I've started trying to do this with the photos.addAssetsToAlbum App Intent schema, but it doesn't seem like Siri is onboard with the concept.

It's replied many times that it is unable to add things to my App and that I should try via other means like opening my app and importing directly.

Should be I be trying this with a different intent schema?

I was thinking about files.open, but documentation says that Schema is not available to Siri.

Answered by Engineer in 891973022

I believe your question is similar to the thread here. You will also need to adopt the .photos.createAsset schema.

Hello!

I would need to see your photos.addAssetsToAlbum adoption as well as how your photo is being set as a photos.asset. It will also help to let Siri know what your image is by associating your view with an appEntityIdentifier if the specific behavior you are looking for is for the intent to act upon what's on screen. Check out appEntityIdentifier and feel free to share code snippets, but I would recommend to associate that view with your corresponding entity and see if that

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()
    }
}

I believe your question is similar to the thread here. You will also need to adopt the .photos.createAsset schema.

Can I import images into my app with App Intent Schemas?
 
 
Q