How does iOS apps here using navigation? Do you us...
# compose-ios
j
How does iOS apps here using navigation? Do you use TabView and bridge back to compose, or using something like Decompose? I am mostly interested in how solving the navigation host thing equivalent in androidx navigation compose. I want to share more compose code between Android and iOS, but having correct lifecycle and gesture handling as well as proper transitions etc natively per platform.
a
Just as a side note, assuming you are using SeiftUI, using Decompose doesn't mean you lose the native UX. For tab navigation you can just avoid using TabView and put SwiftUI buttons with labels on the bottom bar, and then just display the proper view. For stack navigation it is possible to interop with the native NavigationStack etc. So even back gestures work, and navigation back by multiple screens at a time.
d
Compose Multiplatform does not provide a default way how to make navigation. But you can look at open source projects: https://github.com/arkivanov/Decompose https://github.com/adrielcafe/voyager
j
@Arkadii Ivanov Yeah I want to not use swiftui to be clear. Want to get rid of Apples look, to have one codebase. Reason is start getting ugly and not work interop with everything because limitations with iOS versions for badges or Firebase stuff etc. Mostly I want share all code between Android and iOS. But currently androidx NavHost using lifecycle stuff for reasons. But want create actual/expect bridge same bottom navigation compose and navigation code logic but ONLY sharing transition container. So its more other direction, use more compose and as less native as possible 😁
Using interop NavHost in iOS looks very hard. How to get gestures, transitions etc interop from compose to Swift?
@Dima Avdeev If I can I want avoid another library and androidx navigation compose also just got transitions and they work on KMM and they also support new gesture things. Decompose and such does semi enforce me using their structure? Or can I mix androidx viewmodel and iOS viewmodel inside Decompose with savedstateHandle and nav graph dependency injection with custom classes?
d
Sorry, I don't know exact library limitations. At Compose Multiplatform, we provide lower level elements and don't want to enforce our users to use a concrete library for now.
j
@Dima Avdeev I would say the main limitation is androidx being monorepo to tight coupled with Android SDK over time, making it harder make compose multiplatform mixing that with iOS or other platforms. Nothing wrong with that, but that making it more complex with NavHost to be multiplatform. I only want to get a library or util, which I would develop myself if I had more time everyday, that is agnostic in terms of generic interface, but using detail implementation per platform possible. Most libraries is not interface based in its core allows that or not intended for. Like androidx navigation never intended to support iOS, desktop or Web. The name is androidx πŸ˜› I just wished there was a smart way of creating equivalent of a NavHost ONLY and nothing else, and attach platform specifics myself wired. Like creating UI of tabs, bottomnav, transition animation with compose animation, but move the navgraphs/DI and lifecycles separated from that. It would save A LOT of time and make apps more scalable by decouple the navigation details outside of this, like how to open a new screen or switch the tab content in single view approach. Also iOS tend to couple stuff inside Top bars or what they are called, making stuff very hard. And GOogle tend to make BottomSheets coupled with WIndow/Dialog stuff platform specific not working in iOS and vice versa. Not an easy problem, just thinking how to deal with this in general the best possible way, regardless if using native SwiftUI or Compose for this.
@Arkadii Ivanov "TabView and put SwiftUI buttons with labels on the bottom bar, and then just display the proper view" Any example how to do this? I want to have only the proper view content based on navigation state with transitions + back gestures specifics being possible to either implement myself or inject platform details with expect/actual bridge between Swift and Kotlin. Also curious how to do same with web and desktop, but out of scope for now πŸ™‚
a
Here is an example of tab navigation with Decompose+ SwiftUI - https://github.com/arkivanov/Decompose/blob/master/sample/app-ios/app-ios/RootView.swift
j
My idea is maybe something similar to this sample from Decompose: https://github.com/arkivanov/Decompose/blob/master/sample/shared/shared/src/common[…]ple/shared/customnavigation/DefaultCustomNavigationComponent.kt But no examples how to deal with everything I mentioned πŸ˜› Bridge over viewmodels, gestures, lifecycles per platform into the navigation container as Kotlin interface or such, can I do that somehow? Like send in my own implementation of it πŸ˜›
Also dealing with conditional navigation is problem, like authentication layer. Need to observe state and switch screens based on that when navigate back and forth πŸ˜› Including send over platform specific stuff like Google SIgn in for iOS vs Android, because the GoogleSignIn using Activity in Android and Safari / deeplink in iOS, making the code painfully horrible to deal with.
a
If you run that example, you will find tab navigation, plus a native stack navigation inside the first tab. For a fullscreen stack navigation you can refer to Confetti sample - https://github.com/joreilly/Confetti/blob/main/iosApp/iosApp/ConferenceView.swift
Confetti has an example of authentication, but that's just one of the ways of doing that.
j
@Arkadii Ivanov Its a little tricky as I am not a iOS/Swift developer, so hard try grasp with sub parts I need to extract to boost my compose multiplatform code, ideally move entire UI to be only Compose and nothing with Swift or Android stuff at all.
I have something like this at the moment which I want to replace with COmpose code:
Copy code
struct ContentView: View {
    var body: some View {
        MainScreen {
            TabView {
                HomeScreen()
                    .tabItem {
                        Label("Home", systemImage: "house.fill")
                    }
                TodoScreen()
                    .tabItem {
                        Label("Todo", systemImage: "list.bullet")
                    }.badge(2)
                SearchScreen()
                    .tabItem {
                        Label("Search", systemImage: "magnifyingglass")
                    }
                ProfileScreen()
                    .tabItem {
                        Label("Profile", systemImage: "person")
                    }
            }
        }.onOpenURL { url in
            GIDSignIn.sharedInstance.handle(url)
        }
        .onAppear {
            GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
              // Check if `user` exists; otherwise, do something with `error`
            }
        }
    }
}
But to be able to share this with Android, I also need to replace the NavHost thingy in androidx πŸ˜„
a
Oh, if you want to use Compose for iOS, that's a completely different story! That's also certainly possible!
j
I want to use androidx navigation compose for Android for NavHost but share the topbar, bottombar with Android πŸ˜„
a
Though, compose for iOS is in alpha as of now.
j
I havent been able figure out how to structure my code to make this possible πŸ˜„
It doesnt matter if iOS is alpha, this is test project kind a. Not large scale thing.
My idea was to have like CustomNavHost and using expect vs actual, where Android get androidx navigation compose but for iOS something else. That something else I dont know yet how to acheive πŸ˜„
Can I create TabView but not using TabView, send in SwiftUI back to Compose for the nav-container? I wont want the tabs or top bar from iOS however πŸ˜„
I want the TabContent only πŸ˜„
a
It should be possible to embed Compose views in SwiftUI views, so you can leverage the native navigation as well. But I'm not sure how the ViewModel thing will fit.
j
Does iOS/SwiftUI has something similar like NavHost that is? Like the container replace content and animating transitions, deep linking, gestures etc.
I have my custom ViewModel agnostic between iOS and Android πŸ™‚
So my viewmodel is compatible with NavHost in androidx πŸ˜›
a
SwiftUI has its own navigation techniques, different for stack and tabs.
j
Yeah has SwiftUi decoupled that logic for navigation container?
Or is it one single solution, need to use all or nothing?
a
It should be possible to use TabView for navigation, and embed Compose UI inside each tab. Plus host ViewModels inside each screen.
j
One way of doing this would be to use Compose with my own simple Box, and transition myself between the tabs. But then not getting the gestures, animations etc native solutions being optimized πŸ˜„ I mean a separate implementation for only iOS, equivalent to NavHost container in androidx πŸ™‚
"It should be possible to use TabView for navigation, and embed Compose UI inside each tab. Plus host ViewModels inside each screen." Thats what I have now πŸ˜„
Thats what I want to improve and make better.
Each tab right now is like this:
Copy code
struct ProfileScreen: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> some UIViewController {
        return ScreenProvider.shared.createProfileScreenController()
    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
a
@John O'Reilly perhaps has an example of native SwiftUI navigation + ViewModels + Compose content.
j
Basically I want this but iOS:
Copy code
NavHost(
                    modifier = Modifier.padding(it),
                    navController = navController,
                    startDestination = DirectionRoute.Home.route
                ) {
                    composable(DirectionRoute.Home.route) {
                        HomeScreen(homeViewModel)
                    }
                    composable(DirectionRoute.TodoList.route) {
                        TodoScreen(todoViewModel)
                    }
                    composable(DirectionRoute.Search.route) {
                        SearchScreen(searchViewModel)
                    }
                    composable(DirectionRoute.Profile.route) {
                        ProfileScreen(profileViewModel)
                    }
                }
I mean compose for IOS, not SwiftUI equivalent.
And then the bottom and top bar should be compose multiplatform, as well as navigation logic and sharing states. Extracting navigation graph states I think I can solve per platform, but a little ocmplex for sure πŸ˜„
Nested navigation should work, and navigate to other screens moving away bottom navigation also one reason I want to use androidx navigation πŸ˜›
@Arkadii Ivanov It feels like this is what I was looking for actually: https://github.com/arkivanov/Decompose/blob/master/sample/app-ios/app-ios/DecomposeHelpers/StackView.swift
a
Yeah! I posted links to some samples using StackView above.
j
Right sorry I missed it used custom StackView equivalent to NavHost πŸ™‚
Using it like this:
Copy code
var body: some View {
        StackView(
            stackValue: ObservableValue(component.stack),
            onBack: component.onBackClicked
        ) { child in
            switch child {
            case let child as ConferenceComponentChild.Home: HomeView(child.component)
            case let child as ConferenceComponentChild.SessionDetails: SessionDetailsView(child.component)
            case let child as ConferenceComponentChild.SpeakerDetails: SpeakerDetailsView(child.component)
            default: EmptyView()
            }
        }
    }
Then would be possible sharing the code back into compose with Android, by decouple the navigation controller between iOS and Android, when click on tabs so both can observe same logic πŸ™‚
It would however not deal with dialogs, deeplinks or new screens or nested navigation itself but I think thats semi possible to solve anyway πŸ™‚
a
That implies your are using Decompose for shared navigation, not anything else. And you will have dialogs, deeplinks, etc. all covered
j
Yeah maybe would make sense only sharing navigation layer from Decompose, but can I use only that but the rest being my own stuff, like viewmodels and such? Dont want to inherit Decompose architecture or such.
Copy code
Proper dependency injection (DI) and inversion of control (IoC) via constructor, including but not limited to type-safe arguments.
Lifecycle-aware components
Components in the back stack are not destroyed, they continue working in background without UI
Like this I dont want to have inherited from it πŸ™‚ Then get into same issue again as had with androidx πŸ˜„
Using Decompose for iOS but not for Android that would be, then getting back to same issues again πŸ˜„
The main goal is sharing as much percentage compose/KMM code as possible can and decouple all library specifics with KMM interfaces πŸ™‚
Oh found this https://developer.apple.com/documentation/swiftui/navigationstack Sure I need to provide legacy variant as well, NavigationView or such but this feels doable. Then I only need to provide by own decoupled navigation controller πŸ™‚
@Arkadii Ivanov I ended up in doing a new observable thing in Swift converted as CommonFlow I found, not sure if best but testing and then doing this in Android:
Copy code
class AndroidNavigationController(private val navController: NavController): MyNavigationController {
    override fun navigateTo(direction: DirectionRoute) {
        navController.navigate(direction.route) {
            launchSingleTop = true
            restoreState = true
            popUpTo(navController.graph.findStartDestination().id) {
                saveState = true
            }
        }
    }
    
    override fun currentPath(): CommonFlow<String> {
        return navController.currentBackStackEntryFlow
            .mapLatest { it.destination.route.orEmpty() }
            .asCommonFlow()
    }
}
I am doing the equivalent in iOS but more simple of just forward the navigateTo directly to currentPath() and observe in Swift:
Copy code
struct ContentView: View {
    @State private var selection: String? = nil
    
    init() {
        SharedController.shared.navigationPathSelected.watch { path in
            self.selection = path
        }
    }
    
    var body: some View {
        MainScreen {
            NavigationView {
                VStack {
                    NavigationLink(destination: Text("Home"), tag: "home", selection: $selection) {
                        HomeScreen()
                    }
                    NavigationLink(destination: Text("Todo"), tag: "todo", selection: $selection) {
                        TodoScreen()
                    }
                }
                .navigationTitle("Navigation")
            }
        }.onOpenURL { url in
            GIDSignIn.sharedInstance.handle(url)
        }
        .onAppear {
            GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
            }
        }
    }
}
This is the maximum possible shared code I can do I think with limitation we have at the moment πŸ™‚ Leverage both NavigationView in iOS and NavigationHost in Androidx/Android πŸ˜„ Then have decoupled the logic and compose code from all of this mayhem πŸ˜„ If having any smarter way of observing Kotlin FLow in Swift let me know, or such. Just sharing here if someone else struggle with same as me.
681 Views