我的应用程序有四个主要功能区域,用户可以通过 ContentView 底部的自定义标签栏访问它们。当用户点击标签栏中的所需功能时,我想使用幻灯片切换在视图之间移动。
我还希望滑动的方向基于选项卡栏上选项的相对位置。也就是说,如果从选项卡 1 转到选项卡 3,视图将从右向左滑动,或者如果从选项卡 3 转到选项卡 2,视图将从左向右滑动。
这对第一次改变视图和任何后续改变视图改变幻灯片的方向非常有效。例如,以下视图更改顺序有效:1->3、3->2、2->4、4->1。
但是,任何时候如果方向与先前的方向相同,则任何时候都无法正常工作。例如,以下序列中的粗体更改无法正常工作。 1->2, 2->3, 3->4, 4->3, 3->2.
在上述无法正常工作的转换中,传入视图从正确的方向进入,但传出视图以错误的方向离开。例如,这篇文章底部的图像显示新视图从右向左适当移动,但离开视图从left向右移动,留下左边的空白(它也应该从移动从右到左以及传入视图)。
关于为什么会发生这种情况/如何纠正它有什么想法吗?
我正在为我的应用程序使用 iOS 16。
以下是演示此问题的完整代码示例:
import SwiftUI
@main
struct TabBar_testingApp: App {
@StateObject var tabOption = TabOption()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(tabOption)
}
}
}
class TabOption: ObservableObject {
@Published var tab: TabItem = .tab1
@Published var slideLeft: Bool = true
}
enum TabItem: Int, CaseIterable {
// MARK: These are the four main elements of the app that are navigated to via the custom tab or sidebar controls
case tab1 = 0
case tab2 = 1
case tab3 = 2
case tab4 = 3
var description: String {
switch self {
case .tab1: return "Tab 1"
case .tab2: return "Tab 2"
case .tab3: return "Tab 3"
case .tab4: return "Tab 4"
}
}
var icon: String {
switch self {
case .tab1: return "1.circle"
case .tab2: return "2.circle"
case .tab3: return "3.circle"
case .tab4: return "4.circle"
}
}
}
struct ContentView: View {
@EnvironmentObject var tabOption: TabOption
var body: some View {
NavigationStack {
VStack {
// Content
Group {
switch tabOption.tab {
case TabItem.tab1:
SlideOneView()
case TabItem.tab2:
SlideTwoView()
case TabItem.tab3:
Slide3View()
case TabItem.tab4:
SlideFourView()
}
}
// Use a slide transition when changing the tab views
.transition(.move(edge: tabOption.slideLeft ? .leading : .trailing))
Spacer()
// Custom tab bar
HStack {
Spacer()
// Open tab 1
Button(action: {
withAnimation {
// Set the direction the tabs will slide when transitioning between the tabs
tabOption.slideLeft = true
// Change to the selected tab
tabOption.tab = TabItem.tab1
}
}) {
VStack {
Image(systemName: TabItem.tab1.icon).font(.title2)
Text(TabItem.tab1.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab1 ? .primary : .secondary)
.font(.title)
}
Spacer()
// Open tab 2
Button(action: {
withAnimation {
// Set the direction the tabs will slide when transitioning between the tabs
if tabOption.tab.rawValue == TabItem.tab1.rawValue {
tabOption.slideLeft = false
} else {
tabOption.slideLeft = true
}
// Change to the selected tab
tabOption.tab = TabItem.tab2
}
}) {
VStack {
Image(systemName: TabItem.tab2.icon).font(.title2)
Text(TabItem.tab2.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab2 ? .primary : .secondary)
.font(.title)
}
Spacer()
// Open tab 3
Button(action: {
withAnimation {
// Set the direction the tabs will slide when transitioning between the tabs
if tabOption.tab.rawValue == TabItem.tab4.rawValue {
tabOption.slideLeft = true
} else {
tabOption.slideLeft = false
}
// Change to the selected tab
tabOption.tab = TabItem.tab3
}
}) {
VStack {
Image(systemName: TabItem.tab3.icon).font(.title2)
Text(TabItem.tab3.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab3 ? .primary : .secondary)
.font(.title)
}
Spacer()
// Open tab 4
Button(action: {
withAnimation {
// Set the direction the tabs will slide when transitioning between the tabs
tabOption.slideLeft = false
// Change to the selected tab
tabOption.tab = TabItem.tab4
}
}) {
VStack {
Image(systemName: TabItem.tab4.icon).font(.title2)
Text(TabItem.tab4.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab4 ? .primary : .secondary)
.font(.title)
}
Spacer()
} // HStack closure
.foregroundStyle(.blue)
.padding(.top, 5)
}
}
}
}
struct SlideOneView: View {
var body: some View {
ZStack {
Group {
Color.blue
Text("Tab Content 1")
.font(.largeTitle)
.foregroundColor(.white)
}
}
}
}
struct SlideTwoView: View {
var body: some View {
ZStack {
Group {
Color.green
Text("Tab Content 2")
.font(.largeTitle)
.foregroundColor(.white)
}
}
}
}
struct Slide3View: View {
var body: some View {
ZStack {
Group {
Color.purple
Text("Tab Content 3")
.font(.largeTitle)
.foregroundColor(.white)
}
}
}
}
struct SlideFourView: View {
var body: some View {
ZStack {
Group {
Color.red
Text("Tab Content 4")
.font(.largeTitle)
.foregroundColor(.white)
}
}
}
}
最后,这是底部(离开)视图从左到右错误移动的屏幕截图,在左侧短暂留下空白,而传入视图从右到左正确移动。
根据以下评论,这是我修改后的代码:
class TabOption: ObservableObject {
@Published var tab: TabItem = .tab1
@Published var slideLeft: Bool = true
func changeTab(to newTab: TabItem) {
switch newTab.rawValue {
// case let allows you to make a comparison in the case statement
// This determines the direction is decreasing, so we want a right slide
case let t where t < tab.rawValue:
slideLeft = false
// This determines the direction is increasing, so we want a left slide
case let t where t > tab.rawValue:
slideLeft = true
// This determines that the user tapped this tab, so do nothing
default:
return
}
// We have determined the proper direction, so change tabs.
withAnimation(.easeInOut) {
tab = newTab
}
}
}
enum TabItem: Int, CaseIterable {
// MARK: These are the four main elements of the app that are navigated to via the custom tab or sidebar controls
case tab1 = 0
case tab2 = 1
case tab3 = 2
case tab4 = 3
var description: String {
switch self {
case .tab1: return "Tab 1"
case .tab2: return "Tab 2"
case .tab3: return "Tab 3"
case .tab4: return "Tab 4"
}
}
var icon: String {
switch self {
case .tab1: return "1.circle"
case .tab2: return "2.circle"
case .tab3: return "3.circle"
case .tab4: return "4.circle"
}
}
}
struct ContentView: View {
@EnvironmentObject var tabOption: TabOption
var body: some View {
NavigationStack {
VStack {
// Content
Group {
switch tabOption.tab {
case TabItem.tab1:
SlideOneView()
case TabItem.tab2:
SlideTwoView()
case TabItem.tab3:
Slide3View()
case TabItem.tab4:
SlideFourView()
}
}
// Use a slide transition when changing the tab views
.transition(
.asymmetric(
insertion: .move(edge: tabOption.slideLeft ? .trailing : .leading),
removal: .move(edge: tabOption.slideLeft ? .leading : .trailing)
)
)
Spacer()
// Custom tab bar
HStack {
Spacer()
// Open tab 1
Button(action: {
withAnimation {
tabOption.changeTab(to: .tab1)
}
}) {
VStack {
Image(systemName: TabItem.tab1.icon).font(.title2)
Text(TabItem.tab1.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab1 ? .primary : .secondary)
.font(.title)
}
Spacer()
// Open tab 2
Button(action: {
withAnimation {
tabOption.changeTab(to: .tab2)
}
}) {
VStack {
Image(systemName: TabItem.tab2.icon).font(.title2)
Text(TabItem.tab2.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab2 ? .primary : .secondary)
.font(.title)
}
Spacer()
// Open tab 3
Button(action: {
withAnimation {
tabOption.changeTab(to: .tab3)
}
}) {
VStack {
Image(systemName: TabItem.tab3.icon).font(.title2)
Text(TabItem.tab3.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab3 ? .primary : .secondary)
.font(.title)
}
Spacer()
// Open tab 4
Button(action: {
tabOption.changeTab(to: .tab4)
}) {
VStack {
Image(systemName: TabItem.tab4.icon).font(.title2)
Text(TabItem.tab4.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab4 ? .primary : .secondary)
.font(.title)
}
Spacer()
} // HStack closure
.foregroundStyle(.blue)
.padding(.top, 5)
}
}
}
}
这是使用修改后的代码的问题的 GIF(对于 gif 压缩“挤压”屏幕图像表示歉意,但你明白了):
所以,有几件事。首先,您的视图代码中有太多逻辑。记住 DRY 原则(不要重复自己)。本质上,你正在使用
TabOption
,所以你的逻辑应该放在那里。我向TabOption
添加了一个函数,其中包含更改选项卡的所有逻辑:
class TabOption: ObservableObject {
@Published var tab: TabItem = .tab1
@Published var slideLeft: Bool = true
func changeTab(to newTab: TabItem) {
switch newTab.rawValue {
// case let allows you to make a comparison in the case statement
// This determines the direction is decreasing, so we want a right slide
case let t where t < tab.rawValue:
slideLeft = false
// This determines the direction is increasing, so we want a left slide
case let t where t > tab.rawValue:
slideLeft = true
// This determines that the user tapped this tab, so do nothing
default:
return
}
// We have determined the proper direction, so change tabs.
withAnimation(.easeInOut) {
tab = newTab
}
}
}
有了这个,事情就更容易推理了。最终,视图并没有按照您预期的方向滑动,因为您没有意识到您正在处理两个您想用它们做不同事情的视图。如果您有一张幻灯片,您希望通过移动其后缘退出原始视图,并移动其前缘的新视图。右边的幻灯片是相反的。你的过渡告诉他们从同一个方向进入和退出。你想要的是像这样的
.asymmetric()
过渡:
.transition(
.asymmetric(
insertion: .move(edge: tabOption.slideLeft ? .trailing : .leading),
removal: .move(edge: tabOption.slideLeft ? .leading : .trailing)
)
)
最后,要完成这个,你的每一个按钮动作都是这样的:
// Open tab 1
Button(action: {
tabOption.changeTab(to: .tab1)
}) {
...
}
编辑:
使用提供的代码,这是您评论后的结果:
如您所见,没有任何问题。请确保您采用了我的所有代码,而不仅仅是非对称转换。我不确定动画块中的
tabOption.slideLeft = true
是否也不会引起问题。
这是一个非常常见的 UX 要求,但很难做到正确。
在您的情况下,您试图让所有面板都归同一父级所有,并根据最新选择修改过渡边缘。我也尝试这样做,但这是我发现的:
不过,还是有办法解决的。这是为了使整体视图更具层次感,并将其成对构建。我已经在 iOS 14、15 和 16 上测试了以下内容,它在所有系统上都能可靠地运行。
import SwiftUI
/// An enum to describe the possible tab selections
enum TabItem: Int, CaseIterable, Comparable {
case tab1 = 0
case tab2 = 1
case tab3 = 2
case tab4 = 3
var description: String {
"Tab \(self.rawValue + 1)"
}
var icon: String {
"\(self.rawValue + 1).circle"
}
static func < (lhs: TabItem, rhs: TabItem) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
/// View modifier that applies a move transition on the leading edge
struct TransitionLeading: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
content.transition(.move(edge: .leading))
} else {
content.transition(
.asymmetric(
insertion: .move(edge: .leading),
removal: .move(edge: .trailing)
)
)
}
}
}
/// View modifier that applies a move transition on the trailing edge
struct TransitionTrailing: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
content.transition(.move(edge: .trailing))
} else {
content.transition(
.asymmetric(
insertion: .move(edge: .trailing),
removal: .move(edge: .leading)
)
)
}
}
}
/// A container for two alternative display panels
struct PanelPair<LeftContent: View, RightContent: View>: View {
/// The identifier for the left panel
private let leftTab: TabItem
/// Function that delivers the content for the left panel
private let leftContent: () -> LeftContent
/// Function that delivers the content for the right panel
private let rightContent: () -> RightContent
/// Binding to the state variable that controls the panel selection
@Binding private var selectedTab: TabItem
/// Creates a container for two alternative views
init(
leftTab: TabItem,
selectedTab: Binding<TabItem>,
leftContent: @escaping () -> LeftContent,
rightContent: @escaping () -> RightContent
) {
self.leftTab = leftTab
self._selectedTab = selectedTab
self.leftContent = leftContent
self.rightContent = rightContent
}
var body: some View {
// Important: the alternative content needs to be in a ZStack
ZStack {
if selectedTab <= leftTab {
leftContent()
.modifier(TransitionLeading())
} else {
rightContent()
.modifier(TransitionTrailing())
}
}
}
}
/// Working example
struct ContentView: View {
/// State variable that controls the panel selection
@State private var selectedTab = TabItem.tab1
/// Factory function for a panel relating to a particular tab
private func panel(tab: TabItem, color: Color) -> some View {
HStack {
Spacer()
VStack {
Spacer()
Text("Tab Content \(tab.rawValue + 1)")
.font(.largeTitle)
.foregroundColor(.white)
Spacer()
}
Spacer()
}
.background(color)
}
/// Callback for a tab button
private func changeTab(to: TabItem) {
withAnimation {
selectedTab = to
}
}
var body: some View {
VStack {
// The panels
// Panel 1 + others
PanelPair(
leftTab: .tab1,
selectedTab: $selectedTab,
leftContent: { panel(tab: .tab1, color: .blue) },
rightContent: {
// Panel 2 + others
PanelPair(
leftTab: .tab2,
selectedTab: $selectedTab,
leftContent: { panel(tab: .tab2, color: .green) },
rightContent: {
// Panels 3 + 4
// zIndex needed for back jumps to work correctly
PanelPair(
leftTab: .tab3,
selectedTab: $selectedTab,
leftContent: { panel(tab: .tab3, color: .purple) },
rightContent: { panel(tab: .tab4, color: .red).zIndex(1) }
)
.zIndex(1)
}
)
}
)
// The tab buttons
HStack {
ForEach(TabItem.allCases, id: \.self) { tabItem in
Button(action: { changeTab(to: tabItem) }) {
VStack {
Image(systemName: tabItem.icon)
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
Text(tabItem.description)
}
.frame(maxWidth: .infinity)
}
.foregroundColor(selectedTab == tabItem ? .primary : .secondary)
}
}
.padding()
}
}
}
这适用于所有过渡,向前和向后,包括跳跃。注意在最低内容上使用 zIndex - 这是为了确保向后跳转(例如从 4 到 1)正常工作。