SwiftUI 双向移动转换在某些情况下以错误的方式移动

问题描述 投票:0回答:2

我的应用程序有四个主要功能区域,用户可以通过 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 压缩“挤压”屏幕图像表示歉意,但你明白了):

ios swiftui transition
2个回答
0
投票

所以,有几件事。首先,您的视图代码中有太多逻辑。记住 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
是否也不会引起问题。


0
投票

这是一个非常常见的 UX 要求,但很难做到正确。

在您的情况下,您试图让所有面板都归同一父级所有,并根据最新选择修改过渡边缘。我也尝试这样做,但这是我发现的:

  1. 与 iOS 14/15 相比,边缘在 iOS 16 中的工作方式似乎有所不同,至少对于删除案例而言是这样。
  2. 在 iOS 16(至少)中,我怀疑删除过渡的边缘在插入时已经定义。这意味着,试图让边缘依赖于在视图显示后可以改变的状态值是行不通的。
  3. 因此,为了让边缘在移除过渡时正确,您需要知道用户接下来要走哪条路。对于第一个和最后一个面板,这很容易,因为只有一条路可以走,但是对于中间的面板,您必须猜测/预测它。
  4. 如果您不能每次都正确预测下一个移动方向,那么如果所有面板都共享同一个父面板,那么中间面板将永远无法每次都正确工作。

不过,还是有办法解决的。这是为了使整体视图更具层次感,并将其成对构建。我已经在 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)正常工作。

© www.soinside.com 2019 - 2024. All rights reserved.