From 665a79e1e36957821665b7d77b38e4e3fca3316a Mon Sep 17 00:00:00 2001 From: Abe White Date: Sun, 24 Sep 2023 22:58:46 -0500 Subject: [PATCH] Navigation fixes --- .../SkipUI/SkipUI/Containers/Navigation.swift | 201 +++++++++--------- .../SkipUI/SkipUI/Containers/TabView.swift | 81 ++++--- 2 files changed, 152 insertions(+), 130 deletions(-) diff --git a/Sources/SkipUI/SkipUI/Containers/Navigation.swift b/Sources/SkipUI/SkipUI/Containers/Navigation.swift index 0b1b1403..713058b0 100644 --- a/Sources/SkipUI/SkipUI/Containers/Navigation.swift +++ b/Sources/SkipUI/SkipUI/Containers/Navigation.swift @@ -47,6 +47,108 @@ import androidx.navigation.compose.rememberNavController import kotlinx.coroutines.delay #endif +public struct NavigationStack : View where Root: View { + private let root: Root + + public init(@ViewBuilder root: () -> Root) { + self.root = root() + } + + @available(*, unavailable) + public init(path: Any, @ViewBuilder root: () -> Root) { + self.root = root() + } + + #if SKIP + @Composable public override func ComposeContent(context: ComposeContext) { + let preferenceUpdates = remember { mutableStateOf(0) } + let _ = preferenceUpdates.value // Read so that it can trigger recompose on change + let preferencesDidChange = { preferenceUpdates.value += 1 } + + // Have to use rememberSaveable for e.g. a nav stack in each tab + let destinations = rememberSaveable(stateSaver: context.stateSaver as! Saver) { mutableStateOf(NavigationDestinationsPreferenceKey.defaultValue) } + let navController = rememberNavController() + let navigator = rememberSaveable(stateSaver: context.stateSaver as! Saver) { mutableStateOf(Navigator(navController: navController, destinations: destinations.value)) } + navigator.value.didCompose(navController: navController, destinations: destinations.value) + + // SKIP INSERT: val providedNavigator = LocalNavigator provides navigator.value + CompositionLocalProvider(providedNavigator) { + NavHost(navController: navController, startDestination: Navigator.rootRoute, modifier: context.modifier) { + composable(route: Navigator.rootRoute) { entry in + if let state = navigator.value.state(for: entry) { + let entryContext = context.content(stateSaver: state.stateSaver) + ComposeEntry(navController: navController, destinations: destinations, destinationsDidChange: preferencesDidChange, isRoot: true, context: entryContext) { context in + root.Compose(context: context) + } + } + } + for destinationIndex in 0.., destinationsDidChange: () -> Void, isRoot: Bool, context: ComposeContext, content: @Composable (ComposeContext) -> Void) { + let preferenceUpdates = remember { mutableStateOf(0) } + let _ = preferenceUpdates.value // Read so that it can trigger recompose on change + + let title = rememberSaveable(stateSaver: context.stateSaver as! Saver) { mutableStateOf(NavigationTitlePreferenceKey.defaultValue) } + + // We place the top bar scaffold within each entry rather than at the navigation controller level. There isn't a fluid animation + // between navigation bar states on Android, and it is simpler to only hoist navigation bar preferences to this level + let scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) + Scaffold( + modifier: Modifier.nestedScroll(scrollBehavior.nestedScrollConnection).then(context.modifier), + topBar: { + guard !isRoot || !title.value.isEmpty else { + return + } + MediumTopAppBar( + colors: TopAppBarDefaults.topAppBarColors( + containerColor: Color.systemBackground.colorImpl(), + titleContentColor: MaterialTheme.colorScheme.onSurface + ), + title: { + androidx.compose.material3.Text(title.value, maxLines: 1, overflow: TextOverflow.Ellipsis) + }, + navigationIcon: { + if !isRoot { + IconButton(onClick: { navController.popBackStack() }) { + Icon(imageVector: Icons.Filled.ArrowBack, contentDescription: "Back") + } + } + }, + scrollBehavior: scrollBehavior + ) + } + ) { padding in + // Provide our current destinations as the initial value so that we don't forget previous destinations. Only one navigation entry + // will be composed, and we want to retain destinations from previous entries + let destinationsPreference = Preference(key: NavigationDestinationsPreferenceKey.self, initialValue: destinations.value, update: { destinations.value = $0 }, didChange: destinationsDidChange) + let titlePreference = Preference(key: NavigationTitlePreferenceKey.self, update: { title.value = $0 }, didChange: { preferenceUpdates.value += 1 }) + PreferenceValues.shared.collectPreferences([destinationsPreference, titlePreference]) { + Box(modifier: Modifier.padding(padding).fillMaxSize(), contentAlignment: androidx.compose.ui.Alignment.Center) { + content(context.content()) + } + } + } + } + #else + public var body: some View { + stubView() + } + #endif +} + #if SKIP typealias NavigationDestinations = Dictionary struct NavigationDestination { @@ -187,105 +289,6 @@ class Navigator { let LocalNavigator: ProvidableCompositionLocal = compositionLocalOf { nil as Navigator? } #endif -public struct NavigationStack : View where Root: View { - private let root: Root - - public init(@ViewBuilder root: () -> Root) { - self.root = root() - } - - @available(*, unavailable) - public init(path: Any, @ViewBuilder root: () -> Root) { - self.root = root() - } - - #if SKIP - @Composable public override func ComposeContent(context: ComposeContext) { - let preferenceUpdates = remember { mutableStateOf(0) } - let _ = preferenceUpdates.value // Read so that it can trigger recompose on change - let preferencesDidChange = { preferenceUpdates.value += 1 } - - // Have to use rememberSaveable for e.g. a nav stack in each tab - let destinations = rememberSaveable(stateSaver: context.stateSaver as! Saver) { mutableStateOf(NavigationDestinationsPreferenceKey.defaultValue) } - let navController = rememberNavController() - let navigator = rememberSaveable(stateSaver: context.stateSaver as! Saver) { mutableStateOf(Navigator(navController: navController, destinations: destinations.value)) } - navigator.value.didCompose(navController: navController, destinations: destinations.value) - - // SKIP INSERT: val providedNavigator = LocalNavigator provides navigator.value - CompositionLocalProvider(providedNavigator) { - NavHost(navController: navController, startDestination: Navigator.rootRoute, modifier: context.modifier) { - composable(route: Navigator.rootRoute) { entry in - if let state = navigator.value.state(for: entry) { - let entryContext = context.content(stateSaver: state.stateSaver) - ComposeEntry(navController: navController, destinations: destinations, destinationsDidChange: preferencesDidChange, isRoot: true, context: entryContext) { context in - root.Compose(context: context) - } - } - } - for destinationIndex in 0.., destinationsDidChange: () -> Void, isRoot: Bool, context: ComposeContext, content: @Composable (ComposeContext) -> Void) { - let preferenceUpdates = remember { mutableStateOf(0) } - let _ = preferenceUpdates.value // Read so that it can trigger recompose on change - - let title = rememberSaveable(stateSaver: context.stateSaver as! Saver) { mutableStateOf(NavigationTitlePreferenceKey.defaultValue) } - - // We place the top bar scaffold within each entry rather than at the navigation controller level. There isn't a fluid animation - // between navigation bar states on Android, and it is simpler to only hoist navigation bar preferences to this level - 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(title.value, maxLines: 1, overflow: TextOverflow.Ellipsis) - }, - navigationIcon: { - if !isRoot { - IconButton(onClick: { navController.popBackStack() }) { - Icon(imageVector: Icons.Filled.ArrowBack, contentDescription: "Back") - } - } - }, - scrollBehavior: scrollBehavior - ) - } - ) { padding in - // Provide our current destinations as the initial value so that we don't forget previous destinations. Only one navigation entry - // will be composed, and we want to retain destinations from previous entries - let destinationsPreference = Preference(key: NavigationDestinationsPreferenceKey.self, initialValue: destinations.value, update: { destinations.value = $0 }, didChange: destinationsDidChange) - let titlePreference = Preference(key: NavigationTitlePreferenceKey.self, update: { title.value = $0 }, didChange: { preferenceUpdates.value += 1 }) - PreferenceValues.shared.collectPreferences([destinationsPreference, titlePreference]) { - Box(modifier: Modifier.padding(padding).fillMaxSize().then(context.modifier), contentAlignment: androidx.compose.ui.Alignment.Center) { - content(context.content()) - } - } - } - } - #else - public var body: some View { - stubView() - } - #endif -} - extension View { public func navigationDestination(for data: D.Type, @ViewBuilder destination: @escaping (D) -> V) -> some View where D: Any, V : View { #if SKIP diff --git a/Sources/SkipUI/SkipUI/Containers/TabView.swift b/Sources/SkipUI/SkipUI/Containers/TabView.swift index 4f4ab215..1da24769 100644 --- a/Sources/SkipUI/SkipUI/Containers/TabView.swift +++ b/Sources/SkipUI/SkipUI/Containers/TabView.swift @@ -2,43 +2,62 @@ // under the terms of the GNU Lesser General Public License 3.0 // as published by the Free Software Foundation https://fsf.org -public struct TabView : View where Content : View { - let content: Content +// TODO: Process for use in SkipUI - public init(@ViewBuilder content: () -> Content) { - self.content = content() - } +#if !SKIP - @available(*, unavailable) - public init(selection: Binding?, @ViewBuilder content: () -> Content) { - self.content = content() - } +/// A view that switches between multiple child views using interactive user +/// interface elements. +/// +/// To create a user interface with tabs, place views in a `TabView` and apply +/// the ``View/tabItem(_:)`` modifier to the contents of each tab. On iOS, you +/// can also use one of the badge modifiers, like ``View/badge(_:)-84e43``, to +/// assign a badge to each of the tabs. +/// +/// The following example creates a tab view with three tabs, each presenting a +/// custom child view. The first tab has a numeric badge and the third has a +/// string badge. +/// +/// TabView { +/// ReceivedView() +/// .badge(2) +/// .tabItem { +/// Label("Received", systemImage: "tray.and.arrow.down.fill") +/// } +/// SentView() +/// .tabItem { +/// Label("Sent", systemImage: "tray.and.arrow.up.fill") +/// } +/// AccountView() +/// .badge("!") +/// .tabItem { +/// Label("Account", systemImage: "person.crop.circle.fill") +/// } +/// } +/// +/// ![A tab bar with three tabs, each with an icon image and a text label. +/// The first and third tabs have badges.](TabView-1) +/// +/// Use a ``Label`` for each tab item, or optionally a ``Text``, an ``Image``, +/// or an image followed by text. Passing any other type of view results in a +/// visible but empty tab item. +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 7.0, *) +public struct TabView : View where SelectionValue : Hashable, Content : View { - #if SKIP -// @Composable public override func ComposeContent(context: ComposeContext) { -// // Check to see if we've initialized our tab items from our content views .tabItem modifiers. If we haven't, -// // compose the content view with a custom composer that will capture the items. Note that 'content' is just a reference to -// // the enclosing ComposeView, so a custom composer is the only way to receive a reference to our actual content views. -// let rememberedItems = remember { mutableStateOf(nil) } -// if rememberedItems.value == nil { -// root.Compose(context: context.content(composer: { view, context in -// rememberedItems.value = (view as? NavigationDestinationView)?.destinations ?? [:] -// })) -// } -// let items = rememberedItems.value ?? arrayOf() -// -// //~~~ -// } - #else - public var body: some View { - stubView() - } - #endif + /// Creates an instance that selects from content associated with + /// `Selection` values. + public init(selection: Binding?, @ViewBuilder content: () -> Content) { fatalError() } + + @MainActor public var body: some View { get { return stubView() } } + +// public typealias Body = some View } -#if !SKIP +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 7.0, *) +extension TabView where SelectionValue == Int { -// TODO: Process for use in SkipUI + public init(@ViewBuilder content: () -> Content) { } +} /// A specification for the appearance and interaction of a `TabView`. @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)