From 0b10ba06515089226fb997dce1acc91e9e06d3b5 Mon Sep 17 00:00:00 2001 From: JuliaJakubcova Date: Fri, 3 Oct 2025 08:45:35 +0200 Subject: [PATCH 1/9] [WIP] Working solution, before cleaning --- .../xcschemes/MateeStarter_Alpha.xcscheme | 2 +- .../PlatformSpecificView/BottomBarView.swift | 151 ++++++++ .../PlatformSpecificBottomBarObservable.swift | 49 +++ .../ScreenWithBottomBarView.swift | 71 ++++ ...hPlatformSpecificBottomBarObservable.swift | 43 +++ ...iftUIComposeMultiplatformViewFactory.swift | 36 ++ .../SampleComposeMultiplatformView.swift | 1 + .../ui/PlatformSpecificBottomBar.android.kt | 56 +++ .../ui/PlatformSpecificBottomBar.kt | 22 ++ .../ui/SampleComposeMultiplatformScreen.kt | 141 ++++++-- ...omposeMultiplatformScreenViewController.kt | 9 +- ...seSampleComposeMultiplatformViewFactory.kt | 14 + .../ui/PlatformSpecificBottomBar.ios.kt | 338 ++++++++++++++++++ .../ui/PlatformSpecificBottomBarDelegate.kt | 8 + ...enWithPlatformSpecificBottomBarDelegate.kt | 9 + 15 files changed, 924 insertions(+), 26 deletions(-) create mode 100644 ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/BottomBarView.swift create mode 100644 ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/PlatformSpecificBottomBarObservable.swift create mode 100644 ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift create mode 100644 ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithPlatformSpecificBottomBarObservable.swift create mode 100644 shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.android.kt create mode 100644 shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.kt create mode 100644 shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.ios.kt create mode 100644 shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBarDelegate.kt create mode 100644 shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBarDelegate.kt diff --git a/ios/MateeStarter.xcodeproj/xcshareddata/xcschemes/MateeStarter_Alpha.xcscheme b/ios/MateeStarter.xcodeproj/xcshareddata/xcschemes/MateeStarter_Alpha.xcscheme index 29d6bef9..72b78793 100644 --- a/ios/MateeStarter.xcodeproj/xcshareddata/xcschemes/MateeStarter_Alpha.xcscheme +++ b/ios/MateeStarter.xcodeproj/xcshareddata/xcschemes/MateeStarter_Alpha.xcscheme @@ -26,7 +26,7 @@ ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction"> + scriptText = "cd "$SRCROOT/.." ./gradlew :shared:core:embedAndSignAppleFrameworkForXcode < /dev/null "> : UIHostingController { + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .clear // make SwiftUI root clear + } +} + +struct BottomOnlyTabBarView: View { + + @ObservedObject var observable: PlatformSpecificBottomBarObservable + + var body: some View { + UIKitTabBarWrapper( + items: observable.items, + selected: observable.selected, + onSelectedChanged: { item in + observable.onSelectedChanged(item) + }, + onSizeChanged: { width, height in + observable.onSizeChanged(width, height) + } + ) + .frame(height: 50) // fixed height for bottom bar + } +} + +struct UIKitTabBarWrapper: UIViewRepresentable { + + let items: [String] + let selected: String + let onSelectedChanged: (String) -> Void + let onSizeChanged: (CGFloat, CGFloat) -> Void + + func makeUIView(context: Context) -> UITabBar { + let tabBar = UITabBar() + tabBar.delegate = context.coordinator + + // Create UITabBarItem for each string + tabBar.items = items.enumerated().map { index, title in + UITabBarItem(title: title, image: nil, tag: index) + } + + // Set selected item + if let selectedIndex = items.firstIndex(of: selected) { + tabBar.selectedItem = tabBar.items?[selectedIndex] + } + + // Report initial size + DispatchQueue.main.async { + onSizeChanged(tabBar.bounds.width * UIScreen.main.scale, + tabBar.bounds.height * UIScreen.main.scale) + } + + return tabBar + } + + func updateUIView(_ uiView: UITabBar, context: Context) { + // Update selected tab if changed + if let selectedIndex = items.firstIndex(of: selected) { + uiView.selectedItem = uiView.items?[selectedIndex] + } + + // Update size whenever layout changes + DispatchQueue.main.async { + onSizeChanged(uiView.bounds.width * UIScreen.main.scale, + uiView.bounds.height * UIScreen.main.scale) + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, UITabBarDelegate { + let parent: UIKitTabBarWrapper + + init(_ parent: UIKitTabBarWrapper) { + self.parent = parent + } + + func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { + let index = item.tag + guard index < parent.items.count else { return } + parent.onSelectedChanged(parent.items[index]) + } + } +} diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/PlatformSpecificBottomBarObservable.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/PlatformSpecificBottomBarObservable.swift new file mode 100644 index 00000000..0889d182 --- /dev/null +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/PlatformSpecificBottomBarObservable.swift @@ -0,0 +1,49 @@ +// +// Created by Julia Jakubcova on 30/09/2025 +// Copyright © 2025 Matee. All rights reserved. +// + +import KMPShared +import SwiftUI + +class PlatformSpecificBottomBarObservable: ObservableObject, PlatformSpecificBottomBarDelegate { + + @Published var items: [String] + @Published var selected: String + @Published var onSelectedChanged: (String) -> Void + @Published var onSizeChanged: (CGFloat, CGFloat) -> Void + + init( + items: [String], + selected: String, + onSelectedChanged: @escaping (String) -> Void, + onSizeChanged: @escaping (KotlinFloat, KotlinFloat) -> Void + ) { + self.items = items + self.selected = selected + self.onSelectedChanged = onSelectedChanged + self.onSizeChanged = { width, height in + let scale = UIScreen.main.scale + onSizeChanged(KotlinFloat(value: Float(width * scale)), KotlinFloat(value: Float(height * scale))) + } + } + + func updateItems(items: [String]) { + self.items = items + } + + func updateOnSelectedChanged(onSelectedChanged: @escaping (String) -> Void) { + self.onSelectedChanged = onSelectedChanged + } + + func updateSelected(selected: String) { + self.selected = selected + } + + func updateOnSizeChanged(onSizeChanged: @escaping (KotlinFloat, KotlinFloat) -> Void) { + self.onSizeChanged = { width, height in + let scale = UIScreen.main.scale + onSizeChanged(KotlinFloat(value: Float(width * scale)), KotlinFloat(value: Float(height * scale))) + } + } +} diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift new file mode 100644 index 00000000..43d1e862 --- /dev/null +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift @@ -0,0 +1,71 @@ +// +// 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 + + let appearance = UITabBarAppearance() + appearance.configureWithTransparentBackground() + appearance.backgroundEffect = UIBlurEffect(style: .systemMaterial) // liquid glass + appearance.backgroundColor = .clear + appearance.shadowColor = nil + UITabBar.appearance().standardAppearance = appearance + if #available(iOS 15.0, *) { + UITabBar.appearance().scrollEdgeAppearance = appearance + } + } + + var body: some View { + TabView(selection: $observable.selectedTab) { + ForEach(Array(observable.tabs.keys), id: \.self) { key in + let parts = key.components(separatedBy: "::") + let title = parts[0] + let icon = parts[1] + + ComposeViewController { + observable.tabs[key]! + } + .ignoresSafeArea() // draw behind tab bar + .background(Color.clear) + .onAppear { + // Disable scrolling only inside Compose content, not TabView itself + if let vc = observable.tabs[key] { + disableScrollsInCompose(vc.view) + } + } + .tabItem { + Label(title, systemImage: icon) + } + .tag(key) + } + } + .background(Color.clear) + } + + private func disableScrollsInCompose(_ view: UIView?) { + guard let view = view else { return } + + func recursiveDisable(_ v: UIView) { + for subview in v.subviews { + if let scroll = subview as? UIScrollView { + scroll.bounces = false + scroll.alwaysBounceVertical = false + scroll.isScrollEnabled = false + } else { + recursiveDisable(subview) + } + } + } + + recursiveDisable(view) + } +} diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithPlatformSpecificBottomBarObservable.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithPlatformSpecificBottomBarObservable.swift new file mode 100644 index 00000000..b83d3dd2 --- /dev/null +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithPlatformSpecificBottomBarObservable.swift @@ -0,0 +1,43 @@ +// +// Created by Julia Jakubcova on 30/09/2025 +// Copyright © 2025 Matee. All rights reserved. +// + +import KMPShared +import SwiftUI + +class ScreenWithPlatformSpecificBottomBarObservable: NSObject, ObservableObject, ScreenWithPlatformSpecificBottomBarDelegate, UITabBarDelegate { + + @Published var tabs: [String: UIViewController] + @Published var selectedTab: String + @Published var onSelectedTabChanged: (String) -> Void + + init( + tabs: [String: UIViewController], + selectedTab: String, + onSelectedTabChanged: @escaping (String) -> Void + ) { + self.tabs = tabs + self.selectedTab = selectedTab + self.onSelectedTabChanged = onSelectedTabChanged + } + + func updateTabs(tabs: [String: UIViewController]) { + self.tabs = tabs + } + + func updateOnSelectedTabChanged(onSelectedTabChanged: @escaping (String) -> Void) { + self.onSelectedTabChanged = onSelectedTabChanged + } + + func updateSelectedTab(selectedTab: String) { + self.selectedTab = selectedTab + } + + func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { + let index = item.tag + let key = Array(tabs.keys)[index] + selectedTab = key + onSelectedTabChanged(key) + } +} diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/SwiftUIComposeMultiplatformViewFactory.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/SwiftUIComposeMultiplatformViewFactory.swift index 4d39128f..eb123fdf 100644 --- a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/SwiftUIComposeMultiplatformViewFactory.swift +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/SwiftUIComposeMultiplatformViewFactory.swift @@ -20,4 +20,40 @@ public class SwiftUISampleComposeMultiplatformViewFactory: ComposeSampleComposeM return KotlinPair(first: viewController, second: observable) } + + public func createPlatformSpecificBottomBar( + items: [String], + selected: String, + onSelectedChanged: @escaping (String) -> Void, + onSizeChanged: @escaping (KotlinFloat, KotlinFloat) -> Void + ) -> KotlinPair { + let observable = PlatformSpecificBottomBarObservable( + items: items, + selected: selected, + onSelectedChanged: onSelectedChanged, + onSizeChanged: onSizeChanged + ) + let viewController: UIViewController = TransparentHostingController( + rootView: BottomBarView(observable: observable) + ) + + return KotlinPair(first: viewController, second: observable) + } + + public func createScreenWithPlatformSpecificBottomBar( + tabs: [String: UIViewController], + selectedTab: String, + onSelectedTabChanged: @escaping (String) -> Void + ) -> KotlinPair { + let observable = ScreenWithPlatformSpecificBottomBarObservable( + tabs: tabs, + selectedTab: selectedTab, + onSelectedTabChanged: onSelectedTabChanged + ) + let viewController: UIViewController = UIHostingController( + rootView: ScreenWithBottomBarView(observable: observable) + ) + + return KotlinPair(first: viewController, second: observable) + } } diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/SampleComposeMultiplatformView.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/SampleComposeMultiplatformView.swift index 191128b9..22165f44 100644 --- a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/SampleComposeMultiplatformView.swift +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/SampleComposeMultiplatformView.swift @@ -34,6 +34,7 @@ struct SampleComposeMultiplatformView: View { ) } .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea() .toastView($toastData) .navigationTitle(MR.strings().bottom_bar_item_3.toLocalized()) } diff --git a/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.android.kt b/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.android.kt new file mode 100644 index 00000000..f1cfe761 --- /dev/null +++ b/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.android.kt @@ -0,0 +1,56 @@ +package kmp.shared.samplecomposemultiplatform.presentation.ui + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material.BottomNavigation +import androidx.compose.material.BottomNavigationItem +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp + +@Composable +actual fun PlatformSpecificBottomBar( + items: List, + selected: String, + onSelectedChanged: (String) -> Unit, + onSizeChanged: (DpSize) -> Unit, + modifier: Modifier, +) { + val density = LocalDensity.current + BottomNavigation( + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { + with(density) { onSizeChanged(DpSize(it.size.width.toDp(), it.size.height.toDp())) } + }, + ) { + items.forEach { item -> + BottomNavigationItem( + selected = selected == item, + icon = { + Icon(imageVector = Icons.Default.Check, contentDescription = item) + }, + onClick = { onSelectedChanged(item) }, + label = { + Text(item) + } + ) + } + } +} + +@Composable +actual fun ScreenWithPlatformSpecificBottomBar( + tabs: Map Unit)>, + selectedTab: String, + onSelectedTabChanged: (String) -> Unit, + modifier: Modifier, +) { +} \ No newline at end of file diff --git a/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.kt b/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.kt new file mode 100644 index 00000000..4c0a1ee3 --- /dev/null +++ b/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.kt @@ -0,0 +1,22 @@ +package kmp.shared.samplecomposemultiplatform.presentation.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpSize + +@Composable +expect fun PlatformSpecificBottomBar( + items: List, + selected: String, + onSelectedChanged: (String) -> Unit, + onSizeChanged: (DpSize) -> Unit, + modifier: Modifier = Modifier, +) + +@Composable +expect fun ScreenWithPlatformSpecificBottomBar( + tabs: Map Unit>, + selectedTab: String, + onSelectedTabChanged: (String) -> Unit, + modifier: Modifier = Modifier, +) diff --git a/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/SampleComposeMultiplatformScreen.kt b/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/SampleComposeMultiplatformScreen.kt index 279572b8..dc4a2bff 100644 --- a/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/SampleComposeMultiplatformScreen.kt +++ b/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/SampleComposeMultiplatformScreen.kt @@ -8,9 +8,13 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeContentPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -38,33 +42,122 @@ fun SampleComposeMultiplatformScreen( if (loading) { CircularProgressIndicator() } else { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.padding(16.dp), - ) { - Text( - text = "This is a sample with compose multiplatform UI and shared VM", - textAlign = TextAlign.Center, - ) +// Box(contentAlignment = Alignment.BottomCenter) { +// Column( +// horizontalAlignment = Alignment.CenterHorizontally, +// verticalArrangement = Arrangement.spacedBy(16.dp), +// modifier = Modifier +// .verticalScroll(rememberScrollState()) +// .safeContentPadding() +// .padding(16.dp), +// ) { +// repeat(10) { +// Text( +// text = "This is a sample with compose multiplatform UI and shared VM", +// textAlign = TextAlign.Center, +// ) +// +// Text( +// text = state.sampleText?.value ?: "", +// modifier = Modifier.testTag(TestTags.SampleComposeMultiplatformScreen.SampleText), +// ) +// +// var isChecked by remember { mutableStateOf(false) } +// PlatformSpecificCheckboxView( +// text = "This is a view implemented in Compose on Android and SwiftUI on iOS", +// checked = isChecked, +// onCheckedChanged = { isChecked = it }, +// modifier = Modifier.fillMaxWidth().height(60.dp), +// ) +// +// StarterButton(onClick = { onIntent(SampleSharedIntent.OnNextButtonTapped) }) { +// Text(text = "Go to next screen") +// } +// } +// } +// + var selected by remember { mutableStateOf("one") } +// val tabs = listOf("one", "two", "three") +// var size by remember { mutableStateOf(DpSize(10.dp, 10.dp)) } +// PlatformSpecificBottomBar( +// items = tabs, +// selected = selected, +// onSelectedChanged = { selected = it }, +// onSizeChanged = { size = it }, +// modifier = Modifier +// .align(Alignment.BottomCenter) +// .safeContentPadding() +// .size(size), +// ) +// } - Text( - text = state.sampleText?.value ?: "", - modifier = Modifier.testTag(TestTags.SampleComposeMultiplatformScreen.SampleText), - ) - var isChecked by remember { mutableStateOf(false) } - PlatformSpecificCheckboxView( - text = "This is a view implemented in Compose on Android and SwiftUI on iOS", - checked = isChecked, - onCheckedChanged = { isChecked = it }, - modifier = Modifier.fillMaxWidth().height(60.dp), - ) + val factory = LocalSampleComposeMultiplatformViewFactory.current + ScreenWithPlatformSpecificBottomBar( + tabs = mapOf( + "Home::house.fill" to { + CompositionLocalProvider( + LocalSampleComposeMultiplatformViewFactory provides factory, + ) { + Content(state, onIntent) + } + }, + "Search::magnifyingglass" to { + CompositionLocalProvider( + LocalSampleComposeMultiplatformViewFactory provides factory, + ) { + Content(state, onIntent) + } + }, + "Profile::person.crop.circle" to { + CompositionLocalProvider( + LocalSampleComposeMultiplatformViewFactory provides factory, + ) { + Content(state, onIntent) + } + }, + ), + selectedTab = selected, + onSelectedTabChanged = { selected = it }, + modifier = Modifier.fillMaxSize(), + ) + } + } + } +} + +@Composable +private fun Content( + state: SampleSharedState, + onIntent: (SampleSharedIntent) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier.verticalScroll(rememberScrollState()).padding(16.dp).safeContentPadding(), + ) { + repeat(10) { + Text( + text = "This is a sample with compose multiplatform UI and shared VM", + textAlign = TextAlign.Center, + ) + + Text( + text = state.sampleText?.value ?: "", + modifier = Modifier.testTag(TestTags.SampleComposeMultiplatformScreen.SampleText), + ) + + var isChecked by remember { mutableStateOf(false) } + PlatformSpecificCheckboxView( + text = "This is a view implemented in Compose on Android and SwiftUI on iOS", + checked = isChecked, + onCheckedChanged = { isChecked = it }, + modifier = Modifier.fillMaxWidth().height(60.dp), + ) - StarterButton(onClick = { onIntent(SampleSharedIntent.OnNextButtonTapped) }) { - Text(text = "Go to next screen") - } - } + StarterButton(onClick = { onIntent(SampleSharedIntent.OnNextButtonTapped) }) { + Text(text = "Go to next screen") } } } diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/SampleComposeMultiplatformScreenViewController.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/SampleComposeMultiplatformScreenViewController.kt index 3ad41758..f4b0d513 100644 --- a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/SampleComposeMultiplatformScreenViewController.kt +++ b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/SampleComposeMultiplatformScreenViewController.kt @@ -2,8 +2,10 @@ package kmp.shared.samplecomposemultiplatform.presentation import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ExperimentalComposeApi import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.window.ComposeUIViewController import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -18,12 +20,17 @@ import kotlinx.coroutines.flow.collectLatest import org.koin.compose.viewmodel.koinViewModel import platform.UIKit.UIViewController +@OptIn(ExperimentalComposeApi::class, ExperimentalComposeUiApi::class) @Suppress("Unused", "FunctionName") fun SampleComposeMultiplatformScreenViewController( onEvent: (SampleSharedEvent) -> Unit, factory: SampleComposeMultiplatformViewFactory, ): UIViewController { - return ComposeUIViewController { + return ComposeUIViewController( + configure = { + this.opaque = false + } + ) { SampleComposeMultiplatformView( onEvent = onEvent, factory = factory, diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ComposeSampleComposeMultiplatformViewFactory.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ComposeSampleComposeMultiplatformViewFactory.kt index dcc30806..725dedb2 100644 --- a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ComposeSampleComposeMultiplatformViewFactory.kt +++ b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ComposeSampleComposeMultiplatformViewFactory.kt @@ -1,5 +1,6 @@ package kmp.shared.samplecomposemultiplatform.presentation.ui +import androidx.compose.runtime.Composable import platform.UIKit.UIViewController // Originally generated by Touchlab's [compose-swift-bridge] @@ -9,4 +10,17 @@ actual interface ComposeSampleComposeMultiplatformViewFactory { checked: Boolean, onCheckedChanged: (Boolean) -> Unit, ): Pair + + fun createPlatformSpecificBottomBar( + items: List, + selected: String, + onSelectedChanged: (String) -> Unit, + onSizeChanged: (Float, Float) -> Unit, + ): Pair + + fun createScreenWithPlatformSpecificBottomBar( + tabs: Map, + selectedTab: String, + onSelectedTabChanged: (String) -> Unit, + ): Pair } diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.ios.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.ios.kt new file mode 100644 index 00000000..1527f37f --- /dev/null +++ b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.ios.kt @@ -0,0 +1,338 @@ +package kmp.shared.samplecomposemultiplatform.presentation.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitViewController +import androidx.compose.ui.window.ComposeUIViewController +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCClass +import kotlinx.cinterop.readValue +import kotlinx.cinterop.useContents +import platform.Foundation.NSSelectorFromString +import platform.Foundation.NSStringFromClass +import platform.UIKit.NSLayoutConstraint +import platform.UIKit.UIBlurEffect +import platform.UIKit.UIBlurEffectStyle +import platform.UIKit.UIColor +import platform.UIKit.UIDevice +import platform.UIKit.UIEdgeInsetsZero +import platform.UIKit.UIGestureRecognizer +import platform.UIKit.UIImage +import platform.UIKit.UIScreen +import platform.UIKit.UIScrollView +import platform.UIKit.UITabBar +import platform.UIKit.UITabBarAppearance +import platform.UIKit.UITabBarController +import platform.UIKit.UITabBarControllerDelegateProtocol +import platform.UIKit.UITabBarDelegateProtocol +import platform.UIKit.UITabBarItem +import platform.UIKit.UIView +import platform.UIKit.UIViewController +import platform.UIKit.setAdditionalSafeAreaInsets +import platform.UIKit.tabBarItem +import platform.darwin.NSObject +import platform.objc.objc_getClass +import kotlin.random.Random + +@OptIn(ExperimentalForeignApi::class) +@Composable +actual fun PlatformSpecificBottomBar( + items: List, + selected: String, + onSelectedChanged: (String) -> Unit, + onSizeChanged: (DpSize) -> Unit, + modifier: Modifier, +) { + val factory = LocalSampleComposeMultiplatformViewFactory.current + val key = rememberSaveable { Random.nextInt().toString(16) } + val density = LocalDensity.current + val onSizeChangedWithConversion: (Float, Float) -> Unit = { width, height -> + with(density) { onSizeChanged(DpSize(width.toDp(), height.toDp())) } + } + + val viewModel = viewModel(key = key) { + NativeViewHolderViewModel { + factory.createPlatformSpecificBottomBar( + items = items, + selected = selected, + onSelectedChanged = onSelectedChanged, + onSizeChanged = onSizeChangedWithConversion, + ) + } + } + val delegate = remember(viewModel) { viewModel.delegate } + val view = remember(viewModel) { viewModel.view } + + remember(items) { delegate.updateItems(items) } + remember(selected) { delegate.updateSelected(selected) } + remember(onSelectedChanged) { delegate.updateOnSelectedChanged(onSelectedChanged) } + remember(onSizeChanged) { delegate.updateOnSizeChanged(onSizeChangedWithConversion) } + androidx.compose.ui.viewinterop.UIKitViewController( + modifier = modifier.fillMaxWidth().height(50.dp).background(Color.Blue), + factory = { view }, + update = { controller -> + controller.view.backgroundColor = UIColor.clearColor + controller.view.opaque = false + controller.setAdditionalSafeAreaInsets(UIEdgeInsetsZero.readValue()) + }, + ) +} + +//@OptIn(ExperimentalForeignApi::class) +//@Composable +//actual fun PlatformSpecificBottomBar( +// items: List, +// selected: String, +// onSelectedChanged: (String) -> Unit, +// onSizeChanged: (DpSize) -> Unit, +// modifier: Modifier, +//) { +// val density = LocalDensity.current +// +// UIKitViewController( +// factory = { +// object : UIViewController(nibName = null, bundle = null) { +// override fun loadView() { +// // Root view +// view = UIView() +// view.backgroundColor = UIColor.clearColor +// } +// +// override fun viewDidLoad() { +// super.viewDidLoad() +// +// // Create tab bar +// val tabBar = UITabBar() +// tabBar.translatesAutoresizingMaskIntoConstraints = false +// +// // Create tab items +// tabBar.items = items.mapIndexed { index, title -> +// UITabBarItem(title = title, image = null, tag = index.toLong()) +// } +// +// // Set selected item +// val selectedIndex = items.indexOf(selected) +// if (selectedIndex >= 0) { +// tabBar.selectedItem = tabBar.items?.get(selectedIndex) as? UITabBarItem +// } +// +// // Handle selection changes +// tabBar.delegate = object : NSObject(), UITabBarDelegateProtocol { +// override fun tabBar(tabBar: UITabBar, didSelectItem: UITabBarItem) { +// val index = didSelectItem.tag.toInt() +// onSelectedChanged(items[index]) +// } +// } +// +// // Add tab bar to root view +// view.addSubview(tabBar) +// +// // Pin to bottom +// NSLayoutConstraint.activateConstraints( +// listOf( +// tabBar.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor), +// tabBar.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor), +// tabBar.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor), +// tabBar.heightAnchor.constraintEqualToConstant(50.0), // standard tab bar height +// ), +// ) +// +// // Report size +// view.layoutIfNeeded() +// val scale = UIScreen.mainScreen.scale +// tabBar.bounds.useContents { +// onSizeChanged( +// with(density) { +// DpSize( +// width = (size.width * scale).toFloat().toDp(), +// height = (size.height * scale).toFloat().toDp(), +// ) +// }, +// ) +// } +// } +// } +// }, +// modifier = Modifier +// .fillMaxWidth() +// .height(150.dp) +// .padding(bottom = 100.dp), +// ) +//} + +@OptIn(ExperimentalForeignApi::class) +@Composable +actual fun ScreenWithPlatformSpecificBottomBar( + tabs: Map Unit)>, + selectedTab: String, + onSelectedTabChanged: (String) -> Unit, + modifier: Modifier, +) { + val factory = LocalSampleComposeMultiplatformViewFactory.current + val key = rememberSaveable { Random.nextInt().toString(16) } + + val viewModel = viewModel(key = key) { + NativeViewHolderViewModel { + factory.createScreenWithPlatformSpecificBottomBar( + tabs = tabs.map { (title, composable) -> + title to ComposeUIViewController { + composable() + } + }.toMap(), + selectedTab = selectedTab, + onSelectedTabChanged = onSelectedTabChanged, + ) + } + } + val delegate = remember(viewModel) { viewModel.delegate } + val view = remember(viewModel) { viewModel.view } + + remember(tabs) { + delegate.updateTabs( + tabs.map { (title, composable) -> + title to ComposeUIViewController { + composable() + } + }.toMap(), + ) + } + remember(selectedTab) { delegate.updateSelectedTab(selectedTab) } + remember(onSelectedTabChanged) { delegate.updateOnSelectedTabChanged(onSelectedTabChanged) } + UIKitViewController( + modifier = modifier.background(Color.Blue), + factory = { view }, + update = { controller -> + controller.view.backgroundColor = UIColor.clearColor + controller.view.opaque = false + controller.setAdditionalSafeAreaInsets(UIEdgeInsetsZero.readValue()) + }, + ) +} + +//@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class) +//@Composable +//actual fun ScreenWithPlatformSpecificBottomBar( +// tabs: Map Unit)>, +// selectedTab: String, +// onSelectedTabChanged: (String) -> Unit, +// modifier: Modifier, +//) { +// UIKitViewController( +// modifier = modifier, +// factory = { +// object : UITabBarController(nibName = null, bundle = null), +// UITabBarControllerDelegateProtocol { +// +// private var composeControllers: List> = emptyList() +// +// override fun viewDidLoad() { +// super.viewDidLoad() +// delegate = this +// +// // Transparent / blur tab bar background +// if (UIDevice.currentDevice.systemVersion.toDouble() >= 15.0) { +// val appearance = UITabBarAppearance() +// appearance.configureWithDefaultBackground() +// appearance.backgroundEffect = UIBlurEffect.effectWithStyle( +// UIBlurEffectStyle.UIBlurEffectStyleSystemMaterial +// ) +// appearance.backgroundColor = UIColor.clearColor +// tabBar.standardAppearance = appearance +// tabBar.scrollEdgeAppearance = appearance +// } else { +// tabBar.barTintColor = UIColor.clearColor +// tabBar.translucent = true +// } +// +// // Build controllers for each tab +// composeControllers = tabs.map { (key, content) -> +// val vc = ComposeUIViewController { content() } +// +// // Optional syntax "Title::iconName" +// val parts = key.split("::") +// val title = parts[0] +// val iconName = parts.getOrNull(1) +// +// val item = if (iconName != null) { +// UITabBarItem( +// title = title, +// image = UIImage.systemImageNamed(iconName), +// tag = 0 +// ) +// } else { +// UITabBarItem(title = title, image = null, tag = 0) +// } +// +// vc.tabBarItem = item +// title to vc +// } +// +// setViewControllers(composeControllers.map { it.second }, animated = false) +// +// // Initial selection +// val idx = composeControllers.indexOfFirst { it.first == selectedTab } +// if (idx >= 0) selectedIndex = idx.toULong() +// +// // 🔧 Disable vertical scrolling only in Compose views +// composeControllers.forEach { (_, vc) -> +// disableComposeScroll(vc.view) +// } +// } +// +// // Handle tab selection +// override fun tabBarController( +// tabBarController: UITabBarController, +// didSelectViewController: UIViewController +// ) { +// val match = composeControllers.firstOrNull { it.second == didSelectViewController } +// match?.let { onSelectedTabChanged(it.first) } +// } +// +// // React to external selection changes +// fun updateSelectedTab(tab: String) { +// val idx = composeControllers.indexOfFirst { it.first == tab } +// if (idx >= 0 && idx.toULong() != selectedIndex) { +// selectedIndex = idx.toULong() +// } +// } +// +// /** +// * Disable vertical scrolling in Compose UIScrollViews only, +// * leaving UITabBar gestures intact. +// */ +// private fun disableComposeScroll(root: UIView?) { +// root ?: return +// val count = root.subviews.size +// for (i in 0 until count) { +// val child = root.subviews[i] as? UIView ?: continue +// +// // Only target Compose scroll views +// val className = NSStringFromClass(child.`class`() ?: return) ?: "" +// if (className.contains("UIScrollView") && className.contains("Compose")) { +// val scroll = child as UIScrollView +// scroll.bounces = false +// scroll.alwaysBounceVertical = false +// scroll.scrollEnabled = false +// // ⚠️ Do NOT remove gesture recognizers +// } +// +// // Recurse into children +// disableComposeScroll(child) +// } +// } +// } +// }, +// ) +//} \ No newline at end of file diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBarDelegate.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBarDelegate.kt new file mode 100644 index 00000000..63ec189b --- /dev/null +++ b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBarDelegate.kt @@ -0,0 +1,8 @@ +package kmp.shared.samplecomposemultiplatform.presentation.ui + +interface PlatformSpecificBottomBarDelegate { + fun updateItems(items: List) + fun updateSelected(selected: String) + fun updateOnSelectedChanged(onSelectedChanged: (String) -> Unit) + fun updateOnSizeChanged(onSizeChanged: (Float, Float) -> Unit) +} \ No newline at end of file diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBarDelegate.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBarDelegate.kt new file mode 100644 index 00000000..cded3bcf --- /dev/null +++ b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBarDelegate.kt @@ -0,0 +1,9 @@ +package kmp.shared.samplecomposemultiplatform.presentation.ui + +import platform.UIKit.UIViewController + +interface ScreenWithPlatformSpecificBottomBarDelegate { + fun updateTabs(tabs: Map) + fun updateSelectedTab(selectedTab: String) + fun updateOnSelectedTabChanged(onSelectedTabChanged: (String) -> Unit) +} \ No newline at end of file From 506b9c501d0d6f6e825d6a9eb030d6810c8e1e19 Mon Sep 17 00:00:00 2001 From: JuliaJakubcova Date: Fri, 3 Oct 2025 11:05:35 +0200 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=92=84=20[KMM]=20Liquid=20glass=20tab?= =?UTF-8?q?=20bar=20in=20compose=20multiplatform?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/SampleComposeMultiplaformMain.kt | 2 + .../PlatformSpecificView/BottomBarView.swift | 151 -------- .../PlatformSpecificBottomBarObservable.swift | 49 --- .../ScreenWithBottomBarView.swift | 53 +-- ...hPlatformSpecificBottomBarObservable.swift | 47 +-- ...iftUIComposeMultiplatformViewFactory.swift | 31 +- .../commonMain/moko-resources/images/home.svg | 1 + .../moko-resources/images/person.svg | 1 + .../moko-resources/images/search.svg | 1 + .../ui/PlatformSpecificBottomBar.android.kt | 56 --- ...enWithPlatformSpecificBottomBar.android.kt | 48 +++ .../ui/PlatformSpecificBottomBar.kt | 22 -- .../ui/SampleComposeMultiplatformScreen.kt | 124 +++---- .../ui/ScreenWithPlatformSpecificBottomBar.kt | 20 ++ .../presentation/ui/BottomBarTabForIos.kt | 12 + ...seSampleComposeMultiplatformViewFactory.kt | 14 +- .../ui/PlatformSpecificBottomBar.ios.kt | 338 ------------------ .../ui/PlatformSpecificBottomBarDelegate.kt | 8 - ...ScreenWithPlatformSpecificBottomBar.ios.kt | 83 +++++ ...enWithPlatformSpecificBottomBarDelegate.kt | 6 +- 20 files changed, 260 insertions(+), 807 deletions(-) delete mode 100644 ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/BottomBarView.swift delete mode 100644 ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/PlatformSpecificBottomBarObservable.swift create mode 100644 shared/base/src/commonMain/moko-resources/images/home.svg create mode 100644 shared/base/src/commonMain/moko-resources/images/person.svg create mode 100644 shared/base/src/commonMain/moko-resources/images/search.svg delete mode 100644 shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.android.kt create mode 100644 shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt delete mode 100644 shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.kt create mode 100644 shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.kt create mode 100644 shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/BottomBarTabForIos.kt delete mode 100644 shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.ios.kt delete mode 100644 shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBarDelegate.kt create mode 100644 shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.ios.kt diff --git a/android/samplecomposemultiplatform/src/main/kotlin/kmp/android/samplecomposemultiplatform/ui/SampleComposeMultiplaformMain.kt b/android/samplecomposemultiplatform/src/main/kotlin/kmp/android/samplecomposemultiplatform/ui/SampleComposeMultiplaformMain.kt index 16c2e221..a0c9bef4 100644 --- a/android/samplecomposemultiplatform/src/main/kotlin/kmp/android/samplecomposemultiplatform/ui/SampleComposeMultiplaformMain.kt +++ b/android/samplecomposemultiplatform/src/main/kotlin/kmp/android/samplecomposemultiplatform/ui/SampleComposeMultiplaformMain.kt @@ -8,6 +8,7 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -18,6 +19,7 @@ import dev.icerock.moko.resources.compose.stringResource import kmp.android.samplecomposemultiplatform.navigation.SampleComposeMultiplatformGraph import kmp.android.shared.navigation.composableDestination import kmp.shared.base.MR +import kmp.shared.samplecomposemultiplatform.presentation.ui.LocalSampleComposeMultiplatformViewFactory import kmp.shared.samplecomposemultiplatform.presentation.ui.SampleComposeMultiplatformScreen import kmp.shared.samplesharedviewmodel.vm.SampleSharedEvent import kmp.shared.samplesharedviewmodel.vm.SampleSharedIntent diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/BottomBarView.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/BottomBarView.swift deleted file mode 100644 index bf1b9981..00000000 --- a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/BottomBarView.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// Created by Julia Jakubcova on 30/09/2025 -// Copyright © 2025 Matee. All rights reserved. -// - -import KMPShared -import SwiftUI - -struct BottomBarView: View { - @ObservedObject var observable: PlatformSpecificBottomBarObservable - - init(observable: PlatformSpecificBottomBarObservable) { - self.observable = observable - - // Transparent tab bar setup - let appearance = UITabBarAppearance() - appearance.configureWithTransparentBackground() - appearance.backgroundColor = .clear - appearance.backgroundEffect = nil - appearance.shadowColor = nil - - appearance.selectionIndicatorImage = UIImage() - - UITabBar.appearance().standardAppearance = appearance - if #available(iOS 15.0, *) { - UITabBar.appearance().scrollEdgeAppearance = appearance - } - } - - var body: some View { - TabView(selection: $observable.selected) { - ForEach(observable.items, id: \.self) { item in - Color.clear - .frame(height: 0) - .tabItem { - Text(item) - } - .tag(item) - } - } - .frame(height: 50) - .background(Color.clear) - .overlay( - GeometryReader { geometry in - Color.clear - .onAppear { - let scale = UIScreen.main.scale - observable.onSizeChanged(geometry.size.width * scale, geometry.size.height * scale) - } - .onChange(of: geometry.size) { newSize in - let scale = UIScreen.main.scale - observable.onSizeChanged(newSize.width * scale, newSize.height * scale) - } - } - ) - .onAppear { - // This kills the default white system background from HostingController - let scenes = UIApplication.shared.connectedScenes - let windowScene = scenes.first as? UIWindowScene - windowScene?.windows.first?.rootViewController?.view.backgroundColor = .clear - } - } -} - -class TransparentHostingController: UIHostingController { - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .clear // make SwiftUI root clear - } -} - -struct BottomOnlyTabBarView: View { - - @ObservedObject var observable: PlatformSpecificBottomBarObservable - - var body: some View { - UIKitTabBarWrapper( - items: observable.items, - selected: observable.selected, - onSelectedChanged: { item in - observable.onSelectedChanged(item) - }, - onSizeChanged: { width, height in - observable.onSizeChanged(width, height) - } - ) - .frame(height: 50) // fixed height for bottom bar - } -} - -struct UIKitTabBarWrapper: UIViewRepresentable { - - let items: [String] - let selected: String - let onSelectedChanged: (String) -> Void - let onSizeChanged: (CGFloat, CGFloat) -> Void - - func makeUIView(context: Context) -> UITabBar { - let tabBar = UITabBar() - tabBar.delegate = context.coordinator - - // Create UITabBarItem for each string - tabBar.items = items.enumerated().map { index, title in - UITabBarItem(title: title, image: nil, tag: index) - } - - // Set selected item - if let selectedIndex = items.firstIndex(of: selected) { - tabBar.selectedItem = tabBar.items?[selectedIndex] - } - - // Report initial size - DispatchQueue.main.async { - onSizeChanged(tabBar.bounds.width * UIScreen.main.scale, - tabBar.bounds.height * UIScreen.main.scale) - } - - return tabBar - } - - func updateUIView(_ uiView: UITabBar, context: Context) { - // Update selected tab if changed - if let selectedIndex = items.firstIndex(of: selected) { - uiView.selectedItem = uiView.items?[selectedIndex] - } - - // Update size whenever layout changes - DispatchQueue.main.async { - onSizeChanged(uiView.bounds.width * UIScreen.main.scale, - uiView.bounds.height * UIScreen.main.scale) - } - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - class Coordinator: NSObject, UITabBarDelegate { - let parent: UIKitTabBarWrapper - - init(_ parent: UIKitTabBarWrapper) { - self.parent = parent - } - - func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { - let index = item.tag - guard index < parent.items.count else { return } - parent.onSelectedChanged(parent.items[index]) - } - } -} diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/PlatformSpecificBottomBarObservable.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/PlatformSpecificBottomBarObservable.swift deleted file mode 100644 index 0889d182..00000000 --- a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/PlatformSpecificBottomBarObservable.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// Created by Julia Jakubcova on 30/09/2025 -// Copyright © 2025 Matee. All rights reserved. -// - -import KMPShared -import SwiftUI - -class PlatformSpecificBottomBarObservable: ObservableObject, PlatformSpecificBottomBarDelegate { - - @Published var items: [String] - @Published var selected: String - @Published var onSelectedChanged: (String) -> Void - @Published var onSizeChanged: (CGFloat, CGFloat) -> Void - - init( - items: [String], - selected: String, - onSelectedChanged: @escaping (String) -> Void, - onSizeChanged: @escaping (KotlinFloat, KotlinFloat) -> Void - ) { - self.items = items - self.selected = selected - self.onSelectedChanged = onSelectedChanged - self.onSizeChanged = { width, height in - let scale = UIScreen.main.scale - onSizeChanged(KotlinFloat(value: Float(width * scale)), KotlinFloat(value: Float(height * scale))) - } - } - - func updateItems(items: [String]) { - self.items = items - } - - func updateOnSelectedChanged(onSelectedChanged: @escaping (String) -> Void) { - self.onSelectedChanged = onSelectedChanged - } - - func updateSelected(selected: String) { - self.selected = selected - } - - func updateOnSizeChanged(onSizeChanged: @escaping (KotlinFloat, KotlinFloat) -> Void) { - self.onSizeChanged = { width, height in - let scale = UIScreen.main.scale - onSizeChanged(KotlinFloat(value: Float(width * scale)), KotlinFloat(value: Float(height * scale))) - } - } -} diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift index 43d1e862..5c3d224e 100644 --- a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift @@ -12,60 +12,23 @@ struct ScreenWithBottomBarView: View { init(observable: ScreenWithPlatformSpecificBottomBarObservable) { self.observable = observable - - let appearance = UITabBarAppearance() - appearance.configureWithTransparentBackground() - appearance.backgroundEffect = UIBlurEffect(style: .systemMaterial) // liquid glass - appearance.backgroundColor = .clear - appearance.shadowColor = nil - UITabBar.appearance().standardAppearance = appearance - if #available(iOS 15.0, *) { - UITabBar.appearance().scrollEdgeAppearance = appearance - } } var body: some View { TabView(selection: $observable.selectedTab) { - ForEach(Array(observable.tabs.keys), id: \.self) { key in - let parts = key.components(separatedBy: "::") - let title = parts[0] - let icon = parts[1] - + ForEach(observable.tabs, id: \.position) { (tab: BottomBarTabForIos) in ComposeViewController { - observable.tabs[key]! - } - .ignoresSafeArea() // draw behind tab bar - .background(Color.clear) - .onAppear { - // Disable scrolling only inside Compose content, not TabView itself - if let vc = observable.tabs[key] { - disableScrollsInCompose(vc.view) - } + tab.content } + .ignoresSafeArea() .tabItem { - Label(title, systemImage: icon) - } - .tag(key) - } - } - .background(Color.clear) - } - - private func disableScrollsInCompose(_ view: UIView?) { - guard let view = view else { return } - - func recursiveDisable(_ v: UIView) { - for subview in v.subviews { - if let scroll = subview as? UIScrollView { - scroll.bounces = false - scroll.alwaysBounceVertical = false - scroll.isScrollEnabled = false - } else { - recursiveDisable(subview) + if let uiImage = tab.icon.toUIImage() { + Image(uiImage: uiImage) + } + Text(tab.title) } + .tag(tab.position) } } - - recursiveDisable(view) } } diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithPlatformSpecificBottomBarObservable.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithPlatformSpecificBottomBarObservable.swift index b83d3dd2..235ecc2a 100644 --- a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithPlatformSpecificBottomBarObservable.swift +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithPlatformSpecificBottomBarObservable.swift @@ -6,38 +6,39 @@ import KMPShared import SwiftUI -class ScreenWithPlatformSpecificBottomBarObservable: NSObject, ObservableObject, ScreenWithPlatformSpecificBottomBarDelegate, UITabBarDelegate { - - @Published var tabs: [String: UIViewController] - @Published var selectedTab: String - @Published var onSelectedTabChanged: (String) -> Void - +class ScreenWithPlatformSpecificBottomBarObservable: + ObservableObject, + ScreenWithPlatformSpecificBottomBarDelegate { + + @Published var tabs: [BottomBarTabForIos] + @Published var onSelectedTabChanged: (Int32) -> Void + @Published var selectedTab: Int32 { + didSet { + if oldValue != selectedTab { + onSelectedTabChanged(selectedTab) + } + } + } + init( - tabs: [String: UIViewController], - selectedTab: String, - onSelectedTabChanged: @escaping (String) -> Void + tabs: [BottomBarTabForIos], + selectedTab: Int32, + onSelectedTabChanged: @escaping (Int32) -> Void ) { self.tabs = tabs self.selectedTab = selectedTab self.onSelectedTabChanged = onSelectedTabChanged } - - func updateTabs(tabs: [String: UIViewController]) { + + func updateTabs(tabs: [BottomBarTabForIos]) { self.tabs = tabs } - - func updateOnSelectedTabChanged(onSelectedTabChanged: @escaping (String) -> Void) { - self.onSelectedTabChanged = onSelectedTabChanged - } - - func updateSelectedTab(selectedTab: String) { - self.selectedTab = selectedTab + + func updateOnSelectedTabChanged(onSelectedTabChanged: @escaping (KotlinInt) -> Void) { + self.onSelectedTabChanged = { onSelectedTabChanged(KotlinInt(value: $0)) } } - func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { - let index = item.tag - let key = Array(tabs.keys)[index] - selectedTab = key - onSelectedTabChanged(key) + func updateSelectedTab(selectedTabPosition: Int32) { + self.selectedTab = selectedTabPosition } } diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/SwiftUIComposeMultiplatformViewFactory.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/SwiftUIComposeMultiplatformViewFactory.swift index eb123fdf..5eb4a0e7 100644 --- a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/SwiftUIComposeMultiplatformViewFactory.swift +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/SwiftUIComposeMultiplatformViewFactory.swift @@ -20,35 +20,16 @@ public class SwiftUISampleComposeMultiplatformViewFactory: ComposeSampleComposeM return KotlinPair(first: viewController, second: observable) } - - public func createPlatformSpecificBottomBar( - items: [String], - selected: String, - onSelectedChanged: @escaping (String) -> Void, - onSizeChanged: @escaping (KotlinFloat, KotlinFloat) -> Void - ) -> KotlinPair { - let observable = PlatformSpecificBottomBarObservable( - items: items, - selected: selected, - onSelectedChanged: onSelectedChanged, - onSizeChanged: onSizeChanged - ) - let viewController: UIViewController = TransparentHostingController( - rootView: BottomBarView(observable: observable) - ) - - return KotlinPair(first: viewController, second: observable) - } - + public func createScreenWithPlatformSpecificBottomBar( - tabs: [String: UIViewController], - selectedTab: String, - onSelectedTabChanged: @escaping (String) -> Void + tabs: [BottomBarTabForIos], + selectedTabPosition: Int32, + onSelectedTabChanged: @escaping (KotlinInt) -> Void ) -> KotlinPair { let observable = ScreenWithPlatformSpecificBottomBarObservable( tabs: tabs, - selectedTab: selectedTab, - onSelectedTabChanged: onSelectedTabChanged + selectedTab: selectedTabPosition, + onSelectedTabChanged: { onSelectedTabChanged(KotlinInt(value: $0)) } ) let viewController: UIViewController = UIHostingController( rootView: ScreenWithBottomBarView(observable: observable) diff --git a/shared/base/src/commonMain/moko-resources/images/home.svg b/shared/base/src/commonMain/moko-resources/images/home.svg new file mode 100644 index 00000000..8a3ab49b --- /dev/null +++ b/shared/base/src/commonMain/moko-resources/images/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/shared/base/src/commonMain/moko-resources/images/person.svg b/shared/base/src/commonMain/moko-resources/images/person.svg new file mode 100644 index 00000000..338881d5 --- /dev/null +++ b/shared/base/src/commonMain/moko-resources/images/person.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/shared/base/src/commonMain/moko-resources/images/search.svg b/shared/base/src/commonMain/moko-resources/images/search.svg new file mode 100644 index 00000000..b78a3fe1 --- /dev/null +++ b/shared/base/src/commonMain/moko-resources/images/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.android.kt b/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.android.kt deleted file mode 100644 index f1cfe761..00000000 --- a/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.android.kt +++ /dev/null @@ -1,56 +0,0 @@ -package kmp.shared.samplecomposemultiplatform.presentation.ui - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material.BottomNavigation -import androidx.compose.material.BottomNavigationItem -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp - -@Composable -actual fun PlatformSpecificBottomBar( - items: List, - selected: String, - onSelectedChanged: (String) -> Unit, - onSizeChanged: (DpSize) -> Unit, - modifier: Modifier, -) { - val density = LocalDensity.current - BottomNavigation( - modifier = Modifier - .fillMaxWidth() - .onGloballyPositioned { - with(density) { onSizeChanged(DpSize(it.size.width.toDp(), it.size.height.toDp())) } - }, - ) { - items.forEach { item -> - BottomNavigationItem( - selected = selected == item, - icon = { - Icon(imageVector = Icons.Default.Check, contentDescription = item) - }, - onClick = { onSelectedChanged(item) }, - label = { - Text(item) - } - ) - } - } -} - -@Composable -actual fun ScreenWithPlatformSpecificBottomBar( - tabs: Map Unit)>, - selectedTab: String, - onSelectedTabChanged: (String) -> Unit, - modifier: Modifier, -) { -} \ No newline at end of file diff --git a/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt b/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt new file mode 100644 index 00000000..e030008b --- /dev/null +++ b/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt @@ -0,0 +1,48 @@ +package kmp.shared.samplecomposemultiplatform.presentation.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.safeContentPadding +import androidx.compose.material.BottomNavigation +import androidx.compose.material.BottomNavigationItem +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource + +@Composable +actual fun ScreenWithPlatformSpecificBottomBar( + tabs: List, + selectedTabPosition: Int, + onSelectedTabChanged: (Int) -> Unit, + modifier: Modifier, +) { + val selectedTab = tabs.firstOrNull { it.position == selectedTabPosition } + Column(modifier = modifier) { + Box(modifier = Modifier.weight(1f)) { + selectedTab?.content?.invoke() + } + + BottomNavigation( + modifier = Modifier.fillMaxWidth(), + ) { + tabs.forEach { tab -> + BottomNavigationItem( + selected = selectedTabPosition == tab.position, + icon = { + Icon( + painter = painterResource(tab.icon.drawableResId), + contentDescription = tab.title, + ) + }, + onClick = { onSelectedTabChanged(tab.position) }, + label = { + Text(tab.title) + }, + ) + } + } + } +} \ No newline at end of file diff --git a/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.kt b/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.kt deleted file mode 100644 index 4c0a1ee3..00000000 --- a/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.kt +++ /dev/null @@ -1,22 +0,0 @@ -package kmp.shared.samplecomposemultiplatform.presentation.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.DpSize - -@Composable -expect fun PlatformSpecificBottomBar( - items: List, - selected: String, - onSelectedChanged: (String) -> Unit, - onSizeChanged: (DpSize) -> Unit, - modifier: Modifier = Modifier, -) - -@Composable -expect fun ScreenWithPlatformSpecificBottomBar( - tabs: Map Unit>, - selectedTab: String, - onSelectedTabChanged: (String) -> Unit, - modifier: Modifier = Modifier, -) diff --git a/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/SampleComposeMultiplatformScreen.kt b/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/SampleComposeMultiplatformScreen.kt index dc4a2bff..017a1ff8 100644 --- a/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/SampleComposeMultiplatformScreen.kt +++ b/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/SampleComposeMultiplatformScreen.kt @@ -1,6 +1,7 @@ package kmp.shared.samplecomposemultiplatform.presentation.ui import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -14,15 +15,18 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import kmp.shared.base.MR import kmp.shared.samplecomposemultiplatform.presentation.common.AppTheme import kmp.shared.samplecomposemultiplatform.presentation.common.StarterButton import kmp.shared.samplecomposemultiplatform.presentation.ui.test.TestTags @@ -42,82 +46,47 @@ fun SampleComposeMultiplatformScreen( if (loading) { CircularProgressIndicator() } else { -// Box(contentAlignment = Alignment.BottomCenter) { -// Column( -// horizontalAlignment = Alignment.CenterHorizontally, -// verticalArrangement = Arrangement.spacedBy(16.dp), -// modifier = Modifier -// .verticalScroll(rememberScrollState()) -// .safeContentPadding() -// .padding(16.dp), -// ) { -// repeat(10) { -// Text( -// text = "This is a sample with compose multiplatform UI and shared VM", -// textAlign = TextAlign.Center, -// ) -// -// Text( -// text = state.sampleText?.value ?: "", -// modifier = Modifier.testTag(TestTags.SampleComposeMultiplatformScreen.SampleText), -// ) -// -// var isChecked by remember { mutableStateOf(false) } -// PlatformSpecificCheckboxView( -// text = "This is a view implemented in Compose on Android and SwiftUI on iOS", -// checked = isChecked, -// onCheckedChanged = { isChecked = it }, -// modifier = Modifier.fillMaxWidth().height(60.dp), -// ) -// -// StarterButton(onClick = { onIntent(SampleSharedIntent.OnNextButtonTapped) }) { -// Text(text = "Go to next screen") -// } -// } -// } -// - var selected by remember { mutableStateOf("one") } -// val tabs = listOf("one", "two", "three") -// var size by remember { mutableStateOf(DpSize(10.dp, 10.dp)) } -// PlatformSpecificBottomBar( -// items = tabs, -// selected = selected, -// onSelectedChanged = { selected = it }, -// onSizeChanged = { size = it }, -// modifier = Modifier -// .align(Alignment.BottomCenter) -// .safeContentPadding() -// .size(size), -// ) -// } - - - val factory = LocalSampleComposeMultiplatformViewFactory.current + var selected by remember { mutableIntStateOf(0) } ScreenWithPlatformSpecificBottomBar( - tabs = mapOf( - "Home::house.fill" to { - CompositionLocalProvider( - LocalSampleComposeMultiplatformViewFactory provides factory, - ) { - Content(state, onIntent) - } - }, - "Search::magnifyingglass" to { - CompositionLocalProvider( - LocalSampleComposeMultiplatformViewFactory provides factory, - ) { - Content(state, onIntent) - } - }, - "Profile::person.crop.circle" to { - CompositionLocalProvider( - LocalSampleComposeMultiplatformViewFactory provides factory, - ) { - Content(state, onIntent) - } - }, + tabs = listOf( + BottomBarTab( + title = "Home", + icon = MR.images.home, + position = 0, + content = { + Content( + state = state, + onIntent = onIntent, + modifier = Modifier.background(Color.Yellow.copy(alpha = 0.2f)), + ) + }, + ), + BottomBarTab( + title = "Search", + icon = MR.images.search, + position = 1, + content = { + Content( + state = state, + onIntent = onIntent, + modifier = Modifier.background(Color.Blue.copy(alpha = 0.2f)), + ) + }, + ), + BottomBarTab( + title = "Profile", + icon = MR.images.person, + position = 2, + content = { + Content( + state = state, + onIntent = onIntent, + modifier = Modifier.background(Color.Cyan.copy(alpha = 0.2f)), + ) + }, + ), ), - selectedTab = selected, + selectedTabPosition = selected, onSelectedTabChanged = { selected = it }, modifier = Modifier.fillMaxSize(), ) @@ -135,7 +104,10 @@ private fun Content( Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = modifier.verticalScroll(rememberScrollState()).padding(16.dp).safeContentPadding(), + modifier = modifier + .verticalScroll(rememberScrollState()) + .padding(16.dp) + .safeContentPadding(), ) { repeat(10) { Text( diff --git a/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.kt b/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.kt new file mode 100644 index 00000000..50b47eda --- /dev/null +++ b/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.kt @@ -0,0 +1,20 @@ +package kmp.shared.samplecomposemultiplatform.presentation.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import dev.icerock.moko.resources.ImageResource + +@Composable +expect fun ScreenWithPlatformSpecificBottomBar( + tabs: List, + selectedTabPosition: Int, + onSelectedTabChanged: (Int) -> Unit, + modifier: Modifier = Modifier, +) + +data class BottomBarTab( + val title: String, + val icon: ImageResource, + val position: Int, + val content: @Composable () -> Unit, +) diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/BottomBarTabForIos.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/BottomBarTabForIos.kt new file mode 100644 index 00000000..4fd17dc5 --- /dev/null +++ b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/BottomBarTabForIos.kt @@ -0,0 +1,12 @@ +package kmp.shared.samplecomposemultiplatform.presentation.ui + +import androidx.compose.runtime.Composable +import dev.icerock.moko.resources.ImageResource +import platform.UIKit.UIViewController + +data class BottomBarTabForIos( + val title: String, + val icon: ImageResource, + val position: Int, + val content: UIViewController, +) \ No newline at end of file diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ComposeSampleComposeMultiplatformViewFactory.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ComposeSampleComposeMultiplatformViewFactory.kt index 725dedb2..19941f23 100644 --- a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ComposeSampleComposeMultiplatformViewFactory.kt +++ b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ComposeSampleComposeMultiplatformViewFactory.kt @@ -1,6 +1,5 @@ package kmp.shared.samplecomposemultiplatform.presentation.ui -import androidx.compose.runtime.Composable import platform.UIKit.UIViewController // Originally generated by Touchlab's [compose-swift-bridge] @@ -11,16 +10,9 @@ actual interface ComposeSampleComposeMultiplatformViewFactory { onCheckedChanged: (Boolean) -> Unit, ): Pair - fun createPlatformSpecificBottomBar( - items: List, - selected: String, - onSelectedChanged: (String) -> Unit, - onSizeChanged: (Float, Float) -> Unit, - ): Pair - fun createScreenWithPlatformSpecificBottomBar( - tabs: Map, - selectedTab: String, - onSelectedTabChanged: (String) -> Unit, + tabs: List, + selectedTabPosition: Int, + onSelectedTabChanged: (Int) -> Unit, ): Pair } diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.ios.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.ios.kt deleted file mode 100644 index 1527f37f..00000000 --- a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBar.ios.kt +++ /dev/null @@ -1,338 +0,0 @@ -package kmp.shared.samplecomposemultiplatform.presentation.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.UIKitViewController -import androidx.compose.ui.window.ComposeUIViewController -import androidx.lifecycle.viewmodel.compose.viewModel -import kotlinx.cinterop.BetaInteropApi -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.ObjCClass -import kotlinx.cinterop.readValue -import kotlinx.cinterop.useContents -import platform.Foundation.NSSelectorFromString -import platform.Foundation.NSStringFromClass -import platform.UIKit.NSLayoutConstraint -import platform.UIKit.UIBlurEffect -import platform.UIKit.UIBlurEffectStyle -import platform.UIKit.UIColor -import platform.UIKit.UIDevice -import platform.UIKit.UIEdgeInsetsZero -import platform.UIKit.UIGestureRecognizer -import platform.UIKit.UIImage -import platform.UIKit.UIScreen -import platform.UIKit.UIScrollView -import platform.UIKit.UITabBar -import platform.UIKit.UITabBarAppearance -import platform.UIKit.UITabBarController -import platform.UIKit.UITabBarControllerDelegateProtocol -import platform.UIKit.UITabBarDelegateProtocol -import platform.UIKit.UITabBarItem -import platform.UIKit.UIView -import platform.UIKit.UIViewController -import platform.UIKit.setAdditionalSafeAreaInsets -import platform.UIKit.tabBarItem -import platform.darwin.NSObject -import platform.objc.objc_getClass -import kotlin.random.Random - -@OptIn(ExperimentalForeignApi::class) -@Composable -actual fun PlatformSpecificBottomBar( - items: List, - selected: String, - onSelectedChanged: (String) -> Unit, - onSizeChanged: (DpSize) -> Unit, - modifier: Modifier, -) { - val factory = LocalSampleComposeMultiplatformViewFactory.current - val key = rememberSaveable { Random.nextInt().toString(16) } - val density = LocalDensity.current - val onSizeChangedWithConversion: (Float, Float) -> Unit = { width, height -> - with(density) { onSizeChanged(DpSize(width.toDp(), height.toDp())) } - } - - val viewModel = viewModel(key = key) { - NativeViewHolderViewModel { - factory.createPlatformSpecificBottomBar( - items = items, - selected = selected, - onSelectedChanged = onSelectedChanged, - onSizeChanged = onSizeChangedWithConversion, - ) - } - } - val delegate = remember(viewModel) { viewModel.delegate } - val view = remember(viewModel) { viewModel.view } - - remember(items) { delegate.updateItems(items) } - remember(selected) { delegate.updateSelected(selected) } - remember(onSelectedChanged) { delegate.updateOnSelectedChanged(onSelectedChanged) } - remember(onSizeChanged) { delegate.updateOnSizeChanged(onSizeChangedWithConversion) } - androidx.compose.ui.viewinterop.UIKitViewController( - modifier = modifier.fillMaxWidth().height(50.dp).background(Color.Blue), - factory = { view }, - update = { controller -> - controller.view.backgroundColor = UIColor.clearColor - controller.view.opaque = false - controller.setAdditionalSafeAreaInsets(UIEdgeInsetsZero.readValue()) - }, - ) -} - -//@OptIn(ExperimentalForeignApi::class) -//@Composable -//actual fun PlatformSpecificBottomBar( -// items: List, -// selected: String, -// onSelectedChanged: (String) -> Unit, -// onSizeChanged: (DpSize) -> Unit, -// modifier: Modifier, -//) { -// val density = LocalDensity.current -// -// UIKitViewController( -// factory = { -// object : UIViewController(nibName = null, bundle = null) { -// override fun loadView() { -// // Root view -// view = UIView() -// view.backgroundColor = UIColor.clearColor -// } -// -// override fun viewDidLoad() { -// super.viewDidLoad() -// -// // Create tab bar -// val tabBar = UITabBar() -// tabBar.translatesAutoresizingMaskIntoConstraints = false -// -// // Create tab items -// tabBar.items = items.mapIndexed { index, title -> -// UITabBarItem(title = title, image = null, tag = index.toLong()) -// } -// -// // Set selected item -// val selectedIndex = items.indexOf(selected) -// if (selectedIndex >= 0) { -// tabBar.selectedItem = tabBar.items?.get(selectedIndex) as? UITabBarItem -// } -// -// // Handle selection changes -// tabBar.delegate = object : NSObject(), UITabBarDelegateProtocol { -// override fun tabBar(tabBar: UITabBar, didSelectItem: UITabBarItem) { -// val index = didSelectItem.tag.toInt() -// onSelectedChanged(items[index]) -// } -// } -// -// // Add tab bar to root view -// view.addSubview(tabBar) -// -// // Pin to bottom -// NSLayoutConstraint.activateConstraints( -// listOf( -// tabBar.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor), -// tabBar.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor), -// tabBar.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor), -// tabBar.heightAnchor.constraintEqualToConstant(50.0), // standard tab bar height -// ), -// ) -// -// // Report size -// view.layoutIfNeeded() -// val scale = UIScreen.mainScreen.scale -// tabBar.bounds.useContents { -// onSizeChanged( -// with(density) { -// DpSize( -// width = (size.width * scale).toFloat().toDp(), -// height = (size.height * scale).toFloat().toDp(), -// ) -// }, -// ) -// } -// } -// } -// }, -// modifier = Modifier -// .fillMaxWidth() -// .height(150.dp) -// .padding(bottom = 100.dp), -// ) -//} - -@OptIn(ExperimentalForeignApi::class) -@Composable -actual fun ScreenWithPlatformSpecificBottomBar( - tabs: Map Unit)>, - selectedTab: String, - onSelectedTabChanged: (String) -> Unit, - modifier: Modifier, -) { - val factory = LocalSampleComposeMultiplatformViewFactory.current - val key = rememberSaveable { Random.nextInt().toString(16) } - - val viewModel = viewModel(key = key) { - NativeViewHolderViewModel { - factory.createScreenWithPlatformSpecificBottomBar( - tabs = tabs.map { (title, composable) -> - title to ComposeUIViewController { - composable() - } - }.toMap(), - selectedTab = selectedTab, - onSelectedTabChanged = onSelectedTabChanged, - ) - } - } - val delegate = remember(viewModel) { viewModel.delegate } - val view = remember(viewModel) { viewModel.view } - - remember(tabs) { - delegate.updateTabs( - tabs.map { (title, composable) -> - title to ComposeUIViewController { - composable() - } - }.toMap(), - ) - } - remember(selectedTab) { delegate.updateSelectedTab(selectedTab) } - remember(onSelectedTabChanged) { delegate.updateOnSelectedTabChanged(onSelectedTabChanged) } - UIKitViewController( - modifier = modifier.background(Color.Blue), - factory = { view }, - update = { controller -> - controller.view.backgroundColor = UIColor.clearColor - controller.view.opaque = false - controller.setAdditionalSafeAreaInsets(UIEdgeInsetsZero.readValue()) - }, - ) -} - -//@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class) -//@Composable -//actual fun ScreenWithPlatformSpecificBottomBar( -// tabs: Map Unit)>, -// selectedTab: String, -// onSelectedTabChanged: (String) -> Unit, -// modifier: Modifier, -//) { -// UIKitViewController( -// modifier = modifier, -// factory = { -// object : UITabBarController(nibName = null, bundle = null), -// UITabBarControllerDelegateProtocol { -// -// private var composeControllers: List> = emptyList() -// -// override fun viewDidLoad() { -// super.viewDidLoad() -// delegate = this -// -// // Transparent / blur tab bar background -// if (UIDevice.currentDevice.systemVersion.toDouble() >= 15.0) { -// val appearance = UITabBarAppearance() -// appearance.configureWithDefaultBackground() -// appearance.backgroundEffect = UIBlurEffect.effectWithStyle( -// UIBlurEffectStyle.UIBlurEffectStyleSystemMaterial -// ) -// appearance.backgroundColor = UIColor.clearColor -// tabBar.standardAppearance = appearance -// tabBar.scrollEdgeAppearance = appearance -// } else { -// tabBar.barTintColor = UIColor.clearColor -// tabBar.translucent = true -// } -// -// // Build controllers for each tab -// composeControllers = tabs.map { (key, content) -> -// val vc = ComposeUIViewController { content() } -// -// // Optional syntax "Title::iconName" -// val parts = key.split("::") -// val title = parts[0] -// val iconName = parts.getOrNull(1) -// -// val item = if (iconName != null) { -// UITabBarItem( -// title = title, -// image = UIImage.systemImageNamed(iconName), -// tag = 0 -// ) -// } else { -// UITabBarItem(title = title, image = null, tag = 0) -// } -// -// vc.tabBarItem = item -// title to vc -// } -// -// setViewControllers(composeControllers.map { it.second }, animated = false) -// -// // Initial selection -// val idx = composeControllers.indexOfFirst { it.first == selectedTab } -// if (idx >= 0) selectedIndex = idx.toULong() -// -// // 🔧 Disable vertical scrolling only in Compose views -// composeControllers.forEach { (_, vc) -> -// disableComposeScroll(vc.view) -// } -// } -// -// // Handle tab selection -// override fun tabBarController( -// tabBarController: UITabBarController, -// didSelectViewController: UIViewController -// ) { -// val match = composeControllers.firstOrNull { it.second == didSelectViewController } -// match?.let { onSelectedTabChanged(it.first) } -// } -// -// // React to external selection changes -// fun updateSelectedTab(tab: String) { -// val idx = composeControllers.indexOfFirst { it.first == tab } -// if (idx >= 0 && idx.toULong() != selectedIndex) { -// selectedIndex = idx.toULong() -// } -// } -// -// /** -// * Disable vertical scrolling in Compose UIScrollViews only, -// * leaving UITabBar gestures intact. -// */ -// private fun disableComposeScroll(root: UIView?) { -// root ?: return -// val count = root.subviews.size -// for (i in 0 until count) { -// val child = root.subviews[i] as? UIView ?: continue -// -// // Only target Compose scroll views -// val className = NSStringFromClass(child.`class`() ?: return) ?: "" -// if (className.contains("UIScrollView") && className.contains("Compose")) { -// val scroll = child as UIScrollView -// scroll.bounces = false -// scroll.alwaysBounceVertical = false -// scroll.scrollEnabled = false -// // ⚠️ Do NOT remove gesture recognizers -// } -// -// // Recurse into children -// disableComposeScroll(child) -// } -// } -// } -// }, -// ) -//} \ No newline at end of file diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBarDelegate.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBarDelegate.kt deleted file mode 100644 index 63ec189b..00000000 --- a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/PlatformSpecificBottomBarDelegate.kt +++ /dev/null @@ -1,8 +0,0 @@ -package kmp.shared.samplecomposemultiplatform.presentation.ui - -interface PlatformSpecificBottomBarDelegate { - fun updateItems(items: List) - fun updateSelected(selected: String) - fun updateOnSelectedChanged(onSelectedChanged: (String) -> Unit) - fun updateOnSizeChanged(onSizeChanged: (Float, Float) -> Unit) -} \ No newline at end of file diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.ios.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.ios.kt new file mode 100644 index 00000000..24eb29a8 --- /dev/null +++ b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.ios.kt @@ -0,0 +1,83 @@ +package kmp.shared.samplecomposemultiplatform.presentation.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.UIKitViewController +import androidx.compose.ui.window.ComposeUIViewController +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.readValue +import platform.UIKit.UIColor +import platform.UIKit.UIEdgeInsetsZero +import platform.UIKit.setAdditionalSafeAreaInsets +import kotlin.random.Random + +@OptIn(ExperimentalForeignApi::class) +@Composable +actual fun ScreenWithPlatformSpecificBottomBar( + tabs: List, + selectedTabPosition: Int, + onSelectedTabChanged: (Int) -> Unit, + modifier: Modifier, +) { + val factory = LocalSampleComposeMultiplatformViewFactory.current + val key = rememberSaveable { Random.nextInt().toString(16) } + + val viewModel = viewModel(key = key) { + NativeViewHolderViewModel { + factory.createScreenWithPlatformSpecificBottomBar( + tabs = tabs.map { tab -> + BottomBarTabForIos( + title = tab.title, + icon = tab.icon, + position = tab.position, + content = ComposeUIViewController { + CompositionLocalProvider( + LocalSampleComposeMultiplatformViewFactory provides factory, + ) { + tab.content() + } + }, + ) + }, + selectedTabPosition = selectedTabPosition, + onSelectedTabChanged = onSelectedTabChanged, + ) + } + } + val delegate = remember(viewModel) { viewModel.delegate } + val view = remember(viewModel) { viewModel.view } + + remember(tabs) { + delegate.updateTabs( + tabs.map { tab -> + BottomBarTabForIos( + title = tab.title, + icon = tab.icon, + position = tab.position, + content = ComposeUIViewController { + CompositionLocalProvider( + LocalSampleComposeMultiplatformViewFactory provides factory, + ) { + tab.content() + } + }, + ) + }, + ) + } + remember(selectedTabPosition) { delegate.updateSelectedTab(selectedTabPosition) } + remember(onSelectedTabChanged) { delegate.updateOnSelectedTabChanged(onSelectedTabChanged) } + UIKitViewController( + modifier = modifier, + factory = { view }, + update = { controller -> + controller.view.backgroundColor = UIColor.clearColor + controller.view.opaque = false + controller.setAdditionalSafeAreaInsets(UIEdgeInsetsZero.readValue()) + }, + ) +} diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBarDelegate.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBarDelegate.kt index cded3bcf..1a905c47 100644 --- a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBarDelegate.kt +++ b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBarDelegate.kt @@ -3,7 +3,7 @@ package kmp.shared.samplecomposemultiplatform.presentation.ui import platform.UIKit.UIViewController interface ScreenWithPlatformSpecificBottomBarDelegate { - fun updateTabs(tabs: Map) - fun updateSelectedTab(selectedTab: String) - fun updateOnSelectedTabChanged(onSelectedTabChanged: (String) -> Unit) + fun updateTabs(tabs: List) + fun updateSelectedTab(selectedTabPosition: Int) + fun updateOnSelectedTabChanged(onSelectedTabChanged: (Int) -> Unit) } \ No newline at end of file From dda6dac11c7485baff8629e2c32d5a2c40b8308d Mon Sep 17 00:00:00 2001 From: JuliaJakubcova Date: Mon, 6 Oct 2025 11:37:16 +0200 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=A9=B9=20[KMM]=20Use=20ignoreSafeArea?= =?UTF-8?q?=20only=20for=20iOS=20version=2026=20and=20higher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ScreenWithBottomBarView.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift index 5c3d224e..37393885 100644 --- a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift @@ -16,11 +16,15 @@ struct ScreenWithBottomBarView: View { var body: some View { TabView(selection: $observable.selectedTab) { - ForEach(observable.tabs, id: \.position) { (tab: BottomBarTabForIos) in - ComposeViewController { - tab.content + ForEach(observable.tabs, id: \.position) { tab in + Group { + if #available(iOS 26.0, *) { + ComposeViewController { tab.content } + .ignoresSafeArea(.all, edges: .bottom) + } else { + ComposeViewController { tab.content } + } } - .ignoresSafeArea() .tabItem { if let uiImage = tab.icon.toUIImage() { Image(uiImage: uiImage) From d3b2a1eb787ad04daf03f21ead3feece156e4ef7 Mon Sep 17 00:00:00 2001 From: JuliaJakubcova Date: Mon, 6 Oct 2025 11:52:44 +0200 Subject: [PATCH 4/9] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20[KMM]=20Refactor=20rep?= =?UTF-8?q?eating=20code,=20remove=20unused=20imports,=20fix=20lint=20issu?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/SampleComposeMultiplaformMain.kt | 2 - ...enWithPlatformSpecificBottomBar.android.kt | 3 +- .../ui/SampleComposeMultiplatformScreen.kt | 1 - ...omposeMultiplatformScreenViewController.kt | 2 +- .../presentation/ui/BottomBarTabForIos.kt | 3 +- ...ScreenWithPlatformSpecificBottomBar.ios.kt | 48 +++++++------------ ...enWithPlatformSpecificBottomBarDelegate.kt | 4 +- 7 files changed, 20 insertions(+), 43 deletions(-) diff --git a/android/samplecomposemultiplatform/src/main/kotlin/kmp/android/samplecomposemultiplatform/ui/SampleComposeMultiplaformMain.kt b/android/samplecomposemultiplatform/src/main/kotlin/kmp/android/samplecomposemultiplatform/ui/SampleComposeMultiplaformMain.kt index a0c9bef4..16c2e221 100644 --- a/android/samplecomposemultiplatform/src/main/kotlin/kmp/android/samplecomposemultiplatform/ui/SampleComposeMultiplaformMain.kt +++ b/android/samplecomposemultiplatform/src/main/kotlin/kmp/android/samplecomposemultiplatform/ui/SampleComposeMultiplaformMain.kt @@ -8,7 +8,6 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -19,7 +18,6 @@ import dev.icerock.moko.resources.compose.stringResource import kmp.android.samplecomposemultiplatform.navigation.SampleComposeMultiplatformGraph import kmp.android.shared.navigation.composableDestination import kmp.shared.base.MR -import kmp.shared.samplecomposemultiplatform.presentation.ui.LocalSampleComposeMultiplatformViewFactory import kmp.shared.samplecomposemultiplatform.presentation.ui.SampleComposeMultiplatformScreen import kmp.shared.samplesharedviewmodel.vm.SampleSharedEvent import kmp.shared.samplesharedviewmodel.vm.SampleSharedIntent diff --git a/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt b/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt index e030008b..2e0a8501 100644 --- a/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt +++ b/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt @@ -3,7 +3,6 @@ package kmp.shared.samplecomposemultiplatform.presentation.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.safeContentPadding import androidx.compose.material.BottomNavigation import androidx.compose.material.BottomNavigationItem import androidx.compose.material.Icon @@ -45,4 +44,4 @@ actual fun ScreenWithPlatformSpecificBottomBar( } } } -} \ No newline at end of file +} diff --git a/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/SampleComposeMultiplatformScreen.kt b/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/SampleComposeMultiplatformScreen.kt index 017a1ff8..ed15f9f2 100644 --- a/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/SampleComposeMultiplatformScreen.kt +++ b/shared/samplecomposemultiplatform/src/commonMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/SampleComposeMultiplatformScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/SampleComposeMultiplatformScreenViewController.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/SampleComposeMultiplatformScreenViewController.kt index f4b0d513..a13caf00 100644 --- a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/SampleComposeMultiplatformScreenViewController.kt +++ b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/SampleComposeMultiplatformScreenViewController.kt @@ -29,7 +29,7 @@ fun SampleComposeMultiplatformScreenViewController( return ComposeUIViewController( configure = { this.opaque = false - } + }, ) { SampleComposeMultiplatformView( onEvent = onEvent, diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/BottomBarTabForIos.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/BottomBarTabForIos.kt index 4fd17dc5..8a1461c0 100644 --- a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/BottomBarTabForIos.kt +++ b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/BottomBarTabForIos.kt @@ -1,6 +1,5 @@ package kmp.shared.samplecomposemultiplatform.presentation.ui -import androidx.compose.runtime.Composable import dev.icerock.moko.resources.ImageResource import platform.UIKit.UIViewController @@ -9,4 +8,4 @@ data class BottomBarTabForIos( val icon: ImageResource, val position: Int, val content: UIViewController, -) \ No newline at end of file +) diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.ios.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.ios.kt index 24eb29a8..8141a2af 100644 --- a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.ios.kt +++ b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.ios.kt @@ -29,20 +29,7 @@ actual fun ScreenWithPlatformSpecificBottomBar( val viewModel = viewModel(key = key) { NativeViewHolderViewModel { factory.createScreenWithPlatformSpecificBottomBar( - tabs = tabs.map { tab -> - BottomBarTabForIos( - title = tab.title, - icon = tab.icon, - position = tab.position, - content = ComposeUIViewController { - CompositionLocalProvider( - LocalSampleComposeMultiplatformViewFactory provides factory, - ) { - tab.content() - } - }, - ) - }, + tabs = tabs.map { it.toIosTab(factory) }, selectedTabPosition = selectedTabPosition, onSelectedTabChanged = onSelectedTabChanged, ) @@ -51,24 +38,7 @@ actual fun ScreenWithPlatformSpecificBottomBar( val delegate = remember(viewModel) { viewModel.delegate } val view = remember(viewModel) { viewModel.view } - remember(tabs) { - delegate.updateTabs( - tabs.map { tab -> - BottomBarTabForIos( - title = tab.title, - icon = tab.icon, - position = tab.position, - content = ComposeUIViewController { - CompositionLocalProvider( - LocalSampleComposeMultiplatformViewFactory provides factory, - ) { - tab.content() - } - }, - ) - }, - ) - } + remember(tabs) { delegate.updateTabs(tabs.map { it.toIosTab(factory) }) } remember(selectedTabPosition) { delegate.updateSelectedTab(selectedTabPosition) } remember(onSelectedTabChanged) { delegate.updateOnSelectedTabChanged(onSelectedTabChanged) } UIKitViewController( @@ -81,3 +51,17 @@ actual fun ScreenWithPlatformSpecificBottomBar( }, ) } + +private fun BottomBarTab.toIosTab(factory: ComposeSampleComposeMultiplatformViewFactory): BottomBarTabForIos = + BottomBarTabForIos( + title = title, + icon = icon, + position = position, + content = ComposeUIViewController { + CompositionLocalProvider( + LocalSampleComposeMultiplatformViewFactory provides factory, + ) { + content() + } + }, + ) diff --git a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBarDelegate.kt b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBarDelegate.kt index 1a905c47..08c1e8ed 100644 --- a/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBarDelegate.kt +++ b/shared/samplecomposemultiplatform/src/iosMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBarDelegate.kt @@ -1,9 +1,7 @@ package kmp.shared.samplecomposemultiplatform.presentation.ui -import platform.UIKit.UIViewController - interface ScreenWithPlatformSpecificBottomBarDelegate { fun updateTabs(tabs: List) fun updateSelectedTab(selectedTabPosition: Int) fun updateOnSelectedTabChanged(onSelectedTabChanged: (Int) -> Unit) -} \ No newline at end of file +} From 3ae43a7c0f44f08c1c143abbfaf72450fb1d6910 Mon Sep 17 00:00:00 2001 From: JuliaJakubcova Date: Wed, 8 Oct 2025 13:55:09 +0200 Subject: [PATCH 5/9] =?UTF-8?q?=E2=9C=A8=20[KMM]=20Add=20sample=20with=20t?= =?UTF-8?q?abs=20and=20toolbar,=20implement=20NativeScaffold=20composable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kmp/android/MainActivity.kt | 43 ++++- .../src/main/kotlin/config/KmmConfig.kt | 1 + gradle/libs.versions.toml | 12 +- .../DependencyInjection/KMPViewModels.swift | 1 + ios/Application/MainFlowController.swift | 11 +- .../NativeScaffoldObservable.swift | 48 ++++++ .../NativeScaffoldView.swift | 100 ++++++++++++ .../ScreenWithBottomBarView.swift | 64 ++++++-- .../PlatformSpecificView/SwiftUIFactory.swift | 33 ++++ .../SampleTabBarView.swift | 37 +++++ .../TestScreen.swift | 62 ++++++++ .../Extensions/Color+Extensions.swift | 14 ++ settings.gradle.kts | 1 + shared/core/build.gradle.kts | 2 + .../kotlin/kmp/shared/core/di/Module.kt | 2 + .../build.gradle.kts | 1 + ...enWithPlatformSpecificBottomBar.android.kt | 21 ++- shared/sampletabbar/build.gradle.kts | 39 +++++ .../presentation/ui/ComposeViewFactory.kt | 4 + .../presentation/ui/NativeScaffold.android.kt | 116 ++++++++++++++ .../kmp/shared/sampletabbar/di/Module.kt | 9 ++ .../presentation/ui/BlurredContainer.kt | 89 +++++++++++ .../presentation/ui/ComposeViewFactory.kt | 7 + .../presentation/ui/LocalViewFactory.kt | 14 ++ .../presentation/ui/NativeScaffold.kt | 46 ++++++ .../presentation/ui/SampleTabBarScreen.kt | 149 ++++++++++++++++++ .../presentation/ui/SampleTheme.kt | 97 ++++++++++++ .../presentation/vm/SampleTabBarViewModel.kt | 43 +++++ .../SampleTabBarViewController.kt | 47 ++++++ .../presentation/ui/ComposeViewFactory.kt | 15 ++ .../presentation/ui/NativeScaffold.ios.kt | 99 ++++++++++++ .../presentation/ui/NativeScaffoldDelegate.kt | 7 + 32 files changed, 1201 insertions(+), 33 deletions(-) create mode 100644 ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/NativeScaffoldObservable.swift create mode 100644 ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/NativeScaffoldView.swift create mode 100644 ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/SwiftUIFactory.swift create mode 100644 ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/SampleTabBarView.swift create mode 100644 ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/TestScreen.swift create mode 100644 ios/PresentationLayer/UIToolkit/Sources/UIToolkit/Extensions/Color+Extensions.swift create mode 100644 shared/sampletabbar/build.gradle.kts create mode 100644 shared/sampletabbar/src/androidMain/kotlin/kmp/shared/sampletabbar/presentation/ui/ComposeViewFactory.kt create mode 100644 shared/sampletabbar/src/androidMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.android.kt create mode 100644 shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/di/Module.kt create mode 100644 shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/BlurredContainer.kt create mode 100644 shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/ComposeViewFactory.kt create mode 100644 shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/LocalViewFactory.kt create mode 100644 shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.kt create mode 100644 shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/SampleTabBarScreen.kt create mode 100644 shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/SampleTheme.kt create mode 100644 shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/vm/SampleTabBarViewModel.kt create mode 100644 shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/SampleTabBarViewController.kt create mode 100644 shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/ComposeViewFactory.kt create mode 100644 shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt create mode 100644 shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffoldDelegate.kt diff --git a/android/app/src/main/kotlin/kmp/android/MainActivity.kt b/android/app/src/main/kotlin/kmp/android/MainActivity.kt index d5e07758..9e93ce9d 100644 --- a/android/app/src/main/kotlin/kmp/android/MainActivity.kt +++ b/android/app/src/main/kotlin/kmp/android/MainActivity.kt @@ -1,13 +1,19 @@ package kmp.android +import SampleTabBarScreen 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?) { @@ -19,9 +25,34 @@ class MainActivity : BaseActivity() { override fun onStart() { super.onStart() setContent { - AppTheme { - Root() + SampleTheme { +// Root() + SampleTabBarMainRoute() } } } } + +@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, + ) +} diff --git a/build-logic/convention/src/main/kotlin/config/KmmConfig.kt b/build-logic/convention/src/main/kotlin/config/KmmConfig.kt index e8f8af38..0bb2c1a3 100644 --- a/build-logic/convention/src/main/kotlin/config/KmmConfig.kt +++ b/build-logic/convention/src/main/kotlin/config/KmmConfig.kt @@ -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") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7124724f..62e85037 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" @@ -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 @@ -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" } @@ -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 = [ diff --git a/ios/Application/DependencyInjection/Sources/DependencyInjection/KMPViewModels.swift b/ios/Application/DependencyInjection/Sources/DependencyInjection/KMPViewModels.swift index 2db12053..9208b3b2 100644 --- a/ios/Application/DependencyInjection/Sources/DependencyInjection/KMPViewModels.swift +++ b/ios/Application/DependencyInjection/Sources/DependencyInjection/KMPViewModels.swift @@ -14,4 +14,5 @@ public extension Container { // Sample var sampleSharedViewModel: Factory { self { self.kmp().get(SampleSharedViewModel.self) } } var sampleNextViewModel: Factory { self { self.kmp().get(SampleNextViewModel.self) } } + var sampleTabBarViewModel: Factory { self { self.kmp().get(SampleTabBarViewModel.self) } } } diff --git a/ios/Application/MainFlowController.swift b/ios/Application/MainFlowController.swift index 0db7206e..28bd61f0 100644 --- a/ios/Application/MainFlowController.swift +++ b/ios/Application/MainFlowController.swift @@ -3,6 +3,8 @@ // Copyright © 2019 Matee. All rights reserved. // +import DependencyInjection +import Factory import KMPShared import Sample import SampleComposeMultiplatform @@ -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 + return UIHostingController(rootView: SampleTabBarView(flowController: self)) +// return UIHostingController(rootView: TestScreen()) +// let main = UITabBarController() +// main.viewControllers = [setupSampleTab(), setupSampleSharedViewModelTab(), setupSampleComposeMultiplatformTab(), setupSampleComposeNavigationTab()] +// return main } private func setupSampleTab() -> UINavigationController { diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/NativeScaffoldObservable.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/NativeScaffoldObservable.swift new file mode 100644 index 00000000..7e041c79 --- /dev/null +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/NativeScaffoldObservable.swift @@ -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 { + + 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 + } + + func updateToolbar(toolbar: Toolbar?) { + self.toolbar = toolbar + } + + func updateTabs(tabs: [TabItem]) { + self.tabs = tabs + } + + func updateSelectedTab(selectedTabPosition: Int32) { + self.selectedTab = selectedTabPosition + } +} diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/NativeScaffoldView.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/NativeScaffoldView.swift new file mode 100644 index 00000000..68ccec18 --- /dev/null +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/NativeScaffoldView.swift @@ -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) } + } + } + .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 + } + } + } +} + +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) + } +} diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift index 37393885..fcbe47f6 100644 --- a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/ScreenWithBottomBarView.swift @@ -9,30 +9,64 @@ import UIToolkit struct ScreenWithBottomBarView: View { @ObservedObject var observable: ScreenWithPlatformSpecificBottomBarObservable - + init(observable: ScreenWithPlatformSpecificBottomBarObservable) { self.observable = observable } - + var body: some View { - TabView(selection: $observable.selectedTab) { - ForEach(observable.tabs, id: \.position) { tab in - Group { - if #available(iOS 26.0, *) { - ComposeViewController { tab.content } - .ignoresSafeArea(.all, edges: .bottom) - } else { - ComposeViewController { tab.content } + 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) + // } + // } + } + .tabItem { + if let uiImage = tab.icon.toUIImage() { + Image(uiImage: uiImage) + } + Text(tab.title) + } + .tag(tab.position) + } } - .tabItem { - if let uiImage = tab.icon.toUIImage() { - Image(uiImage: uiImage) + .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) } - Text(tab.title) } - .tag(tab.position) } + } else { + // Fallback on earlier versions } } } diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/SwiftUIFactory.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/SwiftUIFactory.swift new file mode 100644 index 00000000..2737fe17 --- /dev/null +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/PlatformSpecificView/SwiftUIFactory.swift @@ -0,0 +1,33 @@ +// +// Created by Julia Jakubcova on 25/11/2024 +// Copyright © 2024 Matee. All rights reserved. +// + +import KMPShared +import SwiftUI + +public class SwiftUIViewFactory: ComposeViewFactory { + + public init() {} + + public func createNativeScaffold( + toolbar: Toolbar?, + tabs: [TabItem], + selectedTabPosition: Int32, + onTabSelected: @escaping (KotlinInt) -> Void, + content: @escaping (KotlinInt?) -> UIViewController + ) -> KotlinPair { + let observable = NativeScaffoldObservable( + toolbar: toolbar, + tabs: tabs, + selectedTab: selectedTabPosition, + onSelectedTabChanged: { onTabSelected(KotlinInt(value: $0)) }, + content: { position in content(position.map { KotlinInt(value: $0) }) } + ) + let viewController: UIViewController = UIHostingController( + rootView: NativeScaffoldView(observable: observable) + ) + + return KotlinPair(first: viewController, second: observable) + } +} diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/SampleTabBarView.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/SampleTabBarView.swift new file mode 100644 index 00000000..ff174f4a --- /dev/null +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/SampleTabBarView.swift @@ -0,0 +1,37 @@ +// +// Created by Julia Jakubcova on 06/10/2025 +// Copyright © 2025 Matee. All rights reserved. +// + +import Factory +import KMPShared +import SwiftUI +import UIToolkit + +public struct SampleTabBarView: View { + + private weak var flowController: FlowController? + + @State private var toastData: ToastData? + + public init(flowController: FlowController?) { + self.flowController = flowController + } + + public var body: some View { + ComposeViewController { + SampleTabBarViewController( + onEvent: { event in + switch onEnum(of: event) { + case .else: + print("Unknown event: \(event)") + } + }, + factory: SwiftUIViewFactory() + ) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea() + .toastView($toastData) + } +} diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/TestScreen.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/TestScreen.swift new file mode 100644 index 00000000..918f8275 --- /dev/null +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/TestScreen.swift @@ -0,0 +1,62 @@ +// +// Created by Julia Jakubcova on 08/10/2025 +// Copyright © 2025 Matee. All rights reserved. +// + +import SwiftUI + +public struct TestScreen: View { + + @State private var selectedTab = 0 + + public init() {} + + public var body: some View { + if #available(iOS 16.0, *) { + NavigationStack { + TabView(selection: $selectedTab) { + ForEach((0...2), id: \.self) { tab in + ScrollView { + ForEach((0...20), id: \.self) { index in + Text("This is item number \(index)") + .font(.largeTitle) + Text("And this is it's body") + } + } + .tabItem { + Image(systemName: "xmark") + Text("Tab \(tab)") + } + .tag(tab) + + } + } + .navigationTitle("Title") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.hidden, for: .navigationBar) + .toolbar { + ToolbarItemGroup(placement: .topBarLeading) { + Button("First", systemImage: "search") { + + } + .tint(.red) + } + + ToolbarItemGroup(placement: .topBarTrailing) { + Button("Second", systemImage: "person") { + + } + } + + ToolbarItemGroup(placement: .topBarTrailing) { + Button("Second", systemImage: "person") { + + } + } + } + } + } else { + // Fallback on earlier versions + } + } +} diff --git a/ios/PresentationLayer/UIToolkit/Sources/UIToolkit/Extensions/Color+Extensions.swift b/ios/PresentationLayer/UIToolkit/Sources/UIToolkit/Extensions/Color+Extensions.swift new file mode 100644 index 00000000..2e6c6cbe --- /dev/null +++ b/ios/PresentationLayer/UIToolkit/Sources/UIToolkit/Extensions/Color+Extensions.swift @@ -0,0 +1,14 @@ +// +// Created by Julia Jakubcova on 08/10/2025 +// Copyright © 2025 Matee. All rights reserved. +// + +import KMPShared +import SwiftUI +import UIKit + +public extension Color { + init(kmpColor: KMPShared.NativeColor) { + self = Color(uiColor: kmpColor.toUIColor()) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 548f1582..6a8d4a99 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,3 +31,4 @@ include(":shared:sample") include(":shared:samplesharedviewmodel") include(":shared:samplecomposemultiplatform") include(":shared:samplecomposenavigation") +include(":shared:sampletabbar") diff --git a/shared/core/build.gradle.kts b/shared/core/build.gradle.kts index 70cf1c74..866c10ad 100644 --- a/shared/core/build.gradle.kts +++ b/shared/core/build.gradle.kts @@ -39,4 +39,6 @@ dependencies { commonMainApi(project(":shared:samplesharedviewmodel")) commonMainApi(project(":shared:samplecomposemultiplatform")) commonMainApi(project(":shared:samplecomposenavigation")) + commonMainApi(project(":shared:sampletabbar")) + commonMainApi(project(":shared:sampletabbar")) } diff --git a/shared/core/src/commonMain/kotlin/kmp/shared/core/di/Module.kt b/shared/core/src/commonMain/kotlin/kmp/shared/core/di/Module.kt index 7ac69604..805ff76b 100644 --- a/shared/core/src/commonMain/kotlin/kmp/shared/core/di/Module.kt +++ b/shared/core/src/commonMain/kotlin/kmp/shared/core/di/Module.kt @@ -7,6 +7,7 @@ import kmp.shared.sample.di.sampleModule import kmp.shared.samplecomposemultiplatform.di.sampleComposeMultiplatformModule import kmp.shared.samplecomposenavigation.di.sampleComposeNavigationModule import kmp.shared.samplesharedviewmodel.di.sampleSharedViewModelModule +import kmp.shared.sampletabbar.di.sampleTabBarModule import org.koin.core.KoinApplication import org.koin.core.context.startKoin import org.koin.dsl.KoinAppDeclaration @@ -22,6 +23,7 @@ fun initKoin(appDeclaration: KoinAppDeclaration = {}): KoinApplication { analyticsModule, sampleComposeMultiplatformModule, sampleComposeNavigationModule, + sampleTabBarModule, ) } diff --git a/shared/samplecomposemultiplatform/build.gradle.kts b/shared/samplecomposemultiplatform/build.gradle.kts index 52abcff1..a5549b6d 100644 --- a/shared/samplecomposemultiplatform/build.gradle.kts +++ b/shared/samplecomposemultiplatform/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { commonMainImplementation(compose.runtime) commonMainImplementation(compose.foundation) commonMainImplementation(compose.material) + commonMainImplementation(compose.material3) @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) commonMainImplementation(compose.components.resources) commonMainImplementation(compose.components.uiToolingPreview) diff --git a/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt b/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt index 2e0a8501..2f0631f7 100644 --- a/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt +++ b/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt @@ -2,11 +2,15 @@ package kmp.shared.samplecomposemultiplatform.presentation.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.BottomNavigation -import androidx.compose.material.BottomNavigationItem -import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -24,11 +28,11 @@ actual fun ScreenWithPlatformSpecificBottomBar( selectedTab?.content?.invoke() } - BottomNavigation( - modifier = Modifier.fillMaxWidth(), + NavigationBar( + modifier = Modifier.fillMaxWidth() ) { tabs.forEach { tab -> - BottomNavigationItem( + NavigationBarItem( selected = selectedTabPosition == tab.position, icon = { Icon( @@ -40,6 +44,9 @@ actual fun ScreenWithPlatformSpecificBottomBar( label = { Text(tab.title) }, + modifier = Modifier.padding( + bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() + ), ) } } diff --git a/shared/sampletabbar/build.gradle.kts b/shared/sampletabbar/build.gradle.kts new file mode 100644 index 00000000..0b53678f --- /dev/null +++ b/shared/sampletabbar/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + alias(libs.plugins.mateeStarter.kmm.library) + alias(libs.plugins.jetbrains.compose) + alias(libs.plugins.jetbrains.compose.compiler) +} + +android { + namespace = "kmp.shared.sampletabbar" +} + +ktlint { + filter { + exclude { entry -> + entry.file.toString().contains("generated") + } + } +} + +dependencies { + commonMainImplementation(project(":shared:base")) + commonMainImplementation(project(":shared:sample")) + commonMainImplementation(project(":shared:samplesharedviewmodel")) + commonMainImplementation(project(":shared:samplecomposemultiplatform")) + + commonMainImplementation(compose.runtime) + commonMainImplementation(compose.foundation) + commonMainImplementation(compose.material) + commonMainImplementation(compose.material3) + @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) + commonMainImplementation(compose.components.resources) + commonMainImplementation(compose.components.uiToolingPreview) + ktlintRuleset(libs.ktlint.composeRules) + + commonMainImplementation(libs.coil) + androidMainImplementation(libs.coil.okhttp) + iosMainImplementation(libs.coil.ktor) + commonMainImplementation(libs.haze) + commonMainImplementation(libs.haze.materials) +} diff --git a/shared/sampletabbar/src/androidMain/kotlin/kmp/shared/sampletabbar/presentation/ui/ComposeViewFactory.kt b/shared/sampletabbar/src/androidMain/kotlin/kmp/shared/sampletabbar/presentation/ui/ComposeViewFactory.kt new file mode 100644 index 00000000..e5f983a1 --- /dev/null +++ b/shared/sampletabbar/src/androidMain/kotlin/kmp/shared/sampletabbar/presentation/ui/ComposeViewFactory.kt @@ -0,0 +1,4 @@ +package kmp.shared.sampletabbar.presentation.ui + +// Originally generated by Touchlab's [compose-swift-bridge] +actual interface ComposeViewFactory diff --git a/shared/sampletabbar/src/androidMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.android.kt b/shared/sampletabbar/src/androidMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.android.kt new file mode 100644 index 00000000..6f6e1e1e --- /dev/null +++ b/shared/sampletabbar/src/androidMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.android.kt @@ -0,0 +1,116 @@ +package kmp.shared.sampletabbar.presentation.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +actual fun NativeScaffold( + modifier: Modifier, + toolbar: Toolbar?, + tabs: List, + selectedTabPosition: Int, + onTabSelected: (Int) -> Unit, + content: @Composable (currentTabPosition: Int?, contentPadding: PaddingValues) -> Unit, +) { + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + Scaffold( + modifier = modifier, + topBar = { + toolbar?.let { + CenterAlignedTopAppBar( + title = { + toolbar.title?.let { title -> + Text(title) + } + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.Transparent, + scrolledContainerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f), + ), + navigationIcon = { + toolbar.buttons + .filter { it.position == ToolbarButtonPosition.Leading } + .forEach { button -> + IconButton(button.onClick) { + Icon( + painter = painterResource(button.icon.drawableResId), + contentDescription = button.description, + tint = button.tint?.composeColor + ?: LocalContentColor.current, + ) + } + } + }, + actions = { + toolbar.buttons + .filter { it.position == ToolbarButtonPosition.Trailing } + .forEach { button -> + IconButton(button.onClick) { + Icon( + painter = painterResource(button.icon.drawableResId), + contentDescription = button.description, + tint = button.tint?.composeColor + ?: LocalContentColor.current, + ) + } + } + }, + modifier = Modifier, + ) + } + }, + bottomBar = { + AnimatedVisibility(visible = tabs.isNotEmpty()) { + NavigationBar( + modifier = Modifier.fillMaxWidth(), + ) { + tabs.forEach { tab -> + NavigationBarItem( + selected = selectedTabPosition == tab.position, + icon = { + Icon( + painter = painterResource(tab.icon.drawableResId), + contentDescription = tab.title, + ) + }, + onClick = { onTabSelected(tab.position) }, + label = { + Text(tab.title) + }, + modifier = Modifier.padding( + bottom = WindowInsets.systemBars.asPaddingValues() + .calculateBottomPadding(), + ), + ) + } + } + } + }, + ) { contentPadding -> + content(selectedTabPosition.takeIf { tabs.isNotEmpty() }, contentPadding) + } +} + +actual class NativeColor actual constructor(val composeColor: Color) diff --git a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/di/Module.kt b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/di/Module.kt new file mode 100644 index 00000000..1e5e4428 --- /dev/null +++ b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/di/Module.kt @@ -0,0 +1,9 @@ +package kmp.shared.sampletabbar.di + +import kmp.shared.sampletabbar.presentation.vm.SampleTabBarViewModel +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val sampleTabBarModule = module { + viewModelOf(::SampleTabBarViewModel) +} \ No newline at end of file diff --git a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/BlurredContainer.kt b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/BlurredContainer.kt new file mode 100644 index 00000000..0a4ed226 --- /dev/null +++ b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/BlurredContainer.kt @@ -0,0 +1,89 @@ +package kmp.shared.sampletabbar.presentation.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.LinearEasing +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import dev.chrisbanes.haze.HazeProgressive +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials +import dev.chrisbanes.haze.rememberHazeState + +@OptIn(ExperimentalHazeMaterialsApi::class) +@Composable +fun BlurredContainer( + top: Dp = 0.dp, + bottom: Dp = 0.dp, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + val hazeState = rememberHazeState() + + Box(modifier = modifier.fillMaxSize()) { + Box(modifier = Modifier.hazeSource(hazeState)) { + content() + } + + Column { + val backgroundColor = MaterialTheme.colorScheme.background + AnimatedVisibility(visible = top > 0.dp) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(top) + .hazeEffect( + hazeState, + style = HazeMaterials.ultraThin(containerColor = Color.White), + ) { + progressive = HazeProgressive.verticalGradient( + easing = LinearEasing, + startIntensity = 0.2f, + endIntensity = 0f, + ) + } + .background( + Brush.verticalGradient( + listOf( + backgroundColor.copy(alpha = 0.7f), + Color.Transparent, + ), + ), + ), + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + AnimatedVisibility(visible = bottom > 0.dp) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(bottom) + .background( + Brush.verticalGradient( + listOf( + Color.Transparent, + backgroundColor.copy(alpha = 0.7f), + ), + ), + ), + ) + } + } + } +} \ No newline at end of file diff --git a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/ComposeViewFactory.kt b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/ComposeViewFactory.kt new file mode 100644 index 00000000..dd96f3bf --- /dev/null +++ b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/ComposeViewFactory.kt @@ -0,0 +1,7 @@ +package kmp.shared.sampletabbar.presentation.ui + +// Originally generated by Touchlab's [compose-swift-bridge] +typealias ViewFactory = + ComposeViewFactory + +expect interface ComposeViewFactory diff --git a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/LocalViewFactory.kt b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/LocalViewFactory.kt new file mode 100644 index 00000000..70ecc0fa --- /dev/null +++ b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/LocalViewFactory.kt @@ -0,0 +1,14 @@ +package kmp.shared.sampletabbar.presentation.ui + +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.compositionLocalOf + +// Originally generated by Touchlab's [compose-swift-bridge] +val LocalViewFactory: ProvidableCompositionLocal = + compositionLocalOf( + defaultFactory = { + error( + """You have to provide LocalViewFactory""", + ) + }, + ) diff --git a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.kt b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.kt new file mode 100644 index 00000000..14a27b08 --- /dev/null +++ b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.kt @@ -0,0 +1,46 @@ +package kmp.shared.sampletabbar.presentation.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import dev.icerock.moko.resources.ImageResource + + +@Composable +expect fun NativeScaffold( + modifier: Modifier = Modifier, + toolbar: Toolbar? = null, + tabs: List = emptyList(), + selectedTabPosition: Int = 0, + onTabSelected: (Int) -> Unit = {}, + content: @Composable (currentTabPosition: Int?, contentPadding: PaddingValues) -> Unit, +) + +data class Toolbar( + val title: String? = null, + val buttons: List = emptyList(), +) + +data class ToolbarButtonData( + val icon: ImageResource, + val description: String? = null, + val position: ToolbarButtonPosition = ToolbarButtonPosition.Trailing, + val tint: NativeColor? = null, + val onClick: () -> Unit, +) + +expect class NativeColor { + constructor(composeColor: Color) +} + +enum class ToolbarButtonPosition { + Leading, + Trailing, +} + +data class TabItem( + val title: String, + val icon: ImageResource, + val position: Int, +) \ No newline at end of file diff --git a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/SampleTabBarScreen.kt b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/SampleTabBarScreen.kt new file mode 100644 index 00000000..13aedd0c --- /dev/null +++ b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/SampleTabBarScreen.kt @@ -0,0 +1,149 @@ +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.icerock.moko.resources.ImageResource +import kmp.shared.base.MR +import kmp.shared.sampletabbar.presentation.ui.NativeColor +import kmp.shared.sampletabbar.presentation.ui.NativeScaffold +import kmp.shared.sampletabbar.presentation.ui.TabItem +import kmp.shared.sampletabbar.presentation.ui.Toolbar +import kmp.shared.sampletabbar.presentation.ui.ToolbarButtonData +import kmp.shared.sampletabbar.presentation.ui.ToolbarButtonPosition +import kmp.shared.sampletabbar.presentation.vm.SampleTab +import kmp.shared.sampletabbar.presentation.vm.SampleTabBarIntent +import kmp.shared.sampletabbar.presentation.vm.SampleTabBarState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SampleTabBarScreen( + state: SampleTabBarState, + onIntent: (SampleTabBarIntent) -> Unit, + modifier: Modifier = Modifier, +) { + NativeScaffold( + toolbar = Toolbar( + title = SampleTab.entries[state.selectedTabPosition].title, + buttons = listOf( + ToolbarButtonData( + icon = MR.images.search, + description = "Search", + onClick = { println("XXX Search clicked") }, + position = ToolbarButtonPosition.Leading, + tint = NativeColor(Color.Red), + ), + ToolbarButtonData( + icon = MR.images.person, + description = "Person", + onClick = { println("XXX Person clicked") }, + position = ToolbarButtonPosition.Trailing, + ), + ToolbarButtonData( + icon = MR.images.home, + description = "Home", + onClick = { println("XXX Home clicked") }, + position = ToolbarButtonPosition.Trailing, + tint = NativeColor(MaterialTheme.colorScheme.primary), + ), + ) + ), + tabs = state.tabs.map { tab -> + TabItem( + position = tab.ordinal, + title = tab.title, + icon = tab.icon, + ) + }, + selectedTabPosition = state.selectedTabPosition, + onTabSelected = { index -> + onIntent(SampleTabBarIntent.OnTabSelected(index)) + }, + modifier = modifier.fillMaxSize(), + ) { position, innerPadding -> + if (position != null) { + val tab = SampleTab.entries[position] + when (tab) { + SampleTab.Numbers -> ScrollableContent(contentPadding = innerPadding) { index -> + Text( + text = index.toString(), + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } + + SampleTab.Sentences -> ScrollableContent(contentPadding = innerPadding) { index -> + Text( + text = "This is item number $index", + style = MaterialTheme.typography.headlineMedium, + ) + Text( + text = "And this is it's body", + style = MaterialTheme.typography.bodySmall, + ) + } + + SampleTab.Images -> ScrollableContent(contentPadding = innerPadding) { index -> + AsyncImage( + model = "https://picsum.photos/id/$index/200/300", + contentDescription = null, + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.FillWidth, + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) +@Composable +private fun ScrollableContent( + contentPadding: PaddingValues = PaddingValues(), + modifier: Modifier = Modifier, + itemCount: Int = 20, + itemContent: @Composable ColumnScope.(index: Int) -> Unit, +) { + LazyColumn( + modifier = modifier, + contentPadding = contentPadding, + ) { + items(count = itemCount) { index -> + Card( + modifier = Modifier.fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + ) { + Column(modifier = Modifier.padding(16.dp)) { + itemContent(index) + } + } + } + } +} + +private val SampleTab.title: String + get() = when (this) { + SampleTab.Numbers -> "Numbers" + SampleTab.Sentences -> "Sentences" + SampleTab.Images -> "Images" + } + +private val SampleTab.icon: ImageResource + get() = when (this) { + SampleTab.Numbers -> MR.images.home + SampleTab.Sentences -> MR.images.search + SampleTab.Images -> MR.images.person + } diff --git a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/SampleTheme.kt b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/SampleTheme.kt new file mode 100644 index 00000000..2584b260 --- /dev/null +++ b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/SampleTheme.kt @@ -0,0 +1,97 @@ +package kmp.shared.sampletabbar.presentation.ui + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import kmp.shared.samplecomposemultiplatform.presentation.common.Values + +// https://coolors.co/f5ab00-b8a422-9aa133-7b9d44-d95700-e0e0e0-f0f0f0 +val LightColorScheme = lightColorScheme( + primary = Color(0xFFF5AB00), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFB8A422), + onPrimaryContainer = Color(0xFFFFFFFF), + + secondary = Color(0xFF7B9D44), + onSecondary = Color(0xFF000000), + secondaryContainer = Color(0xFF9AA133), + onSecondaryContainer = Color(0xFF000000), + + background = Color(0xFFF0F0F0), + onBackground = Color(0xFF000000), + + surface = Color(0xFFE0E0E0), + onSurface = Color(0xFF000000), + + error = Color(0xFFD95700), + onError = Color(0xFF000000), + + // Optional M3 roles — fill with best approximations: + surfaceVariant = Color(0xFFE0E0E0), + onSurfaceVariant = Color(0xFF000000), + outline = Color(0xFF9AA133), + outlineVariant = Color(0xFFB8A422), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFF000000), + inverseOnSurface = Color(0xFFE0E0E0), + inversePrimary = Color(0xFFB8A422), +) + +// https://coolors.co/f5ab00-b8a422-9aa133-7b9d44-d95700-1f1f1f-141414 +val DarkColorScheme = darkColorScheme( + primary = Color(0xFFF5AB00), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFB8A422), + onPrimaryContainer = Color(0xFFFFFFFF), + + secondary = Color(0xFF7B9D44), + onSecondary = Color(0xFF000000), + secondaryContainer = Color(0xFF9AA133), + onSecondaryContainer = Color(0xFF000000), + + background = Color(0xFF141414), + onBackground = Color(0xFFFFFFFF), + + surface = Color(0xFF1F1F1F), + onSurface = Color(0xFFFFFFFF), + + error = Color(0xFFD95700), + onError = Color(0xFFFFFFFF), + + // Optional roles + surfaceVariant = Color(0xFF1F1F1F), + onSurfaceVariant = Color(0xFFFFFFFF), + outline = Color(0xFF9AA133), + outlineVariant = Color(0xFFB8A422), + scrim = Color(0xFF000000), + inverseSurface = Color(0xFFE0E0E0), + inverseOnSurface = Color(0xFF000000), + inversePrimary = Color(0xFFB8A422), +) + +val typography = Typography( + // Define typohraphy +) + +val shapes = Shapes( + small = RoundedCornerShape(Values.Radius.large), + medium = RoundedCornerShape(Values.Radius.medium), + large = RoundedCornerShape(Values.Radius.small), +) + +@Composable +fun SampleTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { + val colors = if (darkTheme) DarkColorScheme else LightColorScheme + MaterialTheme( + colorScheme = colors, + typography = typography, + shapes = shapes, + content = content, + ) +} diff --git a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/vm/SampleTabBarViewModel.kt b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/vm/SampleTabBarViewModel.kt new file mode 100644 index 00000000..77aae1f9 --- /dev/null +++ b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/vm/SampleTabBarViewModel.kt @@ -0,0 +1,43 @@ +package kmp.shared.sampletabbar.presentation.vm + +import kmp.shared.samplesharedviewmodel.base.vm.BaseViewModel +import kmp.shared.samplesharedviewmodel.base.vm.VmEvent +import kmp.shared.samplesharedviewmodel.base.vm.VmIntent +import kmp.shared.samplesharedviewmodel.base.vm.VmState + +class SampleTabBarViewModel : + BaseViewModel(SampleTabBarState()) { + + override suspend fun applyIntent(intent: SampleTabBarIntent) { + when (intent) { + SampleTabBarIntent.OnAppeared -> onAppeared() + is SampleTabBarIntent.OnTabSelected -> update { copy(selectedTabPosition = intent.index) } + } + } + + private suspend fun onAppeared() { + if (state.value.tabs.isEmpty()) { + update { + copy( + tabs = SampleTab.entries, + ) + } + } + } +} + +data class SampleTabBarState( + val tabs: List = emptyList(), + val selectedTabPosition: Int = 0, +) : VmState + +sealed interface SampleTabBarIntent : VmIntent { + data object OnAppeared : SampleTabBarIntent + data class OnTabSelected(val index: Int) : SampleTabBarIntent +} + +sealed interface SampleTabBarEvent : VmEvent + +enum class SampleTab { + Numbers, Sentences, Images, +} diff --git a/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/SampleTabBarViewController.kt b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/SampleTabBarViewController.kt new file mode 100644 index 00000000..5fd23680 --- /dev/null +++ b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/SampleTabBarViewController.kt @@ -0,0 +1,47 @@ +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.ComposeUIViewController +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kmp.shared.sampletabbar.presentation.ui.LocalViewFactory +import kmp.shared.sampletabbar.presentation.ui.SampleTheme +import kmp.shared.sampletabbar.presentation.ui.ViewFactory +import kmp.shared.sampletabbar.presentation.vm.SampleTabBarEvent +import kmp.shared.sampletabbar.presentation.vm.SampleTabBarIntent +import kmp.shared.sampletabbar.presentation.vm.SampleTabBarViewModel +import kotlinx.coroutines.flow.collectLatest +import org.koin.compose.viewmodel.koinViewModel +import platform.UIKit.UIViewController + +@Suppress("Unused", "FunctionName") +fun SampleTabBarViewController( + onEvent: (SampleTabBarEvent) -> Unit, + factory: ViewFactory, +): UIViewController { + return ComposeUIViewController { + val viewModel: SampleTabBarViewModel = koinViewModel() + val state by viewModel.state.collectAsStateWithLifecycle() + + LaunchedEffect(viewModel) { + viewModel.onIntent(SampleTabBarIntent.OnAppeared) + } + + LaunchedEffect(viewModel) { + viewModel.events.collectLatest { event -> + onEvent(event) + } + } + CompositionLocalProvider( + LocalViewFactory provides factory, + ) { + SampleTheme { + SampleTabBarScreen( + state = state, + onIntent = viewModel::onIntent, + modifier = Modifier, + ) + } + } + } +} diff --git a/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/ComposeViewFactory.kt b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/ComposeViewFactory.kt new file mode 100644 index 00000000..9769b160 --- /dev/null +++ b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/ComposeViewFactory.kt @@ -0,0 +1,15 @@ +package kmp.shared.sampletabbar.presentation.ui + +import platform.UIKit.UIViewController + +// Originally generated by Touchlab's [compose-swift-bridge] +actual interface ComposeViewFactory { + + fun createNativeScaffold( + toolbar: Toolbar?, + tabs: List, + selectedTabPosition: Int, + onTabSelected: (Int) -> Unit, + content: (Int?) -> UIViewController, + ): Pair +} diff --git a/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt new file mode 100644 index 00000000..dbe47e80 --- /dev/null +++ b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt @@ -0,0 +1,99 @@ +package kmp.shared.sampletabbar.presentation.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.safeContent +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.viewinterop.UIKitView +import androidx.compose.ui.viewinterop.UIKitViewController +import androidx.compose.ui.window.ComposeUIViewController +import androidx.lifecycle.viewmodel.compose.viewModel +import kmp.shared.samplecomposemultiplatform.presentation.ui.NativeViewHolderViewModel +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.useContents +import platform.Foundation.NSProcessInfo +import platform.UIKit.UIColor +import platform.UIKit.UIViewController +import kotlin.random.Random + +@OptIn(ExperimentalForeignApi::class, ExperimentalComposeUiApi::class) +@Composable +actual fun NativeScaffold( + modifier: Modifier, + toolbar: Toolbar?, + tabs: List, + selectedTabPosition: Int, + onTabSelected: (Int) -> Unit, + content: @Composable (currentTabPosition: Int?, contentPadding: PaddingValues) -> Unit, +) { + val factory = LocalViewFactory.current + val key = rememberSaveable { Random.nextInt().toString(16) } + + fun contentMapper(position: Int?): UIViewController = + ComposeUIViewController(configure = { opaque = false }) { + CompositionLocalProvider( + LocalViewFactory provides factory, + ) { + SampleTheme { + val showBlur = NSProcessInfo.processInfo.operatingSystemVersion.useContents { + this.majorVersion >= 26 + } + if (showBlur) { + BlurredContainer( + top = WindowInsets.safeContent.asPaddingValues().calculateTopPadding(), + bottom = WindowInsets.safeContent.asPaddingValues() + .calculateBottomPadding(), + modifier = Modifier.fillMaxSize(), + ) { + content(position, WindowInsets.safeContent.asPaddingValues()) + } + } else { + content(position, WindowInsets.safeContent.asPaddingValues()) + } + } + } + } + + val viewModel = viewModel(key = key) { + NativeViewHolderViewModel { + factory.createNativeScaffold( + toolbar = toolbar, + tabs = tabs, + selectedTabPosition = selectedTabPosition, + onTabSelected = onTabSelected, + content = ::contentMapper, + ) + } + } + val delegate = remember(viewModel) { viewModel.delegate } + val view = remember(viewModel) { viewModel.view } + + remember(toolbar) { delegate.updateToolbar(toolbar) } + remember(tabs) { delegate.updateTabs(tabs) } + remember(selectedTabPosition) { delegate.updateSelectedTab(selectedTabPosition) } + + UIKitViewController( + modifier = modifier, + factory = { view }, + update = { _ -> }, + ) +} + +actual class NativeColor actual constructor(private val composeColor: Color) { + + fun toUIColor(): UIColor = + UIColor.colorWithRed( + red = composeColor.red.toDouble(), + green = composeColor.green.toDouble(), + blue = composeColor.blue.toDouble(), + alpha = composeColor.alpha.toDouble(), + ) +} \ No newline at end of file diff --git a/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffoldDelegate.kt b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffoldDelegate.kt new file mode 100644 index 00000000..b496f085 --- /dev/null +++ b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffoldDelegate.kt @@ -0,0 +1,7 @@ +package kmp.shared.sampletabbar.presentation.ui + +interface NativeScaffoldDelegate { + fun updateToolbar(toolbar: Toolbar?) + fun updateTabs(tabs: List) + fun updateSelectedTab(selectedTabPosition: Int) +} \ No newline at end of file From cb2afed346bd2e6fd9c6dce2252f583ac7353fa5 Mon Sep 17 00:00:00 2001 From: JuliaJakubcova Date: Thu, 9 Oct 2025 08:54:10 +0200 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=A9=B9=20[KMM]=20Fix=20lint=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/ScreenWithPlatformSpecificBottomBar.android.kt | 4 ++-- .../commonMain/kotlin/kmp/shared/sampletabbar/di/Module.kt | 2 +- .../shared/sampletabbar/presentation/ui/BlurredContainer.kt | 3 +-- .../kmp/shared/sampletabbar/presentation/ui/NativeScaffold.kt | 3 +-- .../shared/sampletabbar/presentation/ui/SampleTabBarScreen.kt | 2 +- .../shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt | 3 +-- .../sampletabbar/presentation/ui/NativeScaffoldDelegate.kt | 2 +- 7 files changed, 8 insertions(+), 11 deletions(-) diff --git a/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt b/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt index 2f0631f7..330368cc 100644 --- a/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt +++ b/shared/samplecomposemultiplatform/src/androidMain/kotlin/kmp/shared/samplecomposemultiplatform/presentation/ui/ScreenWithPlatformSpecificBottomBar.android.kt @@ -29,7 +29,7 @@ actual fun ScreenWithPlatformSpecificBottomBar( } NavigationBar( - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) { tabs.forEach { tab -> NavigationBarItem( @@ -45,7 +45,7 @@ actual fun ScreenWithPlatformSpecificBottomBar( Text(tab.title) }, modifier = Modifier.padding( - bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() + bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(), ), ) } diff --git a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/di/Module.kt b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/di/Module.kt index 1e5e4428..78c2bc56 100644 --- a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/di/Module.kt +++ b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/di/Module.kt @@ -6,4 +6,4 @@ import org.koin.dsl.module val sampleTabBarModule = module { viewModelOf(::SampleTabBarViewModel) -} \ No newline at end of file +} diff --git a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/BlurredContainer.kt b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/BlurredContainer.kt index 0a4ed226..4c229dec 100644 --- a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/BlurredContainer.kt +++ b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/BlurredContainer.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.HazeProgressive -import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi @@ -86,4 +85,4 @@ fun BlurredContainer( } } } -} \ No newline at end of file +} diff --git a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.kt b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.kt index 14a27b08..10747fbd 100644 --- a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.kt +++ b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.kt @@ -6,7 +6,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.ImageResource - @Composable expect fun NativeScaffold( modifier: Modifier = Modifier, @@ -43,4 +42,4 @@ data class TabItem( val title: String, val icon: ImageResource, val position: Int, -) \ No newline at end of file +) diff --git a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/SampleTabBarScreen.kt b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/SampleTabBarScreen.kt index 13aedd0c..15458d92 100644 --- a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/SampleTabBarScreen.kt +++ b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/SampleTabBarScreen.kt @@ -60,7 +60,7 @@ fun SampleTabBarScreen( position = ToolbarButtonPosition.Trailing, tint = NativeColor(MaterialTheme.colorScheme.primary), ), - ) + ), ), tabs = state.tabs.map { tab -> TabItem( diff --git a/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt index dbe47e80..9aaf9e93 100644 --- a/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt +++ b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt @@ -12,7 +12,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.viewinterop.UIKitView import androidx.compose.ui.viewinterop.UIKitViewController import androidx.compose.ui.window.ComposeUIViewController import androidx.lifecycle.viewmodel.compose.viewModel @@ -96,4 +95,4 @@ actual class NativeColor actual constructor(private val composeColor: Color) { blue = composeColor.blue.toDouble(), alpha = composeColor.alpha.toDouble(), ) -} \ No newline at end of file +} diff --git a/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffoldDelegate.kt b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffoldDelegate.kt index b496f085..1086ba95 100644 --- a/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffoldDelegate.kt +++ b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffoldDelegate.kt @@ -4,4 +4,4 @@ interface NativeScaffoldDelegate { fun updateToolbar(toolbar: Toolbar?) fun updateTabs(tabs: List) fun updateSelectedTab(selectedTabPosition: Int) -} \ No newline at end of file +} From 26a9bcb96d395cad178cb70c27e23c27d5ca6126 Mon Sep 17 00:00:00 2001 From: JuliaJakubcova Date: Thu, 9 Oct 2025 12:31:44 +0200 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=94=A5=20[KMM]=20Remove=20unnecessary?= =?UTF-8?q?=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt index 9aaf9e93..6c70ba14 100644 --- a/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt +++ b/shared/sampletabbar/src/iosMain/kotlin/kmp/shared/sampletabbar/presentation/ui/NativeScaffold.ios.kt @@ -37,7 +37,7 @@ actual fun NativeScaffold( val key = rememberSaveable { Random.nextInt().toString(16) } fun contentMapper(position: Int?): UIViewController = - ComposeUIViewController(configure = { opaque = false }) { + ComposeUIViewController { CompositionLocalProvider( LocalViewFactory provides factory, ) { From 3c45aeb3fb6e3811dbec8e4e32cdacee5cea6f37 Mon Sep 17 00:00:00 2001 From: JuliaJakubcova Date: Thu, 9 Oct 2025 13:36:30 +0200 Subject: [PATCH 8/9] =?UTF-8?q?=E2=9C=85=20[KMM]=20Fix=20consist=20test=20?= =?UTF-8?q?for=20modifiers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/konsistTest/android/compose/ComposeTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/core/src/androidUnitTest/kotlin/konsistTest/android/compose/ComposeTest.kt b/shared/core/src/androidUnitTest/kotlin/konsistTest/android/compose/ComposeTest.kt index f49d34cd..66b08440 100644 --- a/shared/core/src/androidUnitTest/kotlin/konsistTest/android/compose/ComposeTest.kt +++ b/shared/core/src/androidUnitTest/kotlin/konsistTest/android/compose/ComposeTest.kt @@ -29,7 +29,7 @@ internal class ComposeTest { .withType { it.name == "Modifier" } .let { params -> params.size == 1 && - params.all { param -> param.hasDefaultValue() && param.name == "modifier" } + params.all { param -> (param.hasDefaultValue() || fn.hasActualModifier) && param.name == "modifier" } } } } From 2eedaa84e60848155b546268a0f3f812c860db76 Mon Sep 17 00:00:00 2001 From: JuliaJakubcova Date: Tue, 14 Oct 2025 10:36:05 +0200 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=92=84=20[KMM]=20Tweak=20blur=20and?= =?UTF-8?q?=20gradient=20opacity=20for=20liquid=20glass=20toolbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TestScreen.swift | 40 +++++++++++++++---- .../presentation/ui/BlurredContainer.kt | 9 ++--- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/TestScreen.swift b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/TestScreen.swift index 918f8275..0042a848 100644 --- a/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/TestScreen.swift +++ b/ios/PresentationLayer/SampleComposeMultiplatform/Sources/SampleComposeMultiplatform/TestScreen.swift @@ -18,14 +18,23 @@ public struct TestScreen: View { ForEach((0...2), id: \.self) { tab in ScrollView { ForEach((0...20), id: \.self) { index in - Text("This is item number \(index)") - .font(.largeTitle) - Text("And this is it's body") + VStack(alignment: .leading) { + Text("This is item number \(index)") + .font(.largeTitle) + .padding([.top, .horizontal]) + + Text("And this is it's body") + .padding([.bottom, .horizontal]) + } + .frame(maxWidth: .infinity) + .background(.gray.opacity(0.5)) + .clipShape(.rect(cornerRadius: 8)) + .padding(.horizontal) } } .tabItem { - Image(systemName: "xmark") - Text("Tab \(tab)") + Image(systemName: image(index: tab)) + Text(text(index: tab)) } .tag(tab) @@ -36,7 +45,7 @@ public struct TestScreen: View { .toolbarBackground(.hidden, for: .navigationBar) .toolbar { ToolbarItemGroup(placement: .topBarLeading) { - Button("First", systemImage: "search") { + Button("First", systemImage: "magnifyingglass") { } .tint(.red) @@ -49,9 +58,10 @@ public struct TestScreen: View { } ToolbarItemGroup(placement: .topBarTrailing) { - Button("Second", systemImage: "person") { + Button("Second", systemImage: "house") { } + .tint(.yellow) } } } @@ -59,4 +69,20 @@ public struct TestScreen: View { // Fallback on earlier versions } } + + func image(index: Int) -> String { + switch index { + case 0: return "house" + case 1: return "magnifyingglass" + default: return "person" + } + } + + func text(index: Int) -> String { + switch index { + case 0: return "Numbers" + case 1: return "Sentences" + default: return "Images" + } + } } diff --git a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/BlurredContainer.kt b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/BlurredContainer.kt index 4c229dec..692edc65 100644 --- a/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/BlurredContainer.kt +++ b/shared/sampletabbar/src/commonMain/kotlin/kmp/shared/sampletabbar/presentation/ui/BlurredContainer.kt @@ -51,16 +51,15 @@ fun BlurredContainer( ) { progressive = HazeProgressive.verticalGradient( easing = LinearEasing, - startIntensity = 0.2f, + startIntensity = 0.3f, endIntensity = 0f, ) } .background( Brush.verticalGradient( - listOf( - backgroundColor.copy(alpha = 0.7f), - Color.Transparent, - ), + 0f to backgroundColor.copy(alpha = 0.85f), + 0.8f to backgroundColor.copy(alpha = 0.75f), + 1f to Color.Transparent, ), ), )