我在 SwiftUI 中的
TabView
中有一个 PageViewTabStyle
,这样我就可以从一个页面滑动到另一个页面。我想要一个设置来“锁定”当前视图,这样用户就无法滑动。谷歌搜索和阅读文档并没有给我带来任何明显的结果,所以我希望 SO 上的专家可以帮助我。
简而言之,我的代码看起来像
TabView {
ForEach(0..<5) { idx in
Text("Cell: \(idx)")
}
}
.tabViewStyle(PageTabViewStyle())
我找到了
disabled
属性,但似乎整个视图上的所有点击事件都被忽略 - 我只是想防止用户切换选项卡(或者,在这种特殊情况下,滑动或按下页面点来切换页面)。我已经尝试了here的解决方案,其中gesture
属性设置为nil
,但这似乎并没有真正阻止滑动手势更改页面(不过,indexDisplayMode
位很好! )
非常感谢任何帮助!谢谢!
参考文献中的解决方案,只是滑动不是被
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
要阻止 TabView 中的所有滑动手势,您必须使用
.simultaneousGesture(DragGesture())
来阻止子视图中的所有滑动手势
TabView {
ForEach(0..<5) { idx in
Text("Cell: \(idx)")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.simultaneousGesture(DragGesture())
}
}
.tabViewStyle(PageTabViewStyle())
对我有用的解决方案是这个。它禁止通过滑动来更改选项卡,并且当我在某些屏幕上使用
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)
}
我决定通过一些反思来推出自己的解决方案。它支持
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()
}
}
与 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