fabulous-dev / Fabulous

Declarative UI framework for cross-platform mobile & desktop apps, using MVU and F# functional programming

Home Page:https://fabulous.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Bug] Navigation error on iOS, but not on Android

reigam opened this issue · comments

commented

The following basic navigation setup works perfectly well for me on android, but shows some strange behavior on iOS:
When I use 'Close' on the second page, everything works fine.
However, if I use the same msg on the third page, the view changes to an empty page with the title of the first page, then immediately changes to the title of the third page, still with an empty view.

Shared.fs;

type GlobalModel = { 
    PageStash: List<AppPages.Name>
    }

module Helpers = 
    let rec reshuffle list: List<'a> =
        match list with
        | [] -> []
        | l -> l |> List.rev |> List.tail |> List.rev

FirstPage.fs

     let update msg (model: Model) (globalModel: GlobalModel) =
        match msg with
        | OpenPage s -> model, {globalModel with PageStash = List.append globalModel.PageStash [s]}, Cmd.none
            
    let view (model: Model) (globalModel: GlobalModel) =        
                ...  
                Button("Go To Second Page", OpenPage AppPages.names.SecondPage)
                ...

SecondPage.fs

     let update msg (model: Model) (globalModel: GlobalModel) =
        match msg with
        | OpenPage s -> model, { globalModel with PageStash = List.append globalModel.PageStash [s] }, Cmd.none
        | Close -> model, { globalModel with PageStash = [AppPages.names.FirstPage] }, Cmd.none

    let view (model: Model) (globalModel: GlobalModel)  =
                ....
                Button("Go To Third Page", OpenPage AppPages.names.ThirdPage)
                Button("Close All", Close)
               ...

ThirdPage.fs

    let update msg (model: Model) (globalModel: GlobalModel) =
        match msg with
        | OpenPage s -> model, { globalModel with PageStash = List.append globalModel.PageStash [s] }, Cmd.none
        | Close -> model, { globalModel with PageStash = [AppPages.names.FirstPage] }, Cmd.none

    let view (model: Model) (globalModel: GlobalModel)  =
                ...
                Button("Close All", Close)
                ...

App.fs

    let initModel = 
        { Global = { 
            PageStash = [AppPages.names.FirstPage] }
          LayoutsPage = fst (LayoutsPage.init())
          FirstPage = fst (FirstPage.init())
          SecondPage = fst (SecondPage.init())
          ThirdPage = fst (ThirdPage.init())
        }
          
    let init () = initModel, Cmd.none

    let update msg model =
        match msg with
        | LayoutsPageMsg m ->
            let l, g, c = LayoutsPage.update m model.LayoutsPage model.Global
            { model with LayoutsPage = l; Global = g }, (Cmd.map LayoutsPageMsg c)       
        | FirstPageMsg m ->
            let l, g, c = FirstPage.update m model.FirstPage model.Global
            { model with FirstPage = l; Global = g }, (Cmd.map FirstPageMsg c)       
        | SecondPageMsg m ->
            let l, g, c = SecondPage.update m model.SecondPage model.Global
            { model with SecondPage = l; Global = g }, (Cmd.map SecondPageMsg c)       
        | ThirdPageMsg m ->
            let l, g, c = ThirdPage.update m model.ThirdPage model.Global
            { model with ThirdPage = l; Global = g },  (Cmd.map ThirdPageMsg c)       
        | NavigationPopped ->
            { model with Global = { PageStash = Helpers.reshuffle model.Global.PageStash } }, Cmd.none

    let view (model: Model) =
        Application(
            (NavigationPage(){
                //View.map LayoutsPageMsg (LayoutsPage.view model.LayoutsPage model.Global)
                for page in model.Global.PageStash do
                    match page with                    
                    |AppPages.Name "First Page" ->
                        let p = View.map FirstPageMsg (FirstPage.view model.FirstPage model.Global)
                        yield p 
                    |AppPages.Name "Second Page" ->
                        let p = View.map SecondPageMsg (SecondPage.view model.SecondPage model.Global)
                        yield p 
                    |AppPages.Name "Third Page" ->
                        let p = View.map ThirdPageMsg (ThirdPage.view model.ThirdPage model.Global)
                        yield p 
                    | _ -> ()
            })
                .onBackNavigated(NavigationPopped)
        )

full app can be viewed on:
https://github.com/reigam/MultiNavPageStashTemplate
(This is an older version, the behavior is the same with the newest version of fabulous and Xamarin).

Thanks for the report @reigam

XF.NavigationPage has always been buggy with Fabulous when popping or pushing several pages at once.
We really need to spend time to fix that.

In your example, the weird issue you're seeing is triggered by Helpers.reshuffle.
This is because Xamarin.Forms will raise the BackNavigated event, both if the user clicked the back button in the nav bar and if you popped a page programmatically.

The problem is that you already have updated the PageStash with the expected end state.
But because NavigationPopped is triggered, Helpers.reshuffle will remove the last page (here "First Page") from the stack leaving it empty when it should not be possible.

Not sure why XF is not straight up throwing an exception here.

As a workaround while waiting for a proper fix, you could flag your PageStash as "ManuallyUpdated", so when NavigationPopped is called you can differentiate between the user clicking the back button of the NavigationPage or clicking the "Close all" button.

In the first case, you can pop a page from the PageStash. In the second case, you can keep the already updated stash.

commented

Ok. I didn't consider, that the 'Close' Message also raises 'NavigationPopped'.

This works fine:

        | NavigationPopped ->
            if model.Global.PoppedByBackButton 
                then { model with Global = { PageStash =  Helpers.reshuffle model.Global.PageStash
                                             PoppedByBackButton = true }}, Cmd.none
                else { model with Global = {model.Global with PoppedByBackButton = true}}, Cmd.none
        | Close -> model, { globalModel with PoppedByBackButton = false
                                             PageStash = [AppPages.names.FirstPage] }, Cmd.none

Thank you very much!