Skip to content

Commit

Permalink
Snapshot work on navigation and preferences
Browse files Browse the repository at this point in the history
  • Loading branch information
aabewhite committed Sep 23, 2023
1 parent 3e07c72 commit b849ef6
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 70 deletions.
7 changes: 7 additions & 0 deletions Sources/SkipUI/SkipUI/Color/Color.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
#endif

public struct Color: View, Hashable, Sendable {
Expand Down Expand Up @@ -153,6 +155,11 @@ public struct Color: View, Hashable, Sendable {
MaterialTheme.colorScheme.onSecondary
})

static let systemBackground = Color(colorImpl: {
// Matches Android's default bottom bar color
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
})

/// Returns the given color value based on whether the view is in dark mode or light mode
@Composable private static func color(light: Int64, dark: Int64) -> androidx.compose.ui.graphics.Color {
// TODO: EnvironmentValues.shared.colorMode == .dark ? dark : light
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Compose/ComposeModifierView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import androidx.compose.runtime.Composable

/// Recognized modifier roles.
public enum ComposeModifierRole { // View.strippingModifiers becomes public in Kotlin and exposes this type, so it must be public
public enum ComposeModifierRole {
case accessibility
case spacing
case unspecified
Expand Down
3 changes: 2 additions & 1 deletion Sources/SkipUI/SkipUI/Containers/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.zIndex
Expand Down Expand Up @@ -155,7 +156,7 @@ public struct List<SelectionValue, Content> : View where SelectionValue: Hashabl
if style == .plain {
return MaterialTheme.colorScheme.surface
} else {
return MaterialTheme.colorScheme.surfaceVariant
return Color.systemBackground.colorImpl()
}
}
#else
Expand Down
99 changes: 77 additions & 22 deletions Sources/SkipUI/SkipUI/Containers/Navigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,37 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.style.TextOverflow
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.navArgument
import androidx.navigation.compose.NavHost
Expand All @@ -33,7 +49,7 @@ import kotlinx.coroutines.delay
#if SKIP
typealias NavigationDestinations = Dictionary<Any.Type, (Any) -> View>

@androidx.compose.runtime.Stable
@Stable
class Navigator {
/// Route for the root of the navigation stack.
static let rootRoute = "navigationroot"
Expand All @@ -43,7 +59,7 @@ class Navigator {
return String(describing: targetType) + "/" + valueString
}

private let navController: NavController
var navController: NavHostController
private let destinations: NavigationDestinations

private var backStackState: [String: BackStackState] = [:]
Expand All @@ -62,7 +78,7 @@ class Navigator {
}
}

init(navController: NavController, destinations: NavigationDestinations) {
init(navController: NavHostController, destinations: NavigationDestinations) {
self.navController = navController
self.destinations = destinations
}
Expand Down Expand Up @@ -148,45 +164,82 @@ public struct NavigationStack<Root> : View where Root: View {
}

#if SKIP
// SKIP INSERT: @OptIn(ExperimentalMaterial3Api::class)
@Composable public override func ComposeContent(context: ComposeContext) {
// Check to see if we've initialized our destinations from our root view's .navigationDestination modifiers. If we haven't,
// compose the root view with a custom composer that will capture the destinations. Note that 'root' is just a reference to
// the enclosing ComposeView, so a custom composer is the only way to receive a reference to our actual root view
// SKIP INSERT: var destinations by remember { mutableStateOf<NavigationDestinations?>(null) }
// the enclosing ComposeView, so a custom composer is the only way to receive a reference to our actual root view.
// Have to use rememberSaveable for e.g. a nav stack in each tab
// SKIP INSERT: var destinations by rememberSaveable(stateSaver = context.stateSaver as Saver<NavigationDestinations?, Any>) { mutableStateOf<NavigationDestinations?>(null) }
if destinations == nil {
root.Compose(context.content(composer: { view, context in
root.Compose(context: context.content(composer: { view, context in
destinations = (view as? NavigationDestinationView)?.destinations ?? [:]
}))
}

let navController = rememberNavController()
// SKIP INSERT: val navigator by remember { mutableStateOf(Navigator(navController = navController, destinations = destinations ?: dictionaryOf())) }
// SKIP INSERT: val navigator by rememberSaveable(stateSaver = context.stateSaver as Saver<Navigator, Any>) { mutableStateOf(Navigator(navController = navController, destinations = destinations ?: dictionaryOf())) }
navigator.navController = navController // May change on recompose
navigator.syncState()

// SKIP INSERT: val providedNavigator = LocalNavigator provides navigator
CompositionLocalProvider(providedNavigator) {
NavHost(navController: navController, startDestination: Navigator.rootRoute, modifier: context.modifier) {
composable(route: Navigator.rootRoute) { entry in
if let state = navigator.state(for: entry) {
androidx.compose.foundation.layout.Box {
root.Compose(context: ComposeContext(stateSaver: state.stateSaver))
}
let scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
Scaffold(
modifier: Modifier.nestedScroll(scrollBehavior.nestedScrollConnection).then(context.modifier),
topBar: {
MediumTopAppBar(
colors: TopAppBarDefaults.topAppBarColors(
containerColor: Color.systemBackground.colorImpl(),
titleContentColor: MaterialTheme.colorScheme.onSurface
),
title: {
// androidx.compose.material3.Text("Cities", maxLines: 1, overflow: TextOverflow.Ellipsis)
},
navigationIcon: NavigationIconComposable(navController: navController),
scrollBehavior: scrollBehavior
)
}
) { padding in
ComposeNavHost(navController: navController, navigator: navigator, destinations: destinations, context: context.content(modifier: Modifier.padding(padding)))
}
}
}

@Composable private func ComposeNavHost(navController: NavHostController, navigator: Navigator, destinations: NavigationDestinations?, context: ComposeContext) {
NavHost(navController: navController, startDestination: Navigator.rootRoute, modifier: context.modifier) {
composable(route: Navigator.rootRoute) { entry in
if let state = navigator.state(for: entry) {
Box(modifier: Modifier.fillMaxSize(), contentAlignment: androidx.compose.ui.Alignment.Center) {
root.Compose(context: ComposeContext(stateSaver: state.stateSaver))
}
}
if let destinations {
for (targetType, viewBuilder) in destinations {
composable(route: Navigator.route(for: targetType, valueString: "{identifier}"), arguments = listOf(navArgument("identifier") { type = NavType.StringType })) { entry in
if let state = navigator.state(for: entry), let targetValue = state.targetValue {
androidx.compose.foundation.layout.Box {
viewBuilder(targetValue).Compose(context: ComposeContext(stateSaver: state.stateSaver))
}
}
if let destinations {
for (targetType, viewBuilder) in destinations {
composable(route: Navigator.route(for: targetType, valueString: "{identifier}"), arguments: listOf(navArgument("identifier") { type = NavType.StringType })) { entry in
if let state = navigator.state(for: entry), let targetValue = state.targetValue {
Box(modifier: Modifier.fillMaxSize(), contentAlignment: androidx.compose.ui.Alignment.Center) {
viewBuilder(targetValue).Compose(context: ComposeContext(stateSaver: state.stateSaver))
}
}
}
}
}
}
}

@Composable private func NavigationIconComposable(navController: NavHostController) -> (@Composable () -> Void) {
// SKIP INSERT: val entryList by navController.currentBackStack.collectAsState()
guard entryList.size > 2 else { // NavGraph + root entry
return {}
}
return {
IconButton(onClick: { navController.popBackStack() }) {
Icon(imageVector: Icons.Filled.ArrowBack, contentDescription: "Back")
}
}
}
#else
public var body: some View {
stubView()
Expand Down Expand Up @@ -215,6 +268,8 @@ struct NavigationDestinationView: View {
let destinations: NavigationDestinations

init(view: any View, dataType: Any.Type, @ViewBuilder destination: @escaping (Any) -> any View) {
// Prevent copying the view
// SKIP REPLACE: this.view = view
self.view = view
if let navigationDestination = view as? NavigationDestinationView {
var combinedDestinations = navigationDestination.destinations
Expand All @@ -226,7 +281,7 @@ struct NavigationDestinationView: View {
}

@Composable override func ComposeContent(context: ComposeContext) {
view.Compose(context)
view.Compose(context: context)
}
}
#endif
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Controls/Toggle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public struct Toggle<Label> : View where Label : View {
} else {
let contentContext = context.content()
Row(modifier: context.modifier, verticalAlignment: androidx.compose.ui.Alignment.CenterVertically) {
label.Compose(contentContext)
label.Compose(context: contentContext)
androidx.compose.foundation.layout.Spacer(modifier: Modifier.weight(Float(1.0)))
Switch(checked: isOn.wrappedValue, onCheckedChange: { isOn.wrappedValue = $0 })
}
Expand Down
57 changes: 47 additions & 10 deletions Sources/SkipUI/SkipUI/Environment/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import Observation

#if SKIP
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.Modifier
import kotlin.reflect.full.companionObjectInstance
#endif
Expand All @@ -32,8 +35,8 @@ public class EnvironmentValues {
#if SKIP
// We type erase all keys and values. The alternative would be to reify these functions.

let compositionLocals: MutableMap<Any, androidx.compose.runtime.ProvidableCompositionLocal<Any>> = mutableMapOf()
let lastSetValues: MutableMap<androidx.compose.runtime.ProvidableCompositionLocal<Any>, Any> = mutableMapOf()
let compositionLocals: MutableMap<Any, ProvidableCompositionLocal<Any>> = mutableMapOf()
let lastSetValues: MutableMap<ProvidableCompositionLocal<Any>, Any> = mutableMapOf()

// SKIP DECLARE: @Composable operator fun <Key, Value> get(key: KClass<Key>): Value where Key: EnvironmentKey<Value>
public func get(key: AnyHashable) -> Any {
Expand All @@ -60,7 +63,7 @@ public class EnvironmentValues {
element
}.toTypedArray()
lastSetValues.clear()
androidx.compose.runtime.CompositionLocalProvider(*provided) {
CompositionLocalProvider(*provided) {
content()
}
}
Expand All @@ -75,27 +78,61 @@ public class EnvironmentValues {
lastSetValues[compositionLocal] = value
}

public func valueCompositionLocal(key: Any.Type) -> androidx.compose.runtime.ProvidableCompositionLocal<Any> {
public func valueCompositionLocal(key: Any.Type) -> ProvidableCompositionLocal<Any> {
// SKIP INSERT: val defaultValue = { (key.companionObjectInstance as EnvironmentKeyCompanion<*>).defaultValue }
return compositionLocal(key: key, defaultValue: defaultValue)
}

public func objectCompositionLocal(type: Any.Type) -> androidx.compose.runtime.ProvidableCompositionLocal<Any> {
public func objectCompositionLocal(type: Any.Type) -> ProvidableCompositionLocal<Any> {
return compositionLocal(key: type, defaultValue: { nil })
}

func compositionLocal(key: AnyHashable, defaultValue: () -> Any?) -> androidx.compose.runtime.ProvidableCompositionLocal<Any> {
func compositionLocal(key: AnyHashable, defaultValue: () -> Any?) -> ProvidableCompositionLocal<Any> {
if let value = compositionLocals[key] {
return value
}
let value = androidx.compose.runtime.compositionLocalOf { defaultValue() ?? Unit }
let value = compositionLocalOf { defaultValue() ?? Unit }
compositionLocals[key] = value
return value
}

// SKIP DECLARE: @Composable internal fun <Key, Value> preference(keyType: KClass<Key>): Preference<Key, Value>? where Key: PreferenceKey<Value>, Value: Any
func preference<Key, Value>(keyType: AnyHashable) -> Preference<Key, Value>? {
return compositionLocals[keyType]?.current as? Preference<Key, Value>
}

// SKIP DECLARE: @Composable internal fun <Key, Value> setPreference(preference: Preference<Key, Value>) where Key: PreferenceKey<Value>, Value: Any
func collectPreferences<Key, Value>(_ preference: Preference<Key, Value>) {
// do below for each key...
// begin collecting each
// execute composable
// end collecting each?
if let value = compositionLocals[preference.keyType] {
value.current = preference
} else {
let value = compositionLocalOf { preference }
compositionLocals[key] = value
}
}
/// Set environment values.
///
/// - Seealso: ``View/environment(_:)``
/// - Warning: Setting environment values should only be done within the `execute` block of this function.
@Composable func setValues(_ execute: @Composable (EnvironmentValues) -> Void, in content: @Composable () -> Void) {
// Set the values in EnvironmentValues to keep any user-defined setter logic in place, then retrieve and clear the last set values
execute(self)
let provided = lastSetValues.map { entry in
// SKIP INSERT: val element = entry.key provides entry.value
element
}.toTypedArray()
lastSetValues.clear()
CompositionLocalProvider(*provided) {
content()
}
}
#endif
}


extension View {
#if SKIP
// Use inline final func to get reified generic type
Expand All @@ -119,7 +156,7 @@ extension View {
let compositionLocal = EnvironmentValues.shared.objectCompositionLocal(type: type)
let value = object ?? Unit
// SKIP INSERT: val provided = compositionLocal provides value
androidx.compose.runtime.CompositionLocalProvider(provided) { view.Compose(context) }
CompositionLocalProvider(provided) { view.Compose(context: context) }
}
#else
return self
Expand All @@ -133,7 +170,7 @@ extension View {
EnvironmentValues.shared.setValues {
_ in setValue(value)
} in: {
view.Compose(context)
view.Compose(context: context)
}
}
#else
Expand Down
Loading

0 comments on commit b849ef6

Please sign in to comment.