From 2815fcd13d69226054b9acbc698ec1059d3eee79 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 20:24:28 -0400 Subject: [PATCH 1/9] Let an image resize to its proposed space --- .../Sources/AndroidSwiftUICore/Primitives/Views.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/Views.swift b/AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/Views.swift index 57ea731..ccf4916 100644 --- a/AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/Views.swift +++ b/AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/Views.swift @@ -50,6 +50,7 @@ public struct Image: View { internal let name: String internal let systemName: String? + internal var isResizable = false public init(_ name: String) { self.name = name @@ -61,6 +62,14 @@ public struct Image: View { self.systemName = systemName } + /// Lets the image scale to fill its proposed space (pair with + /// `scaledToFit`/`scaledToFill`). A method on `Image` itself, as in SwiftUI. + public func resizable() -> Image { + var copy = self + copy.isResizable = true + return copy + } + public typealias Body = Never } @@ -68,6 +77,7 @@ extension Image: PrimitiveView { public func _render(in context: ResolveContext) -> RenderNode { var props: [String: PropValue] = ["name": .string(name)] if let systemName { props["systemName"] = .string(systemName) } + if isResizable { props["resizable"] = .bool(true) } return RenderNode(type: "Image", id: context.path, props: props) } } From ad89eedefc1009e04743c29d0e905cbec159dfdd Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 20:24:28 -0400 Subject: [PATCH 2/9] Add AsyncImage and image content modes --- .../Primitives/AsyncImage.swift | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/AsyncImage.swift diff --git a/AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/AsyncImage.swift b/AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/AsyncImage.swift new file mode 100644 index 0000000..c5a74fe --- /dev/null +++ b/AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/AsyncImage.swift @@ -0,0 +1,58 @@ +// +// AsyncImage.swift +// AndroidSwiftUICore +// +// A remote image. The interpreter fetches and decodes off the main thread, +// showing a progress indicator while loading and a placeholder on failure — +// the whole lifecycle stays Compose-side, so loading never touches the bridge. +// + +import Foundation + +public struct AsyncImage: View { + + internal let url: URL? + + public init(url: URL?) { + self.url = url + } + + public typealias Body = Never +} + +extension AsyncImage: PrimitiveView { + public func _render(in context: ResolveContext) -> RenderNode { + var props: [String: PropValue] = [:] + if let url { props["url"] = .string(url.absoluteString) } + return RenderNode(type: "AsyncImage", id: context.path, props: props) + } +} + +// MARK: - Content mode + +/// How a resizable image maps into the space offered it. +public enum ContentMode: String, Sendable { + case fit, fill +} + +public struct _ContentModeModifier: RenderModifier { + let mode: ContentMode + public var _modifierNode: ModifierNode { + ModifierNode(kind: "contentMode", args: ["mode": .string(mode.rawValue)]) + } +} + +public extension View { + + func scaledToFit() -> ModifiedContent { + modifier(_ContentModeModifier(mode: .fit)) + } + + func scaledToFill() -> ModifiedContent { + modifier(_ContentModeModifier(mode: .fill)) + } + + func aspectRatio(contentMode: ContentMode) -> ModifiedContent { + modifier(_ContentModeModifier(mode: contentMode)) + } +} From 61684976b44d194c123996de1f892afe9deb28fc Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 20:24:28 -0400 Subject: [PATCH 3/9] Fetch image bytes with a real user agent --- .../com/pureswift/swiftui/ImageFetch.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/ImageFetch.kt diff --git a/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/ImageFetch.kt b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/ImageFetch.kt new file mode 100644 index 0000000..1ec903a --- /dev/null +++ b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/ImageFetch.kt @@ -0,0 +1,39 @@ +package com.pureswift.swiftui + +import java.net.HttpURLConnection +import java.net.URL + +/// Bytes for a remote image, shared by both JVM targets. +/// +/// Deliberately not `URL.openStream()`: that sends the default `Java/` +/// user agent, which a number of CDNs reject outright (Wikimedia answers 403), +/// and it neither follows cross-protocol redirects nor surfaces a non-200 as a +/// failure — a redirect or error page would otherwise be decoded as garbage. +internal fun fetchImageBytes(url: String): ByteArray { + var remaining = 5 + var current = URL(url) + while (true) { + val connection = (current.openConnection() as HttpURLConnection).apply { + instanceFollowRedirects = false // handled below, so http↔https works + connectTimeout = 15_000 + readTimeout = 15_000 + setRequestProperty("User-Agent", USER_AGENT) + setRequestProperty("Accept", "image/*") + } + try { + val code = connection.responseCode + if (code in 300..399 && remaining-- > 0) { + val location = connection.getHeaderField("Location") + ?: error("redirect with no location") + current = URL(current, location) + continue + } + if (code != HttpURLConnection.HTTP_OK) error("HTTP $code for $current") + return connection.inputStream.use { it.readBytes() } + } finally { + connection.disconnect() + } + } +} + +private const val USER_AGENT = "AndroidSwiftUI/1.0" From 17f3b68b0b6a108fb2c01d274fc6d57a7a1e48c9 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 20:24:28 -0400 Subject: [PATCH 4/9] Add platform image loading seams --- .../swiftui/PlatformImage.android.kt | 32 +++++++++++++++++++ .../com/pureswift/swiftui/PlatformImage.kt | 16 ++++++++++ .../swiftui/PlatformImage.desktop.kt | 20 ++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 Demo/swiftui/src/androidMain/kotlin/com/pureswift/swiftui/PlatformImage.android.kt create mode 100644 Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/PlatformImage.kt create mode 100644 Demo/swiftui/src/desktopMain/kotlin/com/pureswift/swiftui/PlatformImage.desktop.kt diff --git a/Demo/swiftui/src/androidMain/kotlin/com/pureswift/swiftui/PlatformImage.android.kt b/Demo/swiftui/src/androidMain/kotlin/com/pureswift/swiftui/PlatformImage.android.kt new file mode 100644 index 0000000..b89f10d --- /dev/null +++ b/Demo/swiftui/src/androidMain/kotlin/com/pureswift/swiftui/PlatformImage.android.kt @@ -0,0 +1,32 @@ +package com.pureswift.swiftui + +import android.graphics.BitmapFactory +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.net.URL + +// Asset images come from the app's drawable resources, looked up by name — +// the Android analog of an asset-catalog image. +@Composable +internal actual fun rememberAssetPainter(name: String): Painter? { + val context = LocalContext.current + val id = remember(name) { + context.resources.getIdentifier(name, "drawable", context.packageName) + } + return if (id != 0) painterResource(id) else null +} + +internal actual suspend fun loadRemoteImage(url: String): ImageBitmap? = + withContext(Dispatchers.IO) { + runCatching { + val bytes = fetchImageBytes(url) + BitmapFactory.decodeByteArray(bytes, 0, bytes.size)?.asImageBitmap() + }.getOrNull() + } diff --git a/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/PlatformImage.kt b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/PlatformImage.kt new file mode 100644 index 0000000..c2daded --- /dev/null +++ b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/PlatformImage.kt @@ -0,0 +1,16 @@ +package com.pureswift.swiftui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.Painter + +// Platform seams for image loading, mirroring the VideoPlayer pattern: the +// common interpreter stays free of platform APIs, and each target supplies +// its lookup (Android app resources) and its decoder. + +/// A bundled image by name, or null when the platform can't resolve it. +@Composable +internal expect fun rememberAssetPainter(name: String): Painter? + +/// Fetches and decodes a remote image off the main thread; null on failure. +internal expect suspend fun loadRemoteImage(url: String): ImageBitmap? diff --git a/Demo/swiftui/src/desktopMain/kotlin/com/pureswift/swiftui/PlatformImage.desktop.kt b/Demo/swiftui/src/desktopMain/kotlin/com/pureswift/swiftui/PlatformImage.desktop.kt new file mode 100644 index 0000000..bf0e71d --- /dev/null +++ b/Demo/swiftui/src/desktopMain/kotlin/com/pureswift/swiftui/PlatformImage.desktop.kt @@ -0,0 +1,20 @@ +package com.pureswift.swiftui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.toComposeImageBitmap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +// The desktop rig has no app resource bundle, so named assets don't resolve +// there (a documented desktop limit); the placeholder path renders instead. +@Composable +internal actual fun rememberAssetPainter(name: String): Painter? = null + +internal actual suspend fun loadRemoteImage(url: String): ImageBitmap? = + withContext(Dispatchers.IO) { + runCatching { + org.jetbrains.skia.Image.makeFromEncoded(fetchImageBytes(url)).toComposeImageBitmap() + }.getOrNull() + } From b07b4ddccdc411c3ea61779d059f8e827de920c2 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 20:24:28 -0400 Subject: [PATCH 5/9] Render asset and remote images --- .../kotlin/com/pureswift/swiftui/Render.kt | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt index d377533..3a64104 100644 --- a/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt +++ b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt @@ -138,6 +138,8 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged @@ -337,6 +339,8 @@ private fun RenderResolved(node: ViewNode) { "Image" -> RenderImage(node) + "AsyncImage" -> RenderAsyncImage(node) + "Overlay" -> Box( contentAlignment = zStackAlignment(node), modifier = node.composeModifiers(), @@ -951,11 +955,61 @@ private fun RenderImage(node: ViewNode) { tint = tint ?: LocalContentColor.current, modifier = node.composeModifiers(), ) + return + } + val asset = node.string("name")?.let { rememberAssetPainter(it) } + if (asset != null) { + androidx.compose.foundation.Image( + painter = asset, + contentDescription = node.string("name"), + contentScale = node.imageContentScale(), + modifier = node.composeModifiers(), + ) } else { Text("[${node.string("name") ?: "image"}]", modifier = node.composeModifiers()) } } +// resizable() unlocks scaling; the contentMode modifier then picks fit or fill. +private fun ViewNode.imageContentScale(): ContentScale { + if (string("resizable") != "true") return ContentScale.Inside + return when (modifiers.firstOrNull { it.kind == "contentMode" }?.args?.string("mode")) { + "fit" -> ContentScale.Fit + "fill" -> ContentScale.Crop + else -> ContentScale.FillBounds // bare resizable stretches, as in SwiftUI + } +} + +// Fetch → decode → swap in, entirely Compose-side: a spinner while loading and +// a labeled placeholder on failure. Keyed by URL so a new URL reloads. +@Composable +private fun RenderAsyncImage(node: ViewNode) { + val url = node.string("url") + if (url == null) { + Text("[image]", modifier = node.composeModifiers()) + return + } + var bitmap by remember(url) { mutableStateOf(null) } + var failed by remember(url) { mutableStateOf(false) } + LaunchedEffect(url) { + val loaded = loadRemoteImage(url) + if (loaded != null) bitmap = loaded else failed = true + } + val image = bitmap + when { + image != null -> androidx.compose.foundation.Image( + bitmap = image, + contentDescription = url, + contentScale = node.imageContentScale(), + modifier = node.composeModifiers(), + ) + failed -> Text("[failed: $url]", modifier = node.composeModifiers()) + else -> Box(contentAlignment = Alignment.Center, modifier = node.composeModifiers()) { + CircularProgressIndicator() + } + } +} + private fun ViewNode.colorList(key: String): List { val arr = props[key] as? kotlinx.serialization.json.JsonArray ?: return emptyList() return arr.mapNotNull { (it as? kotlinx.serialization.json.JsonPrimitive)?.content?.toLongOrNull()?.let { c -> Color(c.toInt()) } } @@ -1231,7 +1285,7 @@ private val KNOWN_MODIFIER_KINDS = setOf( "scale", "opacity", "border", "shadow", "clipShape", "onTapGesture", "disabled", "font", "fontWeight", "italic", "foregroundColor", "lineLimit", "multilineTextAlignment", "tint", "onAppear", "onDisappear", "task", "onChange", "animation", "tag", "tabItem", - "transition", "focused", "longPress", "drag", + "transition", "focused", "longPress", "drag", "contentMode", ) // Folds a frame entry: fixed size, fill (maxWidth/Height .infinity), bounded From 6228ba88585b4099524aa70364720bc1832772c2 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 20:24:28 -0400 Subject: [PATCH 6/9] Test image naming, resizing, and AsyncImage --- .../EvaluatorTests.swift | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/EvaluatorTests.swift b/AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/EvaluatorTests.swift index cab4dbe..5615a63 100644 --- a/AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/EvaluatorTests.swift +++ b/AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/EvaluatorTests.swift @@ -249,6 +249,39 @@ struct ModifierTests { #expect(changes == 0) } + @Test("Image distinguishes a system symbol from a named asset") + func imageNaming() { + let symbol = ViewHost(Image(systemName: "star.fill")).evaluate() + #expect(symbol.props["systemName"] == .string("star.fill")) + let asset = ViewHost(Image("sample_photo")).evaluate() + #expect(asset.props["name"] == .string("sample_photo")) + #expect(asset.props["systemName"] == nil) // not a symbol lookup + #expect(asset.props["resizable"] == nil) // opt-in only + } + + @Test("resizable and a content mode travel together to the interpreter") + func imageResizable() { + let node = ViewHost(Image("sample_photo").resizable().scaledToFit()).evaluate() + #expect(node.props["resizable"] == .bool(true)) + let mode = node.modifiers.first { $0.kind == "contentMode" } + #expect(mode?.args["mode"] == .string("fit")) + // scaledToFill and aspectRatio(contentMode:) select the other mode + let filled = ViewHost(Image("x").resizable().scaledToFill()).evaluate() + #expect(filled.modifiers.first { $0.kind == "contentMode" }?.args["mode"] == .string("fill")) + let ratio = ViewHost(Image("x").aspectRatio(contentMode: .fit)).evaluate() + #expect(ratio.modifiers.first { $0.kind == "contentMode" }?.args["mode"] == .string("fit")) + } + + @Test("AsyncImage carries its URL, and a nil URL carries none") + func asyncImage() { + let node = ViewHost(AsyncImage(url: URL(string: "https://example.com/a.png"))).evaluate() + #expect(node.type == "AsyncImage") + #expect(node.props["url"] == .string("https://example.com/a.png")) + let empty = ViewHost(AsyncImage(url: nil)).evaluate() + #expect(empty.type == "AsyncImage") + #expect(empty.props["url"] == nil) + } + @Test("onAppear and onDisappear emit distinct callback kinds") func appearDisappear() { let node = ViewHost(Text("x").onAppear {}.onDisappear {}).evaluate() From 6473e104cf3c8c91c025b7181782337a237693a5 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 20:24:28 -0400 Subject: [PATCH 7/9] Add a sample drawable for the gallery --- Demo/app/src/main/res/drawable/sample_photo.png | Bin 0 -> 2377 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Demo/app/src/main/res/drawable/sample_photo.png diff --git a/Demo/app/src/main/res/drawable/sample_photo.png b/Demo/app/src/main/res/drawable/sample_photo.png new file mode 100644 index 0000000000000000000000000000000000000000..d4b9f5b568505a11c4aa1c31a759c58336fef069 GIT binary patch literal 2377 zcmd5;`#%%vAAfeSO`N%=EoA%Fk&;*>|A+7Qr|13pyq@>_d7dBM@Aq@j&6V`asC0KYgp zIUWK4B8L!wg36b-sP`F% z6TC>cb(7Z4l>g7kch+ty;K89a=SIhX|e9p zS-qpRq7>mXeNb;+JZQdG=6cU_fpAUroY|WAd`eZ^ls2^MF>aYwss+ zyse(*fb*K?WwCvGX}Oqlv-$`)yE*bOoL!GwZgKZ@p><*4&yOHbL?uXgh(h<%B~#BO zSTj2d0__^ENsOWwF$U&j?XeRonklpQ@C~L1Ofa{_*}zPJ=DJJcuAVhwjCY@J+%*cT zV1eqvnZnAR3{1+bu=nzsz@GkC3*JTJfnI_5w=!#A*F@RdW0N#}i?|biE{Umw-48WB z)xZi!>_Lv>gi5&1QbFE@g_CdkcV`gudIk4ZTe(}IFCr-EOD1>vgXc!G{Q(1c?V(&O z>wz^h7#iUaUQy&LsV9deivB!Y&J4>wh z`}38FV|`PdPniR>sJ(tjPVZ5KX(RdSCA%nD{^-t=6)^YE2=Re3#rIWx@TycwV-j-w zxue04;Y(01fWe01{| zD+|kp!5RaNuo5DDy=vhBJGo~e72)vvK0}Z|7ZnHYsMN3c4?G5gCgGe|3=|l~1Lmh1 zpF1$(E>NS#FwBXAU;mWCex0+m+DbmGRa6Bp<-YES{JvX$B#5wu!_IU;a33Hj;4e4- zr<&!0AJm^A5rxEoba@2i31=_KuMS3&P!Fc1H6oYtcTN+*mE69{r{t;IF=yp@G9_XG zoVh$+Zol4smB>UBr_v}y33pG~^pKs~t_-^V{F_4QRI86pGCM))hoHxUEjr2e0)!1( zyWD%c^^)&Yj{M~|d>aHVNaLC*!u$pn)C%>W;2ZF~0kQHByl(0u*nkf=#YlE}vl^=Q zDNW3Rf8CO9?Y+bac~ zA^`>D84iaiyGrK6gx4q(3xp+6LTuZVu5VK8{kMoVOfUJW!h#Xyv@vwoX(%!Xs`y^2 z;$0`v)@6{(AKzdN_kxcE!tsiwu~dpaB&OsF|Hr;G`#gXhFV~7$EYZo^FZr-9&2eW<&(TN8|r%C zF%IzBqbw0C`O8b(aK*Y?xvx*-wGJsYhQZmN5#Ol~*QFHSrOoc2pgvMnDp44Wg?3R# zT<+aFcH%gyiKs$NIV`%%>agEZs6{V@vt=z=T8K8Hy)$OmL6PQX6RhCT_IEVYq_O1} zwG_^JwXdcY^6AZ$DNkaD#Kg(9{C~yolune_@=iZry)7o6_G<*jxYNg=x?}j2vgGiD z^u~M-NfS Date: Thu, 23 Jul 2026 20:24:28 -0400 Subject: [PATCH 8/9] Add an image playground --- .../Sources/ImagePlaygrounds.swift | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 Demo/App.swiftpm/Sources/ImagePlaygrounds.swift diff --git a/Demo/App.swiftpm/Sources/ImagePlaygrounds.swift b/Demo/App.swiftpm/Sources/ImagePlaygrounds.swift new file mode 100644 index 0000000..cc662f6 --- /dev/null +++ b/Demo/App.swiftpm/Sources/ImagePlaygrounds.swift @@ -0,0 +1,58 @@ +#if canImport(AndroidSwiftUI) +import AndroidSwiftUI +#else +import SwiftUI +#endif +import Foundation + +struct ImagePlayground: View { + + @State private var remoteIndex = 0 + + private let remotes = [ + "https://www.gstatic.com/webp/gallery/1.jpg", + "https://picsum.photos/id/237/300/200", + ] + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + Example("Asset image") { + VStack(alignment: .leading, spacing: 8) { + Text("Image(\"sample_photo\") — natural size") + Image("sample_photo") + } + } + Example("resizable + scaledToFit") { + Image("sample_photo") + .resizable() + .scaledToFit() + .frame(width: 160, height: 90) + } + Example("resizable + scaledToFill") { + Image("sample_photo") + .resizable() + .scaledToFill() + .frame(width: 160, height: 90) + } + Example("AsyncImage") { + VStack(alignment: .leading, spacing: 8) { + AsyncImage(url: URL(string: remotes[remoteIndex])) + .frame(width: 200, height: 150) + Button("Load the other image") { + remoteIndex = (remoteIndex + 1) % remotes.count + } + } + } + Example("AsyncImage — failure") { + AsyncImage(url: URL(string: "https://example.invalid/missing.png")) + .frame(width: 200, height: 60) + } + Example("Unknown asset falls back") { + Image("no_such_asset") + } + } + } + .navigationTitle("Images") + } +} From 3bafdb584cc3a4812f4cab1c7242bcf9499b2780 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 23 Jul 2026 20:24:28 -0400 Subject: [PATCH 9/9] List the image playground in the catalog --- Demo/App.swiftpm/Sources/Catalog.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Demo/App.swiftpm/Sources/Catalog.swift b/Demo/App.swiftpm/Sources/Catalog.swift index 2c50be0..e3120f1 100644 --- a/Demo/App.swiftpm/Sources/Catalog.swift +++ b/Demo/App.swiftpm/Sources/Catalog.swift @@ -38,6 +38,7 @@ struct CatalogEntry: Identifiable { CatalogEntry(id: "frame", title: "Frames", screen: AnyCatalogScreen(FramePlayground())), CatalogEntry(id: "spacer", title: "Spacer & Divider", screen: AnyCatalogScreen(SpacerDividerPlayground())), CatalogEntry(id: "color", title: "Color", screen: AnyCatalogScreen(ColorPlayground())), + CatalogEntry(id: "image", title: "Images", screen: AnyCatalogScreen(ImagePlayground())), CatalogEntry(id: "graphics", title: "Graphics", screen: AnyCatalogScreen(GraphicsPlayground())), CatalogEntry(id: "map", title: "Map", screen: AnyCatalogScreen(MapPlayground())), CatalogEntry(id: "video", title: "Video", screen: AnyCatalogScreen(VideoPlayground())),