SwiftUI TabView PageTabViewStyle 防止更改选项卡?

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

我在 SwiftUI 中的

TabView
中有一个
PageViewTabStyle
,这样我就可以从一个页面滑动到另一个页面。我想要一个设置来“锁定”当前视图,这样用户就无法滑动。谷歌搜索和阅读文档并没有给我带来任何明显的结果,所以我希望 SO 上的专家可以帮助我。

简而言之,我的代码看起来像

TabView {
   ForEach(0..<5) { idx in
      Text("Cell: \(idx)")
   }
}
.tabViewStyle(PageTabViewStyle())

我找到了

disabled
属性,但似乎整个视图上的所有点击事件都被忽略 - 我只是想防止用户切换选项卡(或者,在这种特殊情况下,滑动或按下页面点来切换页面)。我已经尝试了here的解决方案,其中
gesture
属性设置为
nil
,但这似乎并没有真正阻止滑动手势更改页面(不过,
indexDisplayMode
位很好! )

非常感谢任何帮助!谢谢!

ios swiftui swiftui-tabview
5个回答
18
投票

参考文献中的解决方案,只是滑动不是被

gesture(nil)
阻挡,而是被
gesture(DragGesture())
阻挡。并且视图应该是全选项卡内容视图范围,就像

    TabView {
      ForEach(0..<5) { idx in
        Text("Cell: \(idx)")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .contentShape(Rectangle())
                .gesture(DragGesture())      // this blocks swipe
      }
    }
    .tabViewStyle(PageTabViewStyle())

使用 Xcode 12.1 / iOS 14.1 进行测试

* 当然,它可以有条件,如https://stackoverflow.com/a/63170431/12299030


9
投票

要阻止 TabView 中的所有滑动手势,您必须使用

.simultaneousGesture(DragGesture())
来阻止子视图中的所有滑动手势

TabView {
          ForEach(0..<5) { idx in
            Text("Cell: \(idx)")
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .contentShape(Rectangle())
                    .simultaneousGesture(DragGesture())
          }
        }
        .tabViewStyle(PageTabViewStyle())

1
投票

对我有用的解决方案是这个。它禁止通过滑动来更改选项卡,并且当我在某些屏幕上使用

List .onDelete
时,它会在屏幕上保持拖动手势启用。

仅适用于 iOS 16

@State private var selectedTab = 1

TabView(selection: $selectedTab) {
    Text("Tab 1")
        .tag(0)
        .toolbar(.hidden, for: .tabBar)
    Text("Tab 2")
        .tag(1)
        .toolbar(.hidden, for: .tabBar)
    Text("Tab 3")
        .tag(2)
        .toolbar(.hidden, for: .tabBar)
}

0
投票

我决定通过一些反思来推出自己的解决方案。它支持

if
语句(条件视图)和
ForEach
视图,以及使用
.tag()
修饰符进行识别。

private enum PagingTransition {
    case next, previous
    var value: AnyTransition {
        switch self {
        case .next:
            return AnyTransition.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
        case .previous:
            return AnyTransition.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing))
        }
    }
}

private func isOptional(_ instance: Any) -> Bool {
    let mirror = Mirror(reflecting: instance)
    let style = mirror.displayStyle
    return style == .optional
}

/// Erases generics to get views out of `ForEach`
fileprivate protocol ViewGeneratable {
    func generateViews() -> [any View]
}

extension ForEach: ViewGeneratable {
    func generateViews() -> [any View] {
        self.data.map { self.content($0) as! any View }
    }
}

/// A paging `TabView` replacement that doesn't allow for the user to interact
/// Follows SwiftUI calling conventions as best as possible with dirty reflection
/// https://www.fivestars.blog/articles/inspecting-views/
struct RestrictedPagingView<SelectionType: Hashable & Comparable, Content: View>: View {
    let selection: SelectionType
    @State private var selectionInternal: SelectionType
    @State private var transition: AnyTransition = PagingTransition.next.value
    private var views: [SelectionType: any View] = [:]
    
    init(selection: SelectionType, @ViewBuilder content: () -> Content) {
        self.selection = selection
        self._selectionInternal = State(initialValue: selection)
        
        // Attempt reflection
        generateViews(from: content(), withBaseTag: selection)
    }
    
    /// This is the most big brain shit I've coded in a long time
    /// Reflects SwiftUI views and puts them in a dictionary to use within the paging view
    private mutating func generateViews(from instance: Any, withBaseTag baseTag: SelectionType) {
        let mirror = Mirror(reflecting: instance)
        
        // Is this a tuple view?
        if let value = mirror.descendant("value") {
            // Yes, so call this function recusrively until it isn't
            let count = Mirror(reflecting: value).children.count
            for i in 0..<count {
                generateViews(from: mirror.descendant("value", ".\(i)")!, withBaseTag: baseTag)
            }
        } else if isOptional(instance) {
            // This is an Optional, so check if it has a value
            if let child = mirror.children.first?.value {
                // It does, send it back through the function
                generateViews(from: child, withBaseTag: baseTag)
            }
        } else if let content = mirror.descendant("content") {
            // This is a ForEach loop, so unwrap and deal with all views separately
            if mirror.descendant("contentID") != nil {
                for view in (instance as! ViewGeneratable).generateViews() {
                    generateViews(from: view, withBaseTag: baseTag)
                }
                return
            }
            // This is a tagged view, extract the tag and the content and put them in the dictionary
            let tag: SelectionType = mirror.descendant("modifier", "value", "tagged") as! SelectionType

            views[tag] = (content as! any View)
        } else {
            // Just insert the view with a baseline tag
            views[baseTag] = (instance as! any View)
        }
    }
    
    // TODO: Handle removed conditional views more gracefully
    var body: some View {
        ForEach(views.keys.sorted(by: >), id: \.self) { idx in
            if idx == selectionInternal {
                AnyView(views[idx]!)
                    .transition(transition)
            }
        }
        .onChange(of: selection) { newSelection in
            if newSelection > selectionInternal {
                transition = PagingTransition.next.value
            } else {
                transition = PagingTransition.previous.value
            }
            withAnimation(.easeInOut(duration: 0.25)) {
                selectionInternal = newSelection
            }
        }
    }
}

private struct RestrictedPagingViewPreview: View {
    @State private var index = 1
    @State private var allow = false
    
    var body: some View {
        VStack {
            RestrictedPagingView(selection: index) {
                ZStack {
                    Rectangle().foregroundColor(.blue)
                    Text("Hi")
                }.tag(1)
                ZStack {
                    Rectangle().foregroundColor(.green)
                    Text("Second")
                }.tag(2)
                ZStack {
                    Rectangle().foregroundColor(.red)
                    Text("Third")
                }.tag(3)
                ZStack {
                    Rectangle().foregroundColor(.yellow)
                    Button("FOURTH") {
                        print("button activated")
                    }
                }.tag(4)
                if allow {
                    ZStack {
                        Rectangle().foregroundColor(.orange)
                        Text("Should be hidden (5)")
                    }.tag(5)

                    ZStack {
                        Rectangle().foregroundColor(.purple)
                        Text("Should be hidden (6)")
                    }.tag(6)
                }
                ForEach(7..<11, id: \.self) { tagVal in
                    ZStack {
                        Rectangle().foregroundColor(.cyan)
                        Text("This view is generated by a ForEach loop! (\(tagVal))")
                    }
                    .tag(tagVal)
                }
            }
            .border(Color.green)
            Button("INCR") {
                index += 1
            }
            Button("INCR 2") {
                index += 2
            }
            Button("DECR") {
                index -= 1
            }
            Button("DECR 2") {
                index -= 2
            }
            Toggle("Show", isOn: $allow)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .border(Color.red)
    }
}

struct RestrictedPagingView_Previews: PreviewProvider {
    static var previews: some View {
        RestrictedPagingViewPreview()
    }
}

0
投票

与 TabView 相比,它可能看起来不同的方向,但我通过简单地使用

VStack
+ 过渡 + 编程更改来实现良好的结果

尝试这样的事情:


@State private var selectedIndex: Int = 0

VStack {
 switch selectedIndex {
  case 0:
    FirstView().tag(0)
      .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
  case 1:
    SecondView().tag(1)
      .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
   case 2:
     ThirdView().tag(2)
      .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
// ....
   default:
     Text("")
  }
}

// You need some logic to change the selectedIndex. 
// You can implement this here, our pass it as Binding to the views

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