Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ let package = Package(
platforms: [
.iOS(.v26),
.macOS(.v26),
.tvOS(.v27),
.visionOS(.v26),
],
products: [
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 23 additions & 0 deletions Sources/ImageForgeKit/ImageForgeAvailability.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
10 changes: 10 additions & 0 deletions Sources/ImageForgeKit/ImageForgeConcept.swift
Original file line number Diff line number Diff line change
@@ -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)
}
16 changes: 15 additions & 1 deletion Sources/ImageForgeKit/ImageForgeKit.docc/ImageForgeKit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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``
2 changes: 1 addition & 1 deletion Sources/ImageForgeKit/ImageForgeKit.swift
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/// A namespace for ImageForgeKit capabilities.
/// A namespace for ImageForgeKit capabilities and package identity.
public enum ImageForgeKit {}
25 changes: 25 additions & 0 deletions Sources/ImageForgeKit/ImageForgeLogging.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
12 changes: 12 additions & 0 deletions Sources/ImageForgeKit/ImageForgeRequest.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
12 changes: 12 additions & 0 deletions Sources/ImageForgeKit/ImageForgeResult.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
41 changes: 41 additions & 0 deletions Sources/ImageForgeKit/View+ImageForge.swift
Original file line number Diff line number Diff line change
@@ -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<Bool>,
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
31 changes: 31 additions & 0 deletions Tests/ImageForgeKitTests/ImageForgeKitTests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Foundation
import Testing

@testable import ImageForgeKit
Expand All @@ -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")
}