Reputation: 862
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:
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
Reputation: 1301
I was able to replicate the following behaviors of the tabview of Instagram using SwiftUI and MVVM:
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:
Upvotes: 0
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
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
}
}
}
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