Skip to content
Open
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
43 changes: 37 additions & 6 deletions android/app/src/main/kotlin/kmp/android/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package kmp.android

import SampleTabBarScreen

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix unqualified import statement.

The import statement import SampleTabBarScreen lacks a package qualifier, which is non-standard and may cause compilation issues or ambiguity.

Apply this diff to add the proper package:

-import SampleTabBarScreen
+import kmp.shared.sampletabbar.presentation.ui.SampleTabBarScreen
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import SampleTabBarScreen
import kmp.shared.sampletabbar.presentation.ui.SampleTabBarScreen
🤖 Prompt for AI Agents
In android/app/src/main/kotlin/kmp/android/MainActivity.kt around line 3, the
import "import SampleTabBarScreen" is unqualified and must be replaced with the
fully qualified package path for SampleTabBarScreen; find the package
declaration in the file where SampleTabBarScreen is defined (or its module
namespace) and change the import to that full package-qualified name (for
example replace with com.your.package.path.SampleTabBarScreen), then save and
rebuild to verify the import resolves.

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.core.view.WindowCompat
import kmp.android.di.initDependencyInjection
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kmp.android.shared.core.system.BaseActivity
import kmp.android.shared.style.AppTheme
import kmp.android.ui.Root
import org.koin.core.context.GlobalContext
import kmp.shared.sampletabbar.presentation.ui.SampleTheme
import kmp.shared.sampletabbar.presentation.vm.SampleTabBarIntent
import kmp.shared.sampletabbar.presentation.vm.SampleTabBarViewModel
import kotlinx.coroutines.flow.collectLatest
import org.koin.androidx.compose.koinViewModel

class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -19,9 +25,34 @@ class MainActivity : BaseActivity() {
override fun onStart() {
super.onStart()
setContent {
AppTheme {
Root()
SampleTheme {
// Root()
SampleTabBarMainRoute()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}

@Composable
internal fun SampleTabBarMainRoute(
viewModel: SampleTabBarViewModel = koinViewModel(),
) {
val state by viewModel.state.collectAsStateWithLifecycle()

LaunchedEffect(key1 = viewModel) {
viewModel.onIntent(SampleTabBarIntent.OnAppeared)
}

LaunchedEffect(key1 = viewModel) {
viewModel.events.collectLatest { event ->
when (event) {
else -> {}
}
}
}

SampleTabBarScreen(
state = state,
onIntent = viewModel::onIntent,
)
}
1 change: 1 addition & 0 deletions build-logic/convention/src/main/kotlin/config/KmmConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ fun KotlinMultiplatformExtension.kmm(
export(project(":shared:samplesharedviewmodel"))
export(project(":shared:samplecomposemultiplatform"))
export(project(":shared:samplecomposenavigation"))
export(project(":shared:sampletabbar"))
}
it.binaries {
compilerOptions.freeCompilerArgs.add("-Xbinary=bundleId=kmp.shared.$nativeName")
Expand Down
12 changes: 10 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ material-icons = "1.7.3"
lifecycle = "2.9.3"
paging = "3.3.6"
composeBom = "2025.08.01"
jetbrains-composePlugin = "1.8.2" # Downgrade to 1.6.11 if you want to use compose multiplatform with UIKit navigation (it fixes the swipe back)
jetbrains-composePlugin = "1.9.0"
activity = "1.10.1"
navigation = "2.9.3"
accompanist = "0.36.0"
Expand All @@ -40,6 +40,8 @@ uiautomator = "2.3.0"
skie = "0.10.6"
firebase = "22.5.0"
googleServices = "4.4.3"
coil = "3.3.0"
haze = "1.6.10"

[libraries]
# Kotlin
Expand Down Expand Up @@ -93,8 +95,11 @@ activity-compose = { module = "androidx.activity:activity-compose", version.ref
# Navigation
navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
# Accompanist
accompanist-coil = { module = "com.google.accompanist:accompanist-coil", version.ref = "accompanist" }
accompanist-navigationMaterial = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanist" }
# Coil
coil = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
coil-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
# Ktor
ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
Expand Down Expand Up @@ -127,6 +132,9 @@ androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomato
skie-annotations = { module = "co.touchlab.skie:configuration-annotations", version.ref = "skie" }
# Firebase
firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx", version.ref = "firebase" }
# Haze
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }

[bundles]
settings = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ public extension Container {
// Sample
var sampleSharedViewModel: Factory<SampleSharedViewModel> { self { self.kmp().get(SampleSharedViewModel.self) } }
var sampleNextViewModel: Factory<SampleNextViewModel> { self { self.kmp().get(SampleNextViewModel.self) } }
var sampleTabBarViewModel: Factory<SampleTabBarViewModel> { self { self.kmp().get(SampleTabBarViewModel.self) } }
}
11 changes: 8 additions & 3 deletions ios/Application/MainFlowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// Copyright © 2019 Matee. All rights reserved.
//

import DependencyInjection
import Factory
import KMPShared
import Sample
import SampleComposeMultiplatform
Expand All @@ -23,9 +25,12 @@ enum MainTab: Int {
final class MainFlowController: FlowController {

override func setup() -> UIViewController {
let main = UITabBarController()
main.viewControllers = [setupSampleTab(), setupSampleSharedViewModelTab(), setupSampleComposeMultiplatformTab(), setupSampleComposeNavigationTab()]
return main
@Injected(\.sampleTabBarViewModel) var viewModel: KMPShared.SampleTabBarViewModel

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused viewModel variable.

The viewModel is injected but never used. Either pass it to SampleTabBarView or remove the injection.

Apply this diff to remove the unused variable:

-        @Injected(\.sampleTabBarViewModel) var viewModel: KMPShared.SampleTabBarViewModel
         return UIHostingController(rootView: SampleTabBarView(flowController: self))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Injected(\.sampleTabBarViewModel) var viewModel: KMPShared.SampleTabBarViewModel
return UIHostingController(rootView: SampleTabBarView(flowController: self))
🤖 Prompt for AI Agents
In ios/Application/MainFlowController.swift around line 28, the Injected
property wrapper creates a viewModel that's not used; remove the unused
`@Injected(\.sampleTabBarViewModel) var viewModel:
KMPShared.SampleTabBarViewModel` declaration or instead pass `viewModel` into
the `SampleTabBarView` initializer where it's created/used. If removing, delete
that single line; if using, update the `SampleTabBarView(...)` call to accept
and forward `viewModel` as a parameter and remove any compiler warnings.

return UIHostingController(rootView: SampleTabBarView(flowController: self))
// return UIHostingController(rootView: TestScreen())
// let main = UITabBarController()
// main.viewControllers = [setupSampleTab(), setupSampleSharedViewModelTab(), setupSampleComposeMultiplatformTab(), setupSampleComposeNavigationTab()]
// return main
Comment on lines +30 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove commented-out code.

The commented alternative implementations should be removed to maintain code cleanliness. Version control preserves the history if needed.

Apply this diff:

-//        return UIHostingController(rootView: TestScreen())
-//        let main = UITabBarController()
-//        main.viewControllers = [setupSampleTab(), setupSampleSharedViewModelTab(), setupSampleComposeMultiplatformTab(), setupSampleComposeNavigationTab()]
-//        return main
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// return UIHostingController(rootView: TestScreen())
// let main = UITabBarController()
// main.viewControllers = [setupSampleTab(), setupSampleSharedViewModelTab(), setupSampleComposeMultiplatformTab(), setupSampleComposeNavigationTab()]
// return main
🤖 Prompt for AI Agents
In ios/Application/MainFlowController.swift around lines 30 to 33, remove the
commented-out alternative implementations (the UIHostingController return and
the sample UITabBarController setup) so the file contains only the active
implementation; commit the cleaned file with those comment lines deleted to keep
codebase tidy (history retained in VCS).

}

private func setupSampleTab() -> UINavigationController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Embed and sign shared framework"
scriptText = "cd &quot;$SRCROOT/..&quot;&#10;ios/scripts/build-kmp.sh&#10;">
scriptText = "cd &quot;$SRCROOT/..&quot;&#10;./gradlew :shared:core:embedAndSignAppleFrameworkForXcode &lt; /dev/null&#10;">
Comment thread
JuliaJakubcova marked this conversation as resolved.
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// Created by Julia Jakubcova on 30/09/2025
// Copyright © 2025 Matee. All rights reserved.
//

import KMPShared
import SwiftUI

class NativeScaffoldObservable: ObservableObject, NativeScaffoldDelegate {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Seal the observable.

Prevent subclassing for a delegate/observable type; improves clarity and performance.

-class NativeScaffoldObservable: ObservableObject, NativeScaffoldDelegate {
+final class NativeScaffoldObservable: ObservableObject, NativeScaffoldDelegate {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class NativeScaffoldObservable: ObservableObject, NativeScaffoldDelegate {
final class NativeScaffoldObservable: ObservableObject, NativeScaffoldDelegate {
🤖 Prompt for AI Agents
In
ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/NativeScaffoldObservable.swift
around line 9, the class declaration allows subclassing but should be sealed for
a delegate/observable type; change the declaration to a final class (e.g.,
replace "class NativeScaffoldObservable: ObservableObject,
NativeScaffoldDelegate {" with "final class NativeScaffoldObservable:
ObservableObject, NativeScaffoldDelegate {") to prevent subclassing and enable
related compiler optimizations.


private let onSelectedTabChanged: (Int32) -> Void
let content: (Int32?) -> UIViewController

@Published var toolbar: Toolbar?
@Published var tabs: [TabItem]
@Published var selectedTab: Int32 {
didSet {
if oldValue != selectedTab {
onSelectedTabChanged(selectedTab)
}
}
}

init(
toolbar: Toolbar?,
tabs: [TabItem],
selectedTab: Int32,
onSelectedTabChanged: @escaping (Int32) -> Void,
content: @escaping (Int32?) -> UIViewController
) {
self.tabs = tabs
self.selectedTab = selectedTab
self.onSelectedTabChanged = onSelectedTabChanged
self.content = content
}
Comment on lines +24 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Missing toolbar initialization.

The toolbar property is never assigned in the initializer, which will leave it with an uninitialized value. This is a critical bug that will cause undefined behavior.

Apply this diff to fix the initialization:

     init(
         toolbar: Toolbar?,
         tabs: [TabItem],
         selectedTab: Int32,
         onSelectedTabChanged: @escaping (Int32) -> Void,
         content: @escaping (Int32?) -> UIViewController
     ) {
+        self.toolbar = toolbar
         self.tabs = tabs
         self.selectedTab = selectedTab
         self.onSelectedTabChanged = onSelectedTabChanged
         self.content = content
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
init(
toolbar: Toolbar?,
tabs: [TabItem],
selectedTab: Int32,
onSelectedTabChanged: @escaping (Int32) -> Void,
content: @escaping (Int32?) -> UIViewController
) {
self.tabs = tabs
self.selectedTab = selectedTab
self.onSelectedTabChanged = onSelectedTabChanged
self.content = content
}
init(
toolbar: Toolbar?,
tabs: [TabItem],
selectedTab: Int32,
onSelectedTabChanged: @escaping (Int32) -> Void,
content: @escaping (Int32?) -> UIViewController
) {
self.toolbar = toolbar
self.tabs = tabs
self.selectedTab = selectedTab
self.onSelectedTabChanged = onSelectedTabChanged
self.content = content
}
🤖 Prompt for AI Agents
In
ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/NativeScaffoldObservable.swift
around lines 24 to 35 the initializer never assigns the toolbar parameter to the
instance property; add an assignment self.toolbar = toolbar inside the init so
the toolbar property is properly initialized (ensure the property exists and
that you use self.toolbar = toolbar to avoid shadowing issues).


func updateToolbar(toolbar: Toolbar?) {
self.toolbar = toolbar
}

func updateTabs(tabs: [TabItem]) {
self.tabs = tabs
}

func updateSelectedTab(selectedTabPosition: Int32) {
self.selectedTab = selectedTabPosition
}
Comment on lines +41 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Defensive: clamp selectedTab when tabs change.

If tabs shrink, selection can point to a non-existent index.

 func updateTabs(tabs: [TabItem]) {
     self.tabs = tabs
+    // Ensure selection stays valid
+    if !tabs.contains(where: { $0.position == selectedTab }) {
+        selectedTab = tabs.first?.position ?? 0
+    }
 }

Confirm whether positions are guaranteed contiguous; adjust clamp accordingly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func updateTabs(tabs: [TabItem]) {
self.tabs = tabs
}
func updateSelectedTab(selectedTabPosition: Int32) {
self.selectedTab = selectedTabPosition
}
func updateTabs(tabs: [TabItem]) {
self.tabs = tabs
// Ensure selection stays valid
if !tabs.contains(where: { $0.position == selectedTab }) {
selectedTab = tabs.first?.position ?? 0
}
}
🤖 Prompt for AI Agents
In
ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/NativeScaffoldObservable.swift
around lines 41–47, updateTabs currently replaces self.tabs without adjusting
self.selectedTab, which can leave selectedTab pointing past the end when tabs
shrink; after assigning self.tabs = tabs, defensively clamp selectedTab: if
tabs.isEmpty set selectedTab to -1 (or your sentinel for “no selection”),
otherwise set selectedTab = max(0, min(selectedTab, Int32(tabs.count - 1))). If
tab positions are not guaranteed to be contiguous 0..n-1, instead compute a
valid selection by mapping/validating against the actual TabItem positions
(choose the nearest valid index or clear selection).

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// Created by Julia Jakubcova on 02/10/2025
// Copyright © 2025 Matee. All rights reserved.
//

import KMPShared
import SwiftUI
import UIToolkit

struct NativeScaffoldView: View {
@ObservedObject var observable: NativeScaffoldObservable

init(observable: NativeScaffoldObservable) {
self.observable = observable
}

var body: some View {
if let toolbar = observable.toolbar {
if #available(iOS 16.0, *) {
Group {
if !observable.tabs.isEmpty {
TabView(selection: $observable.selectedTab) {
ForEach(observable.tabs, id: \.position) { tab in
Group {
if #available(iOS 26.0, *) {
ComposeViewController { observable.content(tab.position) }
.ignoresSafeArea()
} else {
ComposeViewController { observable.content(tab.position) }
}
}
Comment on lines +25 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix availability check: iOS 26.0 will never be true

The .ignoresSafeArea() branch won’t execute on real devices. Apply it unconditionally (or gate at iOS 14+ where it’s available).

-                                    if #available(iOS 26.0, *) {
-                                        ComposeViewController { observable.content(tab.position) }
-                                            .ignoresSafeArea()
-                                    } else {
-                                        ComposeViewController { observable.content(tab.position) }
-                                    }
+                                    ComposeViewController { observable.content(tab.position) }
+                                        .ignoresSafeArea()
-                        if #available(iOS 26.0, *) {
-                            ComposeViewController { observable.content(nil) }
-                                .ignoresSafeArea()
-                        } else {
-                            ComposeViewController { observable.content(nil) }
-                        }
+                        ComposeViewController { observable.content(nil) }
+                            .ignoresSafeArea()

Also applies to: 42-47

🤖 Prompt for AI Agents
In
ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/NativeScaffoldView.swift
around lines 25-31 and 42-47, the availability check uses iOS 26.0 which is
never true so .ignoresSafeArea() never runs; remove or relax the availability
guard and call .ignoresSafeArea() unconditionally (or guard at iOS 14+ if you
prefer a minimum availability), updating both occurrences so
ComposeViewController always applies .ignoresSafeArea().

.tabItem {
if let uiImage = tab.icon.toUIImage() {
Image(uiImage: uiImage)
}
Text(tab.title)
}
.tag(tab.position)
}
}
} else {
if #available(iOS 26.0, *) {
ComposeViewController { observable.content(nil) }
.ignoresSafeArea()
} else {
ComposeViewController { observable.content(nil) }
}
}
}.toolbar(toolbar)
} else {
// Fallback on earlier versions
}
}
}
Comment on lines +17 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Render content even when toolbar is nil (blank screen otherwise).

Currently, body shows nothing if observable.toolbar == nil. Show the content regardless; apply toolbar only when present.

-    var body: some View {
-        if let toolbar = observable.toolbar {
-            if #available(iOS 16.0, *) {
-                Group {
+    var body: some View {
+        if #available(iOS 16.0, *) {
+            Group {
                 // (content unchanged; see other diff to remove iOS 26 gating)
-                }.toolbar(toolbar)
-            } else {
-                // Fallback on earlier versions
-            }
-        }
+            }
+            .modifier(ToolbarWrapper(toolbar: observable.toolbar))
+        } else {
+            // Fallback on earlier versions
+            Group {
+                if !observable.tabs.isEmpty {
+                    TabView(selection: $observable.selectedTab) {
+                        ForEach(observable.tabs, id: \.position) { tab in
+                            ComposeViewController { observable.content(tab.position) }
+                                .ignoresSafeArea()
+                                .tabItem {
+                                    if let uiImage = tab.icon.toUIImage() { Image(uiImage: uiImage) }
+                                    Text(tab.title)
+                                }
+                                .tag(tab.position)
+                        }
+                    }
+                } else {
+                    ComposeViewController { observable.content(nil) }
+                        .ignoresSafeArea()
+                }
+            }
+        }

Add a small helper to conditionally apply toolbar:

+private struct ToolbarWrapper: ViewModifier {
+    let toolbar: Toolbar?
+    @available(iOS 16.0, *)
+    func body(content: Content) -> some View {
+        if let toolbar { content.toolbar(toolbar) } else { content }
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var body: some View {
if let toolbar = observable.toolbar {
if #available(iOS 16.0, *) {
Group {
if !observable.tabs.isEmpty {
TabView(selection: $observable.selectedTab) {
ForEach(observable.tabs, id: \.position) { tab in
Group {
if #available(iOS 26.0, *) {
ComposeViewController { observable.content(tab.position) }
.ignoresSafeArea()
} else {
ComposeViewController { observable.content(tab.position) }
}
}
.tabItem {
if let uiImage = tab.icon.toUIImage() {
Image(uiImage: uiImage)
}
Text(tab.title)
}
.tag(tab.position)
}
}
} else {
if #available(iOS 26.0, *) {
ComposeViewController { observable.content(nil) }
.ignoresSafeArea()
} else {
ComposeViewController { observable.content(nil) }
}
}
}.toolbar(toolbar)
} else {
// Fallback on earlier versions
}
}
}
var body: some View {
if #available(iOS 16.0, *) {
Group {
if !observable.tabs.isEmpty {
TabView(selection: $observable.selectedTab) {
ForEach(observable.tabs, id: \.position) { tab in
Group {
if #available(iOS 26.0, *) {
ComposeViewController { observable.content(tab.position) }
.ignoresSafeArea()
} else {
ComposeViewController { observable.content(tab.position) }
}
}
.tabItem {
if let uiImage = tab.icon.toUIImage() {
Image(uiImage: uiImage)
}
Text(tab.title)
}
.tag(tab.position)
}
}
} else {
if #available(iOS 26.0, *) {
ComposeViewController { observable.content(nil) }
.ignoresSafeArea()
} else {
ComposeViewController { observable.content(nil) }
}
}
}
.modifier(ToolbarWrapper(toolbar: observable.toolbar))
} else {
// Fallback on earlier versions
Group {
if !observable.tabs.isEmpty {
TabView(selection: $observable.selectedTab) {
ForEach(observable.tabs, id: \.position) { tab in
ComposeViewController { observable.content(tab.position) }
.ignoresSafeArea()
.tabItem {
if let uiImage = tab.icon.toUIImage() {
Image(uiImage: uiImage)
}
Text(tab.title)
}
.tag(tab.position)
}
}
} else {
ComposeViewController { observable.content(nil) }
.ignoresSafeArea()
}
}
}
}
private struct ToolbarWrapper: ViewModifier {
let toolbar: Toolbar?
@available(iOS 16.0, *)
func body(content: Content) -> some View {
if let toolbar {
content.toolbar(toolbar)
} else {
content
}
}
}

}

extension View {
@available(iOS 16.0, *)
func toolbar(_ toolbar: Toolbar) -> some View {
NavigationStack {
self
.navigationTitle(toolbar.title ?? "")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
.toolbar {
let leading = toolbar.buttons.filter { $0.position == .leading }
let trailing = toolbar.buttons.filter { $0.position == .trailing }

if !leading.isEmpty {
ToolbarItemGroup(placement: .topBarLeading) {
ForEach(Array(leading.enumerated()), id: \.offset) { _, button in
toolbarButtonView(for: button)
}
}
}

if !trailing.isEmpty {
ToolbarItemGroup(placement: .topBarTrailing) {
ForEach(Array(trailing.enumerated()), id: \.offset) { _, button in
toolbarButtonView(for: button)
}
}
}
}
}
}

@ViewBuilder
private func toolbarButtonView(for button: ToolbarButtonData) -> some View {
Button {
button.onClick()
} label: {
if let uiImage = button.icon.toUIImage() {
Image(uiImage: uiImage)
.renderingMode(.template)
}
}
.tint(button.tint.map { Color(kmpColor: $0) } ?? .primary)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// Created by Julia Jakubcova on 02/10/2025
// Copyright © 2025 Matee. All rights reserved.
//

import KMPShared
import SwiftUI
import UIToolkit

struct ScreenWithBottomBarView: View {
@ObservedObject var observable: ScreenWithPlatformSpecificBottomBarObservable

init(observable: ScreenWithPlatformSpecificBottomBarObservable) {
self.observable = observable
}

var body: some View {
if #available(iOS 16.0, *) {
NavigationStack {
TabView(selection: $observable.selectedTab) {
ForEach(observable.tabs, id: \.position) { tab in
Group {
if #available(iOS 26.0, *) {
ComposeViewController { tab.content }
.ignoresSafeArea()
} else {
ComposeViewController { tab.content }
}

// ScrollView {
// ForEach((0...20), id: \.self) { _ in
// Text("doiaw jmo dcposekoif eswop[ijj fe ss")
// Color.red
// .frame(height: 100)
// }
// }
Comment on lines +30 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove commented-out code.

The commented ScrollView block should be removed to maintain code cleanliness. If this code is needed for reference, consider moving it to documentation or version control history.

Apply this diff to remove the commented code:

-                            //                            ScrollView {
-                            //                                ForEach((0...20), id: \.self) { _ in
-                            //                                    Text("doiaw jmo dcposekoif eswop[ijj fe ss")
-                            //                                    Color.red
-                            //                                        .frame(height: 100)
-                            //                                }
-                            //                            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ScrollView {
// ForEach((0...20), id: \.self) { _ in
// Text("doiaw jmo dcposekoif eswop[ijj fe ss")
// Color.red
// .frame(height: 100)
// }
// }
🤖 Prompt for AI Agents
In
ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift
around lines 30 to 36, remove the commented-out ScrollView block (the
ForEach/Text/Color snippet) to clean up dead code; simply delete those commented
lines from the file so only active code remains, relying on VCS history if the
snippet is needed later.

}
.tabItem {
if let uiImage = tab.icon.toUIImage() {
Image(uiImage: uiImage)
}
Text(tab.title)
}
.tag(tab.position)

}
}
.navigationTitle("Items")
// .toolbarTitleDisplayMode(.inline)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Cancel", systemImage: "xmark") {
// cancel action
}
.tint(.red)
}

ToolbarItem(placement: .topBarTrailing) {
Button("Done", systemImage: "checkmark") {
// done action
}
.badge(3)
}
}
Comment on lines +48 to +66

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Implement button actions and avoid hardcoded values.

The toolbar contains several issues:

  • Lines 54-56 and 61-63: Button actions are empty placeholders
  • Line 48: Navigation title "Items" is hardcoded
  • Line 64: Badge value 3 is hardcoded

Consider:

  1. Wire button actions to observable callbacks or remove the buttons if not needed
  2. Make the navigation title configurable via the observable
  3. Make the badge value dynamic or remove it if it's test code

Example refactor for making the title configurable:

-                .navigationTitle("Items")
+                .navigationTitle(observable.title)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.navigationTitle("Items")
// .toolbarTitleDisplayMode(.inline)
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Cancel", systemImage: "xmark") {
// cancel action
}
.tint(.red)
}
ToolbarItem(placement: .topBarTrailing) {
Button("Done", systemImage: "checkmark") {
// done action
}
.badge(3)
}
}
// := Lines 48–66 in ScreenWithBottomBarView.swift
.navigationTitle(observable.title)
// .toolbarTitleDisplayMode(.inline)
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.hidden, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Cancel", systemImage: "xmark") {
// cancel action
}
.tint(.red)
}
ToolbarItem(placement: .topBarTrailing) {
Button("Done", systemImage: "checkmark") {
// done action
}
.badge(3)
}
}
🤖 Prompt for AI Agents
In
ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift
around lines 48 to 66, replace hardcoded UI values and empty button actions by
wiring them to the view model / observable: read the navigation title from the
observable (instead of the literal "Items"), read the badge count from an
observable Int (or hide the badge when nil/zero) instead of the hardcoded 3, and
call observable methods (e.g., cancelTapped() and doneTapped()) from the Cancel
and Done Button actions (or remove the buttons if the observable has no
handlers); ensure UI updates occur on the main thread and handle
optional/invalid values safely.

}
} else {
// Fallback on earlier versions
}
Comment on lines +68 to +70

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Implement fallback for earlier iOS versions.

The fallback block for iOS versions below 16.0 is empty, which means users on those versions will see nothing. Either implement a fallback UI or explicitly document the minimum iOS version requirement.

Do you want me to suggest a fallback implementation or help document the minimum version requirement?

🤖 Prompt for AI Agents
In
ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift
around lines 68-70, the else branch for the iOS <16.0 fallback is empty;
implement a visible fallback UI or declare the minimum supported iOS version.
Fix by either providing a compatible view path (e.g., use older-view APIs,
simple VStack/HStack replacement, or a basic static view that matches
functionality) in that else block, or update project/Info.plist and package
manifest to document/require iOS 16.0+ and remove the empty else. Ensure the
chosen approach maintains feature parity or clearly degrades gracefully for
older OS versions.

}
}
Loading
Loading