From 2714f5ffbd4bf4f32e82106ba32ed823d9422633 Mon Sep 17 00:00:00 2001 From: Fernando Fernandes Date: Fri, 18 Sep 2026 01:39:59 +0200 Subject: [PATCH] Add interactive Image Playground flow --- CHANGELOG.md | 3 ++ Package.swift | 1 + README.md | 17 +++++++- .../ImageForgeAvailability.swift | 23 +++++++++++ Sources/ImageForgeKit/ImageForgeConcept.swift | 10 +++++ .../ImageForgeKit.docc/ImageForgeKit.md | 16 +++++++- Sources/ImageForgeKit/ImageForgeKit.swift | 2 +- Sources/ImageForgeKit/ImageForgeLogging.swift | 25 +++++++++++ Sources/ImageForgeKit/ImageForgeRequest.swift | 12 ++++++ Sources/ImageForgeKit/ImageForgeResult.swift | 12 ++++++ Sources/ImageForgeKit/View+ImageForge.swift | 41 +++++++++++++++++++ .../ImageForgeKitTests.swift | 31 ++++++++++++++ 12 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 Sources/ImageForgeKit/ImageForgeAvailability.swift create mode 100644 Sources/ImageForgeKit/ImageForgeConcept.swift create mode 100644 Sources/ImageForgeKit/ImageForgeLogging.swift create mode 100644 Sources/ImageForgeKit/ImageForgeRequest.swift create mode 100644 Sources/ImageForgeKit/ImageForgeResult.swift create mode 100644 Sources/ImageForgeKit/View+ImageForge.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 6453baa..e4f0019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,3 +7,6 @@ All notable changes to ImageForgeKit are documented here. ### Added - Bootstrapped the Swift package, DocC catalog, CI/CD workflows, shared AgentGuidelines integration, and repository policy. +- Add a portable request and result model for interactive Image Playground generation. +- Add a SwiftUI presentation helper with availability and cancellation handling. +- Keep shared application targets buildable on tvOS while reporting generation as unavailable. diff --git a/Package.swift b/Package.swift index 5faf1aa..603e445 100644 --- a/Package.swift +++ b/Package.swift @@ -7,6 +7,7 @@ let package = Package( platforms: [ .iOS(.v26), .macOS(.v26), + .tvOS(.v27), .visionOS(.v26), ], products: [ diff --git a/README.md b/README.md index fb9e95b..046cd37 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,22 @@ # ImageForgeKit -ImageForgeKit is a reusable Apple-platform integration layer for the system-managed Image Playground experience. It will centralize availability checks, presentation wiring, cancellation and result handling, and safe transfer of temporary generated files into host-owned storage. +ImageForgeKit is a reusable Apple-platform integration layer for the system-managed Image Playground experience. It centralizes availability checks, concept setup, SwiftUI presentation wiring, cancellation, and temporary result handling while leaving persistence to the host application. -The package is currently a bootstrapped foundation. Its public API is intentionally limited to a namespace while the first consumer establishes the smallest useful contract. +```swift +let request = ImageForgeRequest(concepts: [.text("a small red house")]) + +content + .imageForgeSheet( + isPresented: $isPresented, + request: request, + onCompletion: { result in + // Copy result.temporaryURL into host-owned storage here. + } + ) +``` + +The presentation API is available on Apple platforms that support Image Playground. The package also compiles on tvOS so shared multiplatform application targets can resolve it, while ``ImageForgeAvailability/current`` reports unavailable there. ## Documentation diff --git a/Sources/ImageForgeKit/ImageForgeAvailability.swift b/Sources/ImageForgeKit/ImageForgeAvailability.swift new file mode 100644 index 0000000..4d3532e --- /dev/null +++ b/Sources/ImageForgeKit/ImageForgeAvailability.swift @@ -0,0 +1,23 @@ +import Foundation + +#if canImport(ImagePlayground) && !os(tvOS) + import ImagePlayground +#endif + +/// Describes whether the current device can present Image Playground. +public enum ImageForgeAvailability: Equatable, Sendable { + /// Image Playground can be presented. + case available + + /// Image Playground is unavailable on this device or platform. + case unavailable + + /// The current device's Image Playground capability. + public static var current: Self { + #if canImport(ImagePlayground) && !os(tvOS) + ImagePlaygroundViewController.isAvailable ? .available : .unavailable + #else + .unavailable + #endif + } +} diff --git a/Sources/ImageForgeKit/ImageForgeConcept.swift b/Sources/ImageForgeKit/ImageForgeConcept.swift new file mode 100644 index 0000000..bf8b537 --- /dev/null +++ b/Sources/ImageForgeKit/ImageForgeConcept.swift @@ -0,0 +1,10 @@ +import Foundation + +/// A portable concept used to seed the system Image Playground experience. +public enum ImageForgeConcept: Equatable, Sendable { + /// Lets Image Playground extract useful concepts from prose. + case extracted(text: String, title: String? = nil) + + /// Supplies a concise text concept directly. + case text(String) +} diff --git a/Sources/ImageForgeKit/ImageForgeKit.docc/ImageForgeKit.md b/Sources/ImageForgeKit/ImageForgeKit.docc/ImageForgeKit.md index 33ba8c0..0071c3e 100644 --- a/Sources/ImageForgeKit/ImageForgeKit.docc/ImageForgeKit.md +++ b/Sources/ImageForgeKit/ImageForgeKit.docc/ImageForgeKit.md @@ -4,4 +4,18 @@ Integrate Apple's system-managed Image Playground experience while keeping gener ## Overview -The initial bootstrap intentionally contains no speculative public API. Add capabilities only when a concrete consumer establishes their requirements and tests. +Use ``ImageForgeAvailability/current`` to decide whether to offer generation, build an ``ImageForgeRequest`` from portable ``ImageForgeConcept`` values, and apply `SwiftUI/View/imageForgeSheet(isPresented:request:onCompletion:onCancellation:)` to a host view. + +Image Playground supplies ``ImageForgeResult/temporaryURL`` only as temporary output. The completion callback is the host application's ownership boundary: copy the file into durable, application-owned storage before returning. ImageForgeKit never persists images, reads a photo library, or synchronizes media. + +## Topics + +### Availability and requests + +- ``ImageForgeAvailability`` +- ``ImageForgeConcept`` +- ``ImageForgeRequest`` + +### Presentation results + +- ``ImageForgeResult`` diff --git a/Sources/ImageForgeKit/ImageForgeKit.swift b/Sources/ImageForgeKit/ImageForgeKit.swift index 94f616a..6b131bb 100644 --- a/Sources/ImageForgeKit/ImageForgeKit.swift +++ b/Sources/ImageForgeKit/ImageForgeKit.swift @@ -1,2 +1,2 @@ -/// A namespace for ImageForgeKit capabilities. +/// A namespace for ImageForgeKit capabilities and package identity. public enum ImageForgeKit {} diff --git a/Sources/ImageForgeKit/ImageForgeLogging.swift b/Sources/ImageForgeKit/ImageForgeLogging.swift new file mode 100644 index 0000000..8320d31 --- /dev/null +++ b/Sources/ImageForgeKit/ImageForgeLogging.swift @@ -0,0 +1,25 @@ +import AppLogger + +enum ImageForgeLogging { + static let emoji = "🎨" + static let subsystem = "com.thatfactory.imageforgekit" + + enum Event: Equatable { + case cancelled + case completed + + var message: String { + switch self { + case .cancelled: + "\(emoji) playground | result=cancelled" + case .completed: + "\(emoji) playground | result=completed" + } + } + } + + static func emit(_ event: Event) { + AppLogger(subsystem: subsystem, category: "presentation") + .log(level: .debug, event.message) + } +} diff --git a/Sources/ImageForgeKit/ImageForgeRequest.swift b/Sources/ImageForgeKit/ImageForgeRequest.swift new file mode 100644 index 0000000..2a4ecb1 --- /dev/null +++ b/Sources/ImageForgeKit/ImageForgeRequest.swift @@ -0,0 +1,12 @@ +import Foundation + +/// Input for an interactive Image Playground presentation. +public struct ImageForgeRequest: Equatable, Sendable { + /// Concepts that seed the system-managed experience. + public var concepts: [ImageForgeConcept] + + /// Creates a request from one or more concepts. + public init(concepts: [ImageForgeConcept]) { + self.concepts = concepts + } +} diff --git a/Sources/ImageForgeKit/ImageForgeResult.swift b/Sources/ImageForgeKit/ImageForgeResult.swift new file mode 100644 index 0000000..146338f --- /dev/null +++ b/Sources/ImageForgeKit/ImageForgeResult.swift @@ -0,0 +1,12 @@ +import Foundation + +/// A generated image accepted by the user. +public struct ImageForgeResult: Equatable, Sendable { + /// A temporary file URL that the host must copy before returning from its callback. + public let temporaryURL: URL + + /// Creates a result for a temporary Image Playground output. + public init(temporaryURL: URL) { + self.temporaryURL = temporaryURL + } +} diff --git a/Sources/ImageForgeKit/View+ImageForge.swift b/Sources/ImageForgeKit/View+ImageForge.swift new file mode 100644 index 0000000..8fe9203 --- /dev/null +++ b/Sources/ImageForgeKit/View+ImageForge.swift @@ -0,0 +1,41 @@ +#if canImport(ImagePlayground) && !os(tvOS) + import ImagePlayground + import SwiftUI + + extension View { + /// Presents Image Playground and maps its temporary result into ImageForgeKit values. + /// + /// Copy ``ImageForgeResult/temporaryURL`` into host-owned storage synchronously from + /// `onCompletion`. The system may remove the temporary file after the callback returns. + public func imageForgeSheet( + isPresented: Binding, + request: ImageForgeRequest, + onCompletion: @escaping (ImageForgeResult) -> Void, + onCancellation: (() -> Void)? = nil + ) -> some View { + imagePlaygroundSheet( + isPresented: isPresented, + concepts: request.concepts.map(\.imagePlaygroundConcept), + onCompletion: { temporaryURL in + ImageForgeLogging.emit(.completed) + onCompletion(ImageForgeResult(temporaryURL: temporaryURL)) + }, + onCancellation: { + ImageForgeLogging.emit(.cancelled) + onCancellation?() + } + ) + } + } + + extension ImageForgeConcept { + fileprivate var imagePlaygroundConcept: ImagePlaygroundConcept { + switch self { + case .extracted(let text, let title): + .extracted(from: text, title: title) + case .text(let text): + .text(text) + } + } + } +#endif diff --git a/Tests/ImageForgeKitTests/ImageForgeKitTests.swift b/Tests/ImageForgeKitTests/ImageForgeKitTests.swift index 0b0832a..192c5d3 100644 --- a/Tests/ImageForgeKitTests/ImageForgeKitTests.swift +++ b/Tests/ImageForgeKitTests/ImageForgeKitTests.swift @@ -1,3 +1,4 @@ +import Foundation import Testing @testable import ImageForgeKit @@ -6,3 +7,33 @@ import Testing func packageNamespaceIsAvailable() { _ = ImageForgeKit.self } + +@Test("Requests preserve portable concepts") +func requestsPreservePortableConcepts() { + let request = ImageForgeRequest( + concepts: [ + .text("a small red house"), + .extracted(text: "A welcoming home", title: "House"), + ] + ) + + #expect( + request.concepts == [ + .text("a small red house"), + .extracted(text: "A welcoming home", title: "House"), + ] + ) +} + +@Test("Results preserve the temporary URL") +func resultsPreserveTemporaryURL() { + let url = URL(filePath: "/tmp/generated-image") + + #expect(ImageForgeResult(temporaryURL: url).temporaryURL == url) +} + +@Test("Logging messages do not contain generated content") +func loggingMessagesArePrivacySafe() { + #expect(ImageForgeLogging.Event.cancelled.message == "🎨 playground | result=cancelled") + #expect(ImageForgeLogging.Event.completed.message == "🎨 playground | result=completed") +}