SwiftUI 在 TabView 中禁用特定的 tabItem 选择?
SwiftUI Disable specific tabItem selection in a TabView?
我有一个 TabView
,在点击 [+](第二个)tabItem
后显示 sheet。同时,ContentView
也在切换 TabView
的选项卡选择,所以当我关闭显示的 sheet 时,所选选项卡是一个没有任何内容的空白选项卡。不理想的用户体验。
我的问题:
我想知道如何才能简单地禁用特定的 tabItem
,这样它就不会 "behave like a tab",而只是显示 sheet,同时在点击之前保持之前的选项卡选择[+] 项。这可以用 SwiftUI 实现吗?还是我应该通过另一种方式来实现这种效果?
我的标签栏图片:
这是我的 ContentView
的代码,其中我的 TabView
是:
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)
}
}
这里是AppState
:
final class AppState: ObservableObject {
@Published var selectedTab: Int = 0
}
您可以在 sheet 的关闭中添加一些内容以将 tabView 切换到其他选项卡。也许你可以在这个过程中插入一些动画。
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)
}
}
您已经非常接近您想要实现的目标了。您只需要保留先前选定的选项卡索引,并在 sheet 关闭时使用该保留值重置当前选定的选项卡索引。这意味着:
.sheet(isPresented: self.$isPresenting, onDismiss: {
// change back to previous tab selection
self.appState.selectedTab = self.appState.previousSelectedTab
}, content: { self.content })
那么您如何跟踪与 AppState
的 selectedTab
属性 保持同步的最后选定的选项卡索引?使用 Combine
框架本身的 APIs 可能有更多方法可以做到这一点,但我想到的最简单的解决方案是:
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
}
}
}
注意事项:
上述解决方案可能与 禁用特定选项卡项目选择 完全不同,但在您关闭 sheet 它会在呈现 sheet 之前以舒缓的动画恢复到选定的选项卡。 Here is the result.
我能够使用 SwiftUI 和 MVVM 复制 Instagram 的 tabview 的以下行为:
- 选中中间的选项卡后,将打开模式视图
- 关闭中间标签时,再次选择之前选择的标签,而不是中间标签
一个。 ViewModels(一个用于整个选项卡视图,另一个用于特定选项卡)
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
}
}
乙。视图(使用 ViewModel)
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()
}
}
注意:在我的调查过程中,我尝试在中间选项卡的 onAppear 中添加代码。但是,我发现 SwiftUI 中存在一个当前错误,即使点击不同的选项卡也会触发 onAppear。所以以上似乎是最好的方法。
编码愉快!
参考文献:
我有一个 TabView
,在点击 [+](第二个)tabItem
后显示 sheet。同时,ContentView
也在切换 TabView
的选项卡选择,所以当我关闭显示的 sheet 时,所选选项卡是一个没有任何内容的空白选项卡。不理想的用户体验。
我的问题:
我想知道如何才能简单地禁用特定的 tabItem
,这样它就不会 "behave like a tab",而只是显示 sheet,同时在点击之前保持之前的选项卡选择[+] 项。这可以用 SwiftUI 实现吗?还是我应该通过另一种方式来实现这种效果?
我的标签栏图片:
这是我的 ContentView
的代码,其中我的 TabView
是:
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)
}
}
这里是AppState
:
final class AppState: ObservableObject {
@Published var selectedTab: Int = 0
}
您可以在 sheet 的关闭中添加一些内容以将 tabView 切换到其他选项卡。也许你可以在这个过程中插入一些动画。
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)
}
}
您已经非常接近您想要实现的目标了。您只需要保留先前选定的选项卡索引,并在 sheet 关闭时使用该保留值重置当前选定的选项卡索引。这意味着:
.sheet(isPresented: self.$isPresenting, onDismiss: {
// change back to previous tab selection
self.appState.selectedTab = self.appState.previousSelectedTab
}, content: { self.content })
那么您如何跟踪与 AppState
的 selectedTab
属性 保持同步的最后选定的选项卡索引?使用 Combine
框架本身的 APIs 可能有更多方法可以做到这一点,但我想到的最简单的解决方案是:
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
}
}
}
注意事项:
上述解决方案可能与 禁用特定选项卡项目选择 完全不同,但在您关闭 sheet 它会在呈现 sheet 之前以舒缓的动画恢复到选定的选项卡。 Here is the result.
我能够使用 SwiftUI 和 MVVM 复制 Instagram 的 tabview 的以下行为:
- 选中中间的选项卡后,将打开模式视图
- 关闭中间标签时,再次选择之前选择的标签,而不是中间标签
一个。 ViewModels(一个用于整个选项卡视图,另一个用于特定选项卡)
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
}
}
乙。视图(使用 ViewModel)
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()
}
}
注意:在我的调查过程中,我尝试在中间选项卡的 onAppear 中添加代码。但是,我发现 SwiftUI 中存在一个当前错误,即使点击不同的选项卡也会触发 onAppear。所以以上似乎是最好的方法。
编码愉快!
参考文献: