0

I'm migrating a large iOS project developed using VIPER architecture from ViewControllers to SwiftUi. To do this I'm using ViewAdapters to connect VIPER routers and presenters with SwiftUI views. Although this is working fine I'm having problems with a specific case:

The app has a tabbar and each tab has navigation controllers. I need that if the users is navigating insdide tab1, then changes to tab2 and then goes back to tab1, tab1 must show the last view, not the root one.

I'm trying to show a reduced version of my app:

MyView:

struct MainView: View {
    
    @ObservedObject var observedModel: MainViewAdapter

    @State private var tab1Id = UUID()
    @State private var tab2Id = UUID()

    let tab1 = Tab1Router()
    let tab2 = Tab2Router()

    init(observedModel: MainViewAdapter) {
        self.observedModel = observedModel
    }

    var body: some View {
        
        VStack(spacing: 0) {
            
            TabView(selection: $observedModel.currentTab) {
                tab1.swiftUIView
                    .tag("tab1")
                    .id(tab1Id)
                tab2.swiftUIView
                    .tag("tab2")
                    .id(tab2Id)
            }

            MyCustomTabBar(observedModel: observedModel,
                           currentTab: $observedModel.currentTab)
        }
        .ignoresSafeArea(.keyboard, edges: .bottom)
    }
}

Tab2 View (Tab1 View and its child views would be equivalent):

struct Tab2: View {
    
    @ObservedObject var observedModel: Tab2ViewAdapter

    @State private var hasToGoToChild2 = false

    init(observedModel: Tab2ViewAdapter) {
        self.observedModel = observedModel
    }

    var body: some View {
        
        NavigationView {
            
            VStack(spacing: 0) {

                if hasToGoToChild2 {
                    
                    NavigationLink(destination: Child2Router().swiftUIView.navigationBarHidden(true),
                                   isActive: $hasToGoToChild2) {
                    }
                    .navigationBarHidden(true)
                    .hidden()
                }

                myLogFunction("refreshing Tab2")
                Button {
                    hasToGoToChild2 = true
                } label: {
                    Text("Go to child 2")
                }
            }
            .navigationBarHidden(true)
            .onAppear {
                print("Tab2 onAppear")
                hasToGoToChild2 = false
                observedModel.presenter?.readData()
            }
        }
    }
}

Child2 View:

struct Child2: View {
    
    @ObservedObject var observedModel: Child2ViewAdapter
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    init(observedModel: Child2ViewAdapter) {
        self.observedModel = observedModel
    }

    var body: some View {
        
        NavigationView {
            
            VStack(spacing: 0) {
            
                Button {
                    presentationMode.wrappedValue.dismiss()
                } label: {
                    Text("Back")
                }

                if observedModel.hasToGoToChild2_1 {
                
                    NavigationLink(destination: Child2_1_Router().swiftUIView.navigationBarHidden(true),
                                   isActive: $observedModel.hasToGoToChild2_1) {
                    }
                    .navigationBarHidden(true)
                    .hidden()
                }

                ...

                if observedModel.hasToGoToChild2_n {
                
                    NavigationLink(destination: Child2_n_Router().swiftUIView.navigationBarHidden(true),
                                   isActive: $observedModel.hasToGoToChild2_n) {
                    }
                    .navigationBarHidden(true)
                    .hidden()
                }

                List() {
                    
                    let items = observedModel.items
                    myLogFunction("refreshing Child2 with items.count \(items.count)")
                    ForEach(items) { item in
                        Button(action: item.handler) {
                            Text("Go to item destination")
                        }
                    }
                }
            }
            .navigationBarHidden(true)
            .onAppear {
                print("Child2 onAppear")
                observedModel.hasToGoToChild2_1 = false
                ...
                observedModel.hasToGoToChild2_n = false
                observedModel.presenter?.readData()
            }
        }
    }
}

Finally, Child2_x view:

struct Child2_x: View {
    
    @ObservedObject var observedModel: Child2_xViewAdapter
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    init(observedModel: Child2_xViewAdapter) {
        self.observedModel = observedModel
    }

    var body: some View {
        
        NavigationView {
            
            VStack(spacing: 0) {
            
                Button {
                    presentationMode.wrappedValue.dismiss()
                } label: {
                    Text("Back")
                }

                List() {
                    
                    let items = observedModel.items
                    myLogFunction("refreshing Child2_x with items.count \(items.count)")
                    ForEach(items) { item in
                        Text(item.name)
                    }
                }
            }
            .navigationBarHidden(true)
            .onAppear {
                print("Child2_x onAppear")
                observedModel.presenter?.readData()
            }
        }
    }
}

If I do:

  1. enter the app (tab1 by default) > click on tab2 > go to child2 > click on tab1 > go back to tab 2 then everything goes well and child2 is shown.
  2. enter the app (tab1 by default) > click on tab2 > go to child2 > go to child2_x > click on tab1 > go back to tab 2 then child2_x is shown for a second and then a blank view appears.

As you can see I've added logs inside the views to check what's going on and I don't understand anything. Could you help me understand whats wrong with the code?

Logs for case 1)

- enter the app (tab1 by default)

refreshing Tab2

- click on tab2

Tab2 onAppear

refreshing Tab2

refreshing Tab2

- go to child2

refreshing Tab2

refreshing Child2 with items.count 0

Child2 onAppear

refreshing Child2 with items.count 2

refreshing Child2 with items.count 2

refreshing Child2 with items.count 2

Child2 onAppear Question: Why is it calling onAppear again??

refreshing Child2 with items.count 2

- click on tab1

refreshing Child2 with items.count 2

- go back to tab 2

refreshing Child2 with items.count 2

refreshing Child2 with items.count 2

Child2 onAppear

refreshing Child2 with items.count 2

refreshing Child2 with items.count 2

refreshing Child2 with items.count 2

refreshing Child2 with items.count 2

Everything is OK, Child2 is shown with its 2 items.

Logs for case 2)

- enter the app (tab1 by default)

refreshing Tab2

- click on tab2

Tab2 onAppear

refreshing Tab2

refreshing Tab2

- go to child2

refreshing Tab2

refreshing Child2 with items.count 0

Child2 onAppear

refreshing Child2 with items.count 2

refreshing Child2 with items.count 2

refreshing Child2 with items.count 2

Child2 onAppear Question: Why is it calling onAppear again??

refreshing Child2 with items.count 2

- go to Child2_x

refreshing Child2 with items.count 2

refreshing Child2_x with items.count 0

Child2_x onAppear

refreshing Child2_x with items.count 1

refreshing Child2_x with items.count 1

refreshing Child2_x with items.count 1

- click on tab1

refreshing Child2 with items.count 2 Question: Why is it refreshing its parent view (Child2)??

refreshing Child2_x with items.count 1

refreshing Child2_x with items.count 0 Question: Why is it refreshing the view without items??

- go back to tab 2

refreshing Child2 with items.count 2 Question: Why is it refreshing its parent view (Child2)??

refreshing Child2_x with items.count 0

refreshing Child2 with items.count 2

refreshing Child2_x with items.count 0

Child2_x onAppear

refreshing Child2_x with items.count 1

refreshing Child2 with items.count 2

refreshing Child2_x with items.count 1

refreshing Child2_x with items.count 1

refreshing Child2 with items.count 2

refreshing Child2 with items.count 2

refreshing Child2_x with items.count 1

refreshing Child2_x with items.count 0 Question: Why is it refreshing the view without items??

I think this is the problem, this 0 items shows no items in the list and therefore a blank view

Wonton
  • 1,033
  • 16
  • 33
  • Look into NavigationPath, the documentation has really good examples – lorem ipsum Oct 16 '22 at 13:38
  • The problem is that NavigationPath (and NavigationStack), which seem to be very helpful, are only available for iOS 16 and my app has to support iOS 14. – Wonton Oct 16 '22 at 13:44
  • You need to create a version of your own then. – lorem ipsum Oct 16 '22 at 13:45
  • But according to documentation NavigationLink pushes its next view into the navigation stack, I guess NavigationView has to manage all of this. – Wonton Oct 16 '22 at 13:48
  • Yes it is all internal until NavigationStack. SwiftUI was a baby for iOS 14 only a year old iOS 16 is the first version that is fairly decent for non-default behavior – lorem ipsum Oct 16 '22 at 13:58

0 Answers0