Skip to content

Commit

Permalink
Navigation fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
aabewhite committed Sep 25, 2023
1 parent 86973e4 commit 665a79e
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 130 deletions.
201 changes: 102 additions & 99 deletions Sources/SkipUI/SkipUI/Containers/Navigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,108 @@ import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.delay
#endif

public struct NavigationStack<Root> : 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<NavigationDestinations, Any>) { mutableStateOf(NavigationDestinationsPreferenceKey.defaultValue) }
let navController = rememberNavController()
let navigator = rememberSaveable(stateSaver: context.stateSaver as! Saver<Navigator, Any>) { 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..<Navigator.destinationCount {
composable(route: Navigator.route(for: destinationIndex, valueString: "{identifier}"), arguments: listOf(navArgument("identifier") { type = NavType.StringType })) { entry in
if let state = navigator.value.state(for: entry), let targetValue = state.targetValue {
let entryContext = context.content(stateSaver: state.stateSaver)
ComposeEntry(navController: navController, destinations: destinations, destinationsDidChange: preferencesDidChange, isRoot: false, context: entryContext) { context in
state.destination?(targetValue).Compose(context: context)
}
}
}
}
}
}
}

// SKIP INSERT: @OptIn(ExperimentalMaterial3Api::class)
@Composable private func ComposeEntry(navController: NavHostController, destinations: MutableState<NavigationDestinations>, 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<String, Any>) { 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<NavigationDestinations>(key: NavigationDestinationsPreferenceKey.self, initialValue: destinations.value, update: { destinations.value = $0 }, didChange: destinationsDidChange)
let titlePreference = Preference<String>(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<Any.Type, NavigationDestination>
struct NavigationDestination {
Expand Down Expand Up @@ -187,105 +289,6 @@ class Navigator {
let LocalNavigator: ProvidableCompositionLocal<Navigator?> = compositionLocalOf { nil as Navigator? }
#endif

public struct NavigationStack<Root> : 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<NavigationDestinations, Any>) { mutableStateOf(NavigationDestinationsPreferenceKey.defaultValue) }
let navController = rememberNavController()
let navigator = rememberSaveable(stateSaver: context.stateSaver as! Saver<Navigator, Any>) { 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..<Navigator.destinationCount {
composable(route: Navigator.route(for: destinationIndex, valueString: "{identifier}"), arguments: listOf(navArgument("identifier") { type = NavType.StringType })) { entry in
if let state = navigator.value.state(for: entry), let targetValue = state.targetValue {
let entryContext = context.content(stateSaver: state.stateSaver)
ComposeEntry(navController: navController, destinations: destinations, destinationsDidChange: preferencesDidChange, isRoot: false, context: entryContext) { context in
state.destination?(targetValue).Compose(context: context)
}
}
}
}
}
}
}

// SKIP INSERT: @OptIn(ExperimentalMaterial3Api::class)
@Composable private func ComposeEntry(navController: NavHostController, destinations: MutableState<NavigationDestinations>, 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<String, Any>) { 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<NavigationDestinations>(key: NavigationDestinationsPreferenceKey.self, initialValue: destinations.value, update: { destinations.value = $0 }, didChange: destinationsDidChange)
let titlePreference = Preference<String>(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<D, V>(for data: D.Type, @ViewBuilder destination: @escaping (D) -> V) -> some View where D: Any, V : View {
#if SKIP
Expand Down
81 changes: 50 additions & 31 deletions Sources/SkipUI/SkipUI/Containers/TabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Content> : 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<Any>?, @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<SelectionValue, Content> : 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<NavigationDestinations?>(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<SelectionValue>?, @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, *)
Expand Down

0 comments on commit 665a79e

Please sign in to comment.