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
70 changes: 70 additions & 0 deletions Assets/architecture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Assets/bridge-hero.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Assets/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Assets/demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 5 additions & 4 deletions Benchmarks/RESULTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ These numbers are reference measurements, not universal performance claims.
Run the committed XCTest performance suite on your target hardware before
making capacity decisions.

## 2026-07-23 reference run
## 2026-07-28 reference run

| Environment | Value |
| --- | --- |
Expand All @@ -16,10 +16,11 @@ making capacity decisions.

| Scenario | Work per measured iteration | Mean | Relative standard deviation |
| --- | ---: | ---: | ---: |
| Immediate emissions | 10,000 publisher emissions | 0.007 s | 18.198% |
| Store lifecycle | 1,000 creation/teardown cycles | 0.004 s | 14.365% |
| Immediate emissions | 10,000 publisher emissions | 0.007 s | 28.199% |
| Store lifecycle | 1,000 creation/teardown cycles | 0.005 s | 18.604% |
| Shared static setup | 1,000 setup/teardown cycles | 0.007 s | 28.158% |

The full XCTest run completed two performance tests with zero failures.
The full XCTest run completed three performance tests with zero failures.
Variance includes local machine scheduling and should be reduced with dedicated
CI hardware before establishing regression thresholds.

Expand Down
23 changes: 23 additions & 0 deletions Examples/DailyPulse/iosApp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,28 @@ iosApp/
| Combine publisher adapter | `CallbackExamples.swift` and `BridgeCallbackPublisher.swift` |
| SKIE value reads and owned-model disposal | `SKIEStateFlowInterop.swift` and `KMPInterop.swift` |

## Example architecture

Each feature demonstrates the same production-friendly boundary:

```text
KMP ViewModel
↓ thin bridge container
Native Swift values + Binding + action closures
Pure SwiftUI presentation
```

The container owns or observes the Kotlin model and performs synchronous value
projection. The presentation view knows nothing about KMP, SKIE, flows,
collectors, or cancellation. This keeps rendering code reusable and makes
previews deterministic.

Every example includes preview states for the UI it owns, including populated,
loading, empty, callback, Combine, NativeFlow, writable binding, and dark-mode
variants. These previews use immutable Swift fixtures and do not initialize
Koin, allocate Kotlin ViewModels, start coroutines, collect flows, or perform
network requests.

There is no generated Swift source or build-tool plugin. Every ViewModel's
typed observation plan is declared locally with `@KMPObservable`.
22 changes: 21 additions & 1 deletion Examples/DailyPulse/iosApp/iosApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ struct ContentView: View {

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
NavigationView {
List {
ExampleHeader(
title: "SwiftUI Ownership",
subtitle: "StateObject, ObservedObject, and environment.",
systemImage: "rectangle.stack"
)
ExampleHeader(
title: "SKIE StateFlow",
subtitle: "Macro-checked, field-level observation.",
systemImage: "newspaper"
)
ExampleHeader(
title: "Explicit Adapters",
subtitle: "Callback and Combine cancellation.",
systemImage: "arrow.triangle.2.circlepath"
)
}
.navigationTitle("Bridge Examples")
}
.previewDisplayName("Example catalog")
}
}
154 changes: 142 additions & 12 deletions Examples/DailyPulse/iosApp/iosApp/Examples/ArticleSKIEExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,68 @@ private struct ArticleContentView: View {
_article = KMPObservedObject(viewModel)
}

var body: some View {
ArticleListContent(
isLoading: article.articleState.isLoading,
error: article.articleState.error,
articles: article.articleState.articles.map(ArticleRowModel.init)
)
}
}

private struct ArticleRowModel: Identifiable {
let title: String
let description: String
let date: String
let imageURL: URL?

var id: String {
"\(title)|\(date)"
}

init(_ article: Article) {
title = article.title
description = article.description_
date = article.date
imageURL = URL(string: article.imageUrl)
}

init(
title: String,
description: String,
date: String,
imageURL: URL?
) {
self.title = title
self.description = description
self.date = date
self.imageURL = imageURL
}
}

private struct ArticleListContent: View {
let isLoading: Bool
let error: String?
let articles: [ArticleRowModel]

var body: some View {
Group {
if article.articleState.isLoading {
ProgressView()
} else if article.articleState.articles.isEmpty {
VStack(spacing: 8) {
Image(systemName: "newspaper").font(.largeTitle)
Text("No Articles").font(.headline)
Text("There are no articles to show.")
.foregroundColor(.secondary)
}
if isLoading {
ProgressView("Loading articles…")
} else if let error {
ArticleUnavailableView(
title: "Unable to Load Articles",
message: error,
systemImage: "exclamationmark.triangle"
)
} else if articles.isEmpty {
ArticleUnavailableView(
title: "No Articles",
message: "There are no articles to show.",
systemImage: "newspaper"
)
} else {
List(article.articleState.articles, id: \.title) { article in
List(articles) { article in
ArticleRow(article: article)
}
.listStyle(.plain)
Expand All @@ -51,13 +100,49 @@ private struct ArticleContentView: View {
}
}

private struct ArticleUnavailableView: View {
let title: String
let message: String
let systemImage: String

var body: some View {
VStack(spacing: 8) {
Image(systemName: systemImage).font(.largeTitle)
Text(title).font(.headline)
Text(message)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding()
}
}

private struct ArticleRow: View {
let article: Article
let article: ArticleRowModel

var body: some View {
VStack(alignment: .leading, spacing: 8) {
AsyncImage(url: article.imageURL) { phase in
if let image = phase.image {
image
.resizable()
.scaledToFill()
} else if phase.error != nil {
Image(systemName: "photo")
.font(.largeTitle)
.foregroundColor(.secondary)
} else {
ProgressView()
}
}
.frame(maxWidth: .infinity)
.frame(height: 200)
.background(Color.secondary.opacity(0.08))
.clipShape(RoundedRectangle(cornerRadius: 16))

Text(article.title).font(.headline)
Text(article.description_)
Text(article.description)
.font(.subheadline)
.foregroundColor(.secondary)
Text(article.date)
Expand All @@ -67,3 +152,48 @@ private struct ArticleRow: View {
.padding(.vertical, 8)
}
}

struct ArticleSKIEExampleView_Previews: PreviewProvider {
private static let articles = [
ArticleRowModel(
title: "Kotlin state with native SwiftUI rendering",
description: "The preview uses immutable Swift data and never starts Koin or a Kotlin collector.",
date: "Today",
imageURL: URL(string: "https://picsum.photos/800/400")
),
ArticleRowModel(
title: "Field-level Observation",
description: "Only views reading the changed projected field are invalidated.",
date: "Yesterday",
imageURL: nil
),
]

static var previews: some View {
Group {
NavigationView {
ArticleListContent(
isLoading: false,
error: nil,
articles: articles
)
.navigationTitle("Macro SKIE")
}
.previewDisplayName("Articles")

ArticleListContent(
isLoading: true,
error: nil,
articles: []
)
.previewDisplayName("Loading")

ArticleListContent(
isLoading: false,
error: nil,
articles: []
)
.previewDisplayName("Empty")
}
}
}
51 changes: 44 additions & 7 deletions Examples/DailyPulse/iosApp/iosApp/Examples/CallbackExamples.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,54 @@ private struct AdapterContent: View {

var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text(title).font(.title2.bold())
Text(message)
HStack {
Button("Increment", action: increment)
.primaryExampleButtonStyle()
Button("Reset", action: reset)
.secondaryExampleButtonStyle()
ExampleCard {
ExampleHeader(
title: title,
subtitle: "Explicit adapter with deterministic cancellation.",
systemImage: "arrow.triangle.2.circlepath"
)

Divider().padding(.vertical, 4)

Text(message).font(.headline)

HStack {
Button("Increment", action: increment)
.primaryExampleButtonStyle()
Button("Reset", action: reset)
.secondaryExampleButtonStyle()
}
}
Spacer()
}
.padding()
.navigationTitle("\(title) Adapter")
}
}

struct CallbackExamplesView_Previews: PreviewProvider {
static var previews: some View {
Group {
NavigationView {
AdapterContent(
title: "Callback",
message: "Callback updated to 3",
increment: {},
reset: {}
)
}
.previewDisplayName("Callback adapter")

NavigationView {
AdapterContent(
title: "Combine",
message: "Publisher delivered on MainActor",
increment: {},
reset: {}
)
}
.preferredColorScheme(.dark)
.previewDisplayName("Combine adapter")
}
}
}
Loading
Loading