Steven Schafer
Steven Schafer

Reputation: 862

SwiftUI Disable specific tabItem selection in a TabView?

I have a TabView that presents a sheet after tapping on the [+] (2nd) tabItem. At the same time, the ContentView is also switching the TabView's tab selection, so when I dismiss the sheet that is presented, the selected tab is a blank one without any content. Not an ideal user experience.

My question:

I am wondering how I can simply disable that specific tabItem so it doesn't "behave like a tab" and simply just present's the sheet while maintaining the previous tab selection prior to tapping the [+] item. Is this possible with SwiftUI or should I got about this another way to achieve this effect?

Image of my tab bar:

enter image description here

Here's the code for my ContentView where my TabView is:

struct SheetPresenter<Content>: View where Content: View {
    @EnvironmentObject var appState: AppState
    @Binding var isPresenting: Bool

    var content: Content
    var body: some View {
        Text("")
            .sheet(isPresented: self.$isPresenting, onDismiss: {
                // change back to previous tab selection
                print("New listing sheet was dismissed")

            }, content: { self.content})
            .onAppear {
                DispatchQueue.main.async {
                    self.isPresenting = true
                    print("New listing sheet appeared with previous tab as tab \(self.appState.selectedTab).")
                }
            }
    }
}

struct ContentView: View {
    @EnvironmentObject var appState: AppState
    @State private var selection = 0
    @State var newListingPresented = false

    var body: some View {

        $appState.selectedTab back to just '$selection'
        TabView(selection: $appState.selectedTab){

            // Browse
            BrowseView()
                .tabItem {
                    Image(systemName: (selection == 0 ? "square.grid.2x2.fill" : "square.grid.2x2")).font(.system(size: 22))
                }
                .tag(0)


            // New Listing
            SheetPresenter(isPresenting: $newListingPresented, content: NewListingView(isPresented: self.$newListingPresented))
                .tabItem {
                    Image(systemName: "plus.square").font(.system(size: 22))
                }
                .tag(1)

            // Bag
            BagView()
                .tabItem {
                    Image(systemName: (selection == 2 ? "bag.fill" : "bag")).font(.system(size: 22))
                }
                .tag(2)

            // Profile
            ProfileView()
                .tabItem {
                    Image(systemName: (selection == 3 ? "person.crop.square.fill" : "person.crop.square")).font(.system(size: 22))
                }
                .tag(3)
        }.edgesIgnoringSafeArea(.top)
    }
}

And here's AppState:

final class AppState: ObservableObject {

    @Published var selectedTab: Int = 0
}

Upvotes: 3

Views: 4741

Answers (3)

Meo Flute
Meo Flute

Reputation: 1301

I was able to replicate the following behaviors of the tabview of Instagram using SwiftUI and MVVM:

  1. when the middle tab is selected, a modal view will open
  2. when the middle tab is closed, the previously selected tab is again selected, not the middle tab

A. ViewModels (one for the whole tabview and another for a specific tab)

import Foundation

class TabContainerViewModel: ObservableObject {
    
    //tab with sheet that will not be selected
    let customActionTab: TabItemViewModel.TabItemType = .addPost
    
    //selected tab: this is the most important code; here, when the selected tab is the custom action tab, set the flag that is was selected, then whatever is the old selected tab, make it the selected tab
    @Published var selectedTab: TabItemViewModel.TabItemType = .feed {
        didSet{
            if selectedTab == customActionTab {
                customActionTabSelected = true
                selectedTab = oldValue
            }
        }
    }
    //flags whether the middle tab is selected or not
    var customActionTabSelected: Bool = false
    
    //create the individual tabItemViewModels that will get displayed
    let tabItemViewModels:[TabItemViewModel] = [
        TabItemViewModel(imageName:"house.fill", title:"Feed", type: .feed),
        TabItemViewModel(imageName:"magnifyingglass.circle.fill", title:"Search", type: .search),
        TabItemViewModel(imageName:"plus.circle.fill", title:"Add Post", type: .addPost),
        TabItemViewModel(imageName:"heart.fill", title:"Notifications", type: .notifications),
        TabItemViewModel(imageName:"person.fill", title:"Profile", type: .profile),
    ]
}

//this is the individual tabitem ViewModel
import SwiftUI

struct TabItemViewModel: Hashable {
    let imageName:String
    let title:String
    let type: TabItemType
    
    enum TabItemType {
        case feed
        case search
        case addPost
        case notifications
        case profile
    }
}

B. View (makes use of the ViewModels)

import SwiftUI

struct TabContainerView: View {
    
    @StateObject private var tabContainerViewModel = TabContainerViewModel()
    
    @ViewBuilder
    func tabView(for tabItemType: TabItemViewModel.TabItemType) -> some View {
        switch tabItemType {
        case .feed:
            FeedView()
        case .search:
            SearchView()
        case .addPost:
            AddPostView(tabContainerViewModel: self.tabContainerViewModel)
        case .notifications:
            NotificationsView()
        case .profile:
            ProfileView()
        }
    }
    
    var body: some View {
        TabView(selection: $tabContainerViewModel.selectedTab){
            ForEach(tabContainerViewModel.tabItemViewModels, id: \.self){ viewModel in
                tabView(for: viewModel.type)
                    .tabItem {
                        Image(systemName: viewModel.imageName)
                        Text(viewModel.title)
                    }
                    .tag(viewModel.type)
            }
        }
        .accentColor(.primary)
        .sheet(isPresented: $tabContainerViewModel.customActionTabSelected) {
            PicsPicker()
        }
    }
}

struct TabContainerView_Previews: PreviewProvider {
    static var previews: some View {
        TabContainerView()
    }
}

Note: In the course of my investigation, I tried adding code to onAppear in the middle tab. However, I found out that there is a current bug in SwiftUI that fires the onAppear even if a different tab was tapped. So the above seems to be the best way.

Happy coding!

References:

  1. https://www.youtube.com/watch?v=SZj3CjMfT-8

Upvotes: 0

E.Coms
E.Coms

Reputation: 11531

You may add something in the dismiss of sheet to switch the tabView to other tabs. Maybe you can insert some animation during the process.

struct SheetPresenter<Content>: View where Content: View {

    @EnvironmentObject var appState: AppState
    @Binding var isPresenting: Bool
    @Binding var showOtherTab: Int
    var content: Content

    var body: some View {
        Text("")
        .sheet(isPresented: self.$isPresenting,
               onDismiss: {
                   // change back to previous tab selection
                   self.showOtherTab = 0
               } ,
               content: { self.content})
        .onAppear {
            DispatchQueue.main.async {
                self.isPresenting = true
                print("New listing sheet appeared with previous tab as tab \(self.appState.selectedTab).")
            }
        }
    }
}

struct ContentView: View {

    @EnvironmentObject var appState: AppState
    @State private var selection = 0
    @State var newListingPresented = false

    var body: some View {
        //  $appState.selectedTab back to just '$selection'
        TabView(selection: $appState.selectedTab){
            // Browse
            Text("BrowseView") //BrowseView()
                .tabItem {
                    Image(systemName: (selection == 0 ? "square.grid.2x2.fill" : "square.grid.2x2"))
                        .font(.system(size: 22))
            }   .tag(0)
            // New Listing
            SheetPresenter(isPresenting: $newListingPresented,
                           showOtherTab: $appState.selectedTab,
                           content: Text("1232"))//NewListingView(isPresented: self.$newListingPresented))
                .tabItem {
                    Image(systemName: "plus.square")
                        .font(.system(size: 22))
                }   .tag(1)
            // Bag
            //  BagView()
            Text("BAGVIEW")
                .tabItem {
                    Image(systemName: (selection == 2 ? "bag.fill" : "bag"))
                        .font(.system(size: 22))
            }   .tag(2)
            // Profile
            Text("ProfileView") //   ProfileView()
                .tabItem {
                     Image(systemName: (selection == 3 ? "person.crop.square.fill" : "person.crop.square"))
                         .font(.system(size: 22))
            }   .tag(3)
        }   .edgesIgnoringSafeArea(.top)
    }
}

Upvotes: 0

nayem
nayem

Reputation: 7585

You are pretty close to what you want to achieve. You will just need to preserve the previous selected tab index and reset the current selected tab index with that preserved value at the time of the dismissal of the sheet. That means:

.sheet(isPresented: self.$isPresenting, onDismiss: {
    // change back to previous tab selection
    self.appState.selectedTab = self.appState.previousSelectedTab
}, content: { self.content })

So how do you keep track of the last selected tab index that stays in sync with the selectedTab property of the AppState? There may be more ways to do that with the APIs from Combine framework itself, but the simplest solution that comes to my mind is:

final class AppState: ObservableObject {
    // private setter because no other object should be able to modify this
    private (set) var previousSelectedTab = -1 
    @Published var selectedTab: Int = 0 {
        didSet {
            previousSelectedTab = oldValue
        }
    }
}

Caveats:

The above solution of may not be the exact thing as disable specific tab item selection but after you dismiss the sheet it will revert back with a soothing animation to the selected tab prior to presenting the sheet. Here is the result.

Upvotes: 3

Related Questions