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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions android/app/src/main/kotlin/kmp/android/ui/Root.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.navigation3.ui.NavDisplay
import kmp.android.shared.navigation.LocalNavigator
import kmp.android.samplefeature.navigation.SampleFeatureNavKey
import kmp.android.samplefeature.navigation.sampleFeatureEntries
import kmp.android.shared.navigation.mateePopTransitionSpec
import kmp.android.shared.navigation.mateePredictivePopTransitionSpec
import kmp.android.shared.navigation.mateeTransitionSpec
import kmp.android.shared.navigation.rememberNavigator
import kmp.shared.base.presentation.navigation.LocalNavigator
import kmp.shared.base.presentation.navigation.rememberNavigator

@Composable
fun Root(modifier: Modifier = Modifier) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package kmp.android.samplefeature.navigation

import android.widget.Toast
import androidx.compose.ui.platform.LocalContext
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import kmp.android.samplefeature.ui.SampleFeatureMainRoute
import kmp.android.shared.navigation.Navigator
import kmp.shared.base.presentation.navigation.Navigator
import kmp.shared.samplefeature.presentation.ui.SampleFeatureRoute

fun EntryProviderScope<NavKey>.sampleFeatureEntries(
navigator: Navigator,
) {
entry<SampleFeatureNavKey.Home> {
SampleFeatureMainRoute(
val context = LocalContext.current
SampleFeatureRoute(
onShowMessage = { message ->
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
// Use provided `navigator` to navigate to other screens
)
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package plugin

import extensions.apply
import extensions.compose
import extensions.debugImplementation
import extensions.ktlintRuleset
import extensions.libs
Expand Down Expand Up @@ -34,8 +33,15 @@ class KmpLibraryComposeConventionPlugin : Plugin<Project> {
implementation(libs.jetbrains.compose.material3)
implementation(libs.jetbrains.compose.uiUtil)
implementation(libs.jetbrains.compose.uiToolingPreview)
implementation(libs.mokoResources.compose)
implementation(libs.haze)
implementation(libs.haze.materials)
ktlintRuleset(libs.ktlint.composeRules)
}
androidMain.dependencies {
implementation(libs.navigation3.runtime)
implementation(libs.lifecycle.viewModel.navigation3)
}
}
}

Expand All @@ -45,4 +51,4 @@ class KmpLibraryComposeConventionPlugin : Plugin<Project> {
}
}
}
}
}
6 changes: 5 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ koin = "4.2.1"
androidXCore = "1.18.0"
lifecycle = "2.10.0"
paging = "3.4.2"
jetbrains-compose = "1.11.0-beta01" # Note: version 1.11.X fixes iOS lags
jetbrains-compose = "1.11.0-alpha03" # Note: version 1.11.X fixes iOS lags, `haze` supports alpha03
jetbrains-compose-material3 = "1.9.0"
activity = "1.13.0"
navigation3 = "1.0.1"
Expand All @@ -38,6 +38,7 @@ skie = "0.10.11"
firebase = "22.5.0"
googleServices = "4.4.4"
sentiary = "1.0.1"
haze = "1.7.2"

[libraries]
# Kotlin
Expand Down Expand Up @@ -125,6 +126,9 @@ firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx", ve
ktlint-gradlePlugin = { module = "org.jlleitschuh.gradle.ktlint:org.jlleitschuh.gradle.ktlint.gradle.plugin", version.ref = "ktLint" }
# Sentiary
sentiary-gradlePlugin = { module = "com.sentiary:gradle-plugin", version.ref = "sentiary" }
# Haze
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }

[bundles]
settings = [
Expand Down
8 changes: 0 additions & 8 deletions ios/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {

// MARK: Setup appearance
private func setupAppearance() {
// Navigation bar
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = UIColor(AppTheme.Colors.navBarBackground)
appearance.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor(AppTheme.Colors.navBarTitle)]
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
UINavigationBar.appearance().tintColor = UIColor(AppTheme.Colors.navBarTitle)

// Tab bar
UITabBar.appearance().tintColor = UIColor(AppTheme.Colors.primaryColor)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//
// Created by Lukáš Matuška on 14.04.2026
// Copyright © 2026 Matee. All rights reserved.
//

import Foundation
import KMPShared

extension SampleFeatureViewModel: @retroactive ObservableObject { }
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,26 @@ import SwiftUI
import UIToolkit

public struct SampleFeatureView: View {

@State private var toastData: ToastData?
@Injected(\.sampleFeatureViewModel) private var viewModel: SampleFeatureViewModel
@InjectedObject(\.sampleFeatureViewModel) private var viewModel: SampleFeatureViewModel

public init() {}

public var body: some View {
ManagedNavigationStack { _ in
ComposeViewController {
SampleFeatureMainScreenViewController(viewModel: viewModel)
SampleFeatureMainScreenViewController(
viewModel: viewModel,
onShowMessage: { message in
toastData = ToastData(message, hideAfter: 2)
}
)
}
.ignoresSafeArea()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.bindViewModel(viewModel)
}
.tint(AppTheme.Colors.navBarTitle) // Back button color
.toastView($toastData)
.bindViewModel(viewModel, onEvent: onEvent)
}

private func onEvent(_ event: SampleFeatureEvent) {
switch onEnum(of: event) {
case .showMessage(let data):
toastData = ToastData(data.message, hideAfter: 2)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@

import Foundation
import KMPShared
import SwiftUI

public extension StringResource {

func toLocalized() -> String {
return self.desc().localized()
self.desc().localized()
}
}

public extension StringDesc {
func toLocalized() -> String {
localized()
}
}

public extension Image {
init(_ resource: KMPShared.ImageResource) {
self.init(resource.assetImageName, bundle: resource.bundle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,33 @@ public extension View {
@MainActor
public extension View {
func bindViewModel<S: VmState & Sendable, I: VmIntent, E: VmEvent & Sendable>(
_ viewModel: BaseScopedViewModel<S, I, E>,
onEvent: @escaping (E) -> Void
_ viewModel: BaseScopedViewModel<S, I, E>
) -> some View {
self
.task {
// Make sure that onViewAppeared will be called after event subcsription
Task {
viewModel.onViewAppeared()
}
for await event in viewModel.events {
onEvent(event)
}
}
.modifier(ToolbarBindingModifier(viewModel: viewModel))
.onDismiss {
viewModel.clearScope()
}
}
}

private struct ToolbarBindingModifier<S: VmState & Sendable, I: VmIntent, E: VmEvent & Sendable>: ViewModifier {
let viewModel: BaseScopedViewModel<S, I, E>

@State private var toolbar: Toolbar?

init(viewModel: BaseScopedViewModel<S, I, E>) {
self.viewModel = viewModel
_toolbar = State(initialValue: viewModel.toolbar.value)
}

func body(content: Content) -> some View {
content
.task {
for await toolbar in viewModel.toolbar {
self.toolbar = toolbar
}
}
.toolbar(toolbar)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// Created by Lukáš Matuška on 14.04.2026
// Copyright © 2026 Matee. All rights reserved.
//

import SwiftUI

extension View {
@ViewBuilder
func navigationBarTitleColor(_ color: Color?) -> some View {
if let color {
self.background(NavBarTitleColorSetter(color: UIColor(color)))
} else {
self
}
}
}

private struct NavBarTitleColorSetter: UIViewControllerRepresentable {
let color: UIColor

func makeUIViewController(context: Context) -> NavBarColorVC {
NavBarColorVC(color: color)
}

func updateUIViewController(_ vc: NavBarColorVC, context: Context) {
vc.color = color
vc.applyColor()
}
}

private final class NavBarColorVC: UIViewController {
var color: UIColor

init(color: UIColor) {
self.color = color
super.init(nibName: nil, bundle: nil)
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
applyColor()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
applyColor()
}

func applyColor() {
var host = parent
while let current = host, current.navigationController == nil {
host = current.parent
}

guard let host, let navBar = host.navigationController?.navigationBar else {
return
}

let appearance = UINavigationBarAppearance(barAppearance: navBar.standardAppearance)
appearance.titleTextAttributes[.foregroundColor] = color
host.navigationItem.standardAppearance = appearance
host.navigationItem.scrollEdgeAppearance = appearance
}
}
Loading
Loading