1

I am developing an app using SwiftUI and Firestore.

On the home page, I'm showing the list of items from Firestore that are updating properly when any updations from the backend. Now on the Top plus button, I am navigating to another screen to add a new item. On adding the new item, the list on the home page gets updated, that pushing the AddNewItemView again in the stack.

In this case, I want to detect if the Listing Screen is not visible right now and stop listing to events.

import SwiftUI

struct HomeView: View {
    
    private let useCase: AuthLogoutProvider = AuthService.shared
    @ObservedObject var viewModel: HomeViewModel
    
    init(_ viewModel: HomeViewModel) {
        self.viewModel = viewModel
    }
    
    private var addTeam: some View {
        return HStack {
            Text("Add Team +")
                .font(.system(size: 20))
                .fontWeight(.regular)
                .foregroundColor(Color.secondary)
                .padding(.vertical)
        }
        .frame(width: 150, height: 54)
        .overlay(
            RoundedRectangle(cornerRadius: 10)
                .stroke(Color.secondary, lineWidth: 1)
        )
    }
    
    var body: some View {
        NavigationView {
            VStack {
                HStack {
                    NavigationLink(destination: AddTeamInputView(), label: {
                        addTeam
                    })
                    Spacer()
                }
                .padding()
                
                List(viewModel.teamsInfoModels, id: \.id) { teamInfoModel in
                    VStack {
                        NavigationLink(destination: TeamDetailView(teamInfoModel), label: {
                            EmptyView()
                        })
                        TeamInfoCell(teamInfoModel)
                    }
                }
                Spacer()
            }
            .navigationTitle( Text("Countries"))
            .navigationBarItems(
                trailing: Button(action: {
                    useCase.signOut()
                    self.viewModel.logoutPerformed()
                }) {
                    Text("Log out")
                        .font(.title2)
                        .fontWeight(.bold)
                        .foregroundColor(.secondary)
                }
            )
        }
        .onAppear {
            print("HomeView onAppear")
        }
        .onDisappear {
            print("HomeView onDisappear")
        }
    }
}

This is my Home View having a list of team and Add Team Button

on clicking Add new button navigating to another screen to add the Team on Firestore

AddTeamInputView

struct AddTeamInputView: View {
    
    @ObservedObject private var viewModel: AddTeamInputViewModel
    
    init(_ viewModel: AddTeamInputViewModel = AddTeamInputViewModel()) {
        self.viewModel = viewModel
        print("AddTeamInputView init called")
    }
    
    var body: some View {
        VStack {
            
            HStack {
                TextField("Enter Team name", text: $viewModel.teamName)
                    .font(.title2)
                    .foregroundColor(.secondary)
                    .frame(width: UIScreen.main.bounds.width - 40)
            }
            .padding(10)
            .overlay(
                RoundedRectangle(cornerRadius: 10)
                    .stroke(Color.secondary, lineWidth: 1)
            )
            
            if viewModel.move {
                NavigationLink(destination: CountrySelectionView(viewModel: CountrySelectionViewModel(viewModel.createdTeam())), isActive: $viewModel.move) {
                    EmptyView()
                }
            }
            
            AppButton(action: {
                
                self.viewModel.addTeam()
                
            }, title: "Save", width: 200)
            .padding(.vertical, 40)
        }
        .navigationTitle("Name Your Team!")
    }
}

AddTeamViewModel

class AddTeamInputViewModel: ObservableObject {
    
    init() {
        print("AddTeamInputViewModel init")
    }
    
    @Published var teamName: String = ""
    
    @Published var move: Bool = false
    private var team: Team? = nil
        
    func addTeam() {
        TeamDataProvider.shared.createTeam(teamName) { (error, teamId) in
            if let teamId = teamId {
                if let team = TeamDataProvider.shared.team(forId: teamId)  {
                    
                    DispatchQueue.main.async {
                        self.team = team
                        print("Move to select country for team == \(self.team?.teamName ?? "") == \(self.move)")
                        self.move.toggle()
                        print("Move Value after toggle == \(self.move)")
                    }
                    
                } else {
                    print("Team issue")
                }
            } else {
                print("Team issue")
            }
        }
    }
    
    func createdTeam() -> Team {
        return team!
    }
    
    deinit {
        print("AddTeamInputViewModel deinit called")
    }
}
b m gevariya
  • 300
  • 1
  • 4
  • 12
Rahish Kansal
  • 39
  • 1
  • 8
  • You haven't shown any code. Assuming, you have some model that determines _what_ will be rendered, your data representing the ListView is empty or nil. So, you may find your answer in the "source of truth" (model). If not, rethink your design. – CouchDeveloper Aug 11 '21 at 09:12
  • If your ListView is covered by a modal view, your model or ViewModel should know this. If your view is covered by another View on a Navigation Stack, your model (ViewModel) should know this. There are only a few cases where the model does not know about a modal view partly covering the ListView. In this case, you should not stop observing events. – CouchDeveloper Aug 11 '21 at 09:15
  • @CouchDeveloper could you check the given code and suggest something – Rahish Kansal Aug 11 '21 at 17:27
  • I assume you see the updates of teams in the HomeView due to adding a new team in the AddTeamInputView and committing this to the data provider. Then getting onChange notifications for the teams from your ViewModel. IMHO, that is how it should work! However, you might change the AddTeamInputView to a modal view (sheet), and gather the new team info there, and _on dismiss_, let your HomeViewModel handle the "add new team" action. So, you may only need _one_ ViewModel but with a more complete API to the underlying DataProvider (CRUD) and a simple InputView which provides attributes for the team. – CouchDeveloper Aug 11 '21 at 17:57
  • I understood you opinion but we are strict to this flow, as there is another flow after adding the team like inviting the member to that. Here the flow is same as you described that on HomeView observing the changes and then adding new team on the AddTeamView which creating the another event for Homeview and then HomeView pushing the AddTeamView unexpectedly – Rahish Kansal Aug 11 '21 at 18:19
  • You could add members to a team within the Detail View of team. You can even navigate programmatically from the ListView to the DetailView controlled by the ViewModel after creating a new team. Also, I cannot reproduce your issue with a minimal example, see below. – CouchDeveloper Aug 11 '21 at 20:14
  • @CouchDeveloper flow is per the client requirements, we can't change that I can add you to git project if you could help anyway. Your help will be appreciated – Rahish Kansal Aug 11 '21 at 20:17
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/235923/discussion-between-rahish-kansal-and-couchdeveloper). – Rahish Kansal Aug 12 '21 at 07:12
  • @RahishKansal huhu! – vikingosegundo Aug 12 '21 at 18:34
  • I will toss out a suggestion; if you have two views; a master view and detail view as they are often called, and the master view shows the detail view with details about what the user selected, the master view has control over that view. e.g. just before or as the detail view is displayed, remove the observer in the master view so it no longer receives updates. Then, when returning from the child view, re-enable the observer. Seems like a pretty simple approach. – Jay Aug 13 '21 at 18:40

3 Answers3

0

This is not an answer, but rather an example that shows no issue with a minimal implementation so far:

import SwiftUI

struct InputView: View {
    @State var name: String
    let submitOnDisappear: Bool
    let onSubmit: (String) -> Void

    init(name: String, onSubmit: @escaping (String) -> Void, submitOnDisappear: Bool = false) {
        self.name = name
        self.submitOnDisappear = submitOnDisappear
        self.onSubmit = onSubmit
    }
    var body: some View {
        Form {
            TextField("Name", text: $name)
                .onSubmit {
                    if submitOnDisappear == false {
                        onSubmit(name)
                    }
                }
        }
        .onDisappear {
            if submitOnDisappear {
                onSubmit(name)
            }
        }
    }
}

struct DetailView: View {
    let detail: String

    var body: some View {
        Text(detail)
    }
}

struct ListView: View {
    let items: [String]
    let addNewItem: (String) -> Void

    enum Sheet: Int, Identifiable {
        case new
        var id: Int { self.rawValue }
    }

    @State private var showSheet: Sheet? = nil

    var body: some View {
        VStack {
            Button(action: { showSheet = .new } ) {
                Text("New item via Sheet")
            }
            NavigationLink(
                destination: InputView(name: "", onSubmit: addNewItem, submitOnDisappear: true),
                label: { Text("New item via push") }
            )

            List(items, id: \.self) { item in
                NavigationLink(destination: DetailView(detail: item),
                               label: { Text(item) } )
            }
        }
        .sheet(item: $showSheet) { sheet in
            switch sheet {
            case .new:
                InputView(name: "",
                          onSubmit: { value in
                    showSheet = nil
                    addNewItem(value)
                })
            }
        }
    }
}

class ViewModel: ObservableObject {
    struct ViewState {
        var items: [String] = []
    }

    @Published private(set) var viewState = ViewState()

    init() {
        self.viewState.items = ["John"]
    }

    func add(item: String) {
        self.viewState.items.append(item)
    }
}


struct ContentView: View {
    @StateObject var viewModel = ViewModel()

    var body: some View {
        ListView(items: viewModel.viewState.items,
                 addNewItem: viewModel.add(item:))
            .listStyle(.plain)
            .navigationTitle("Items")
    }
}

import PlaygroundSupport
PlaygroundPage.current.setLiveView(
    NavigationView {
        ContentView()
    }
    .navigationViewStyle(.stack)
)

Adding a new item via a view pushed to the navigation stack feels not right for me. It looks and behaves awkward. From a UX perspective, it should be a modal view.

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
0

Guys I have fixed this issue by just changing the ObservableObject to StateObject, although still don't know why exactly this was happening. I just do that for dependency injection

struct AddTeamInputView: View {
    
    @StateObject private var viewModel = AddTeamInputViewModel()
    
    init(_ viewModel) {
        print("AddTeamInputView init called")
    }
    
    var body: some View {
        VStack {
            
            HStack {
                TextField("Enter Team name", text: $viewModel.teamName)
                    .font(.title2)
                    .foregroundColor(.secondary)
                    .frame(width: UIScreen.main.bounds.width - 40)
            }
            .padding(10)
            .overlay(
                RoundedRectangle(cornerRadius: 10)
                    .stroke(Color.secondary, lineWidth: 1)
            )
            
            if viewModel.move {
                NavigationLink(destination: CountrySelectionView(viewModel: CountrySelectionViewModel(viewModel.createdTeam())), isActive: $viewModel.move) {
                    EmptyView()
                }
            }
            
            AppButton(action: {
                
                self.viewModel.addTeam()
                
            }, title: "Save", width: 200)
            .padding(.vertical, 40)
        }
        .navigationTitle("Name Your Team!")
    }
}
b m gevariya
  • 300
  • 1
  • 4
  • 12
Rahish Kansal
  • 39
  • 1
  • 8
-1

You should check their behavior but I think what you are looking for are an .onAppear and .onDisappear view modifiers which are receiving a callback once the view appear & disappear accordingly for more info you can read about it in the links below:

Mr Spring
  • 533
  • 8
  • 17
  • 1
    onDisappear is only called when removing the view from the stack. Here ListView is not being removed from the stack but the another view is push on the top of it – Rahish Kansal Aug 10 '21 at 22:23
  • That's correct only if you put those modifiers on the navigationView, try putting these modifiers on one of the items inside of your view, for example your List or your ForEach – Mr Spring Aug 10 '21 at 22:30
  • actually inner views also not working as expected. as whenever state is updated those appear and disappear block is called not on the actually view willapear and disappear – Rahish Kansal Aug 11 '21 at 04:07