简体   繁体   English

SwiftUI TabView - 在连续点击后在子视图中运行代码

[英]SwiftUI TabView - run code in subview after sequential taps

I am trying to implement the behavior in a TabView when the user taps the same tab multiple times, such as in the iOS AppStore app.当用户多次点击同一个选项卡时,我试图在TabView中实现该行为,例如在 iOS AppStore 应用程序中。 First tap: switch to that view, second tap: pop to root, third tap: scroll to the top if needed.第一次点击:切换到该视图,第二次点击:弹出到根,第三次点击:如果需要滚动到顶部。

The code below works fine for switching and didTap() is called for every tap.下面的代码适用于切换,每次点击都会调用didTap()

import SwiftUI

enum Tab: String {
    case one
    case two
}

struct AppView: View {
    @State private var activeTab = Tab.one

    var body: some View {
        TabView(selection: $activeTab.onChange(didTap)) {
            One()
                .tabItem {
                    Label("one", systemImage: "1.lane")
                }
                .tag(Tab.one)

            Two()
                .tabItem {
                    Label("two", systemImage: "2.lane")
                }
                .tag(Tab.two)
        }
    }
    
    func didTap(to value: Tab) {
        print(value) // this captures every tap
    }
}

extension Binding {
    func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {
        Binding(
            get: { self.wrappedValue },
            set: { newValue in
                self.wrappedValue = newValue
                handler(newValue)
            }
        )
    }
}

What I am struggling with, is how to tell either One or Two that it was tapped for a second or third time?我正在苦苦挣扎的是,如何告诉OneTwo它已被第二次或第三次点击? (How to pop and scroll is not the issue). (如何弹出和滚动不是问题)。

I have seen this: TabView, tabItem: running code on selection or adding an onTapGesture but it doesn't explain how to run code in one of the views.我看过这个: TabView, tabItem: running code on selection or adding an onTapGesture但它没有解释如何在其中一个视图中运行代码。

Any suggestions?有什么建议么?

You can record additional taps (of same value) in an array.您可以在数组中记录额外的抽头(具有相同的值)。 The array count gives you the number of taps on the same Tab.数组计数为您提供同一选项卡上的点击次数。

EDIT: now with explicit subview struct.编辑:现在有明确的子视图结构。

struct ContentView: View {
    
    @State private var activeTab = Tab.one

    @State private var tapState: [Tab] = [Tab.one] // because .one is default
    
    var body: some View {
        TabView(selection: $activeTab.onChange(didTap)) {
            
            SubView(title: "One", tapCount: tapState.count)
                .tabItem {
                    Label("one", systemImage: "1.lane")
                }
                .tag(Tab.one)

            SubView(title: "Two", tapCount: tapState.count)
                .tabItem {
                    Label("two", systemImage: "2.lane")
                }
                .tag(Tab.two)
        }
    }
    
    func didTap(to value: Tab) {
        print(value) // this captures every tap
        if tapState.last == value {
            tapState.append(value) // apped next tap if same value
            print("tapped \(tapState.count) times")
        } else {
            tapState = [value] // reset tap state to new tab selection
        }
    }
}


struct SubView: View {
    
    let title: String
    let tapCount: Int
    
    var body: some View {
        VStack {
            Text("Subview \(title)").font(.title)
            Text("tapped \(tapCount) times")
        }
    }
}

Although the answer by @ChrisR below did answer my question, I couldn't figure out the next step, ie the logic when to pop or scroll based on the number of taps in a SubView .虽然下面@ChrisR 的回答确实回答了我的问题,但我无法弄清楚下一步,即根据SubView中的点击次数弹出或滚动的逻辑。 After lots of reading and trial and error, I recently came across this article: https://notificare.com/blog/2022/11/25/a-better-tabview-in-swiftui/经过大量阅读和反复试验,我最近看到了这篇文章: https://notificare.com/blog/2022/11/25/a-better-tabview-in-swiftui/

Inspired by this article, but with some modifications, I came up with the following which does exactly what I was looking for.受这篇文章的启发,但经过一些修改,我想出了以下内容,它完全符合我的要求。

The two main changes are:两个主要变化是:

  1. An EmptyView with an id is added as the first row in the List to be used by proxy.scrollTo() .带有idEmptyView添加为List中的第一行,供proxy.scrollTo()使用。
  2. Instead of the global @StateObject var appState that stores the navigation paths for the subviews, I added the paths as separate @State properties.我没有使用存储子视图导航路径的全局@StateObject var appState ,而是将路径添加为单独的@State属性。 This avoids the Update NavigationAuthority bound path tried to update multiple times per frame.这避免了Update NavigationAuthority bound path tried to update multiple times per frame. warning.警告。

Hopefully this is helpful for someone.希望这对某人有帮助。

enum Tab: String {
    case one
    case two
}

struct ContentView: View {
    @State var selectedTab = Tab.one
    @State var oneNavigationPath = NavigationPath()
    @State var twoNavigationPath = NavigationPath()

    var body: some View {
        ScrollViewReader { proxy in
            TabView(selection: tabViewSelectionBinding(proxy: proxy)) {
                SubView(title: "One", path: $oneNavigationPath)
                    .tabItem {
                        Label("one", systemImage: "1.lane")
                    }
                    .tag(Tab.one)

                SubView(title: "Two", path: $twoNavigationPath)
                    .tabItem {
                        Label("two", systemImage: "2.lane")
                    }
                    .tag(Tab.two)
            }
        }
    }

    private func tabViewSelectionBinding(proxy: ScrollViewProxy) -> Binding<Tab> {
        Binding<Tab>(
            get: { selectedTab },
            set: { newValue in
                if selectedTab == newValue {
                    switch selectedTab {
                    case .one:
                        if oneNavigationPath.isEmpty {
                            withAnimation {
                                proxy.scrollTo(Tab.one, anchor: .bottom)
                            }
                        } else {
                            withAnimation {
                                oneNavigationPath = NavigationPath()
                            }
                        }
                    case .two:
                        if twoNavigationPath.isEmpty {
                            withAnimation {
                                proxy.scrollTo(Tab.two, anchor: .bottom)
                            }
                        } else {
                            withAnimation {
                                twoNavigationPath = NavigationPath()
                            }
                        }
                    }
                }

                selectedTab = newValue
            }
        )
    }
}

struct SubView: View {
    let title: String
    let items = Array(1 ... 100)

    @Binding var path: NavigationPath

    var body: some View {
        NavigationStack(path: $path) {
            List {
                EmptyView()
                    .id(Tab(rawValue: title.lowercased()))

                ForEach(items, id: \.self) { item in
                    NavigationLink(value: item) {
                        Text("Item \(item)")
                    }
                }
            }
            .navigationTitle(title)
            .navigationDestination(for: Int.self) { item in
                Text("Item \(item)")
            }
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM