Я пытаюсь реализовать поведение в TabView
, когда пользователь несколько раз нажимает на одну и ту же вкладку, например, в приложении iOS AppStore. Первое касание: переключиться на этот вид, второе касание: открыть корневой каталог, третье касание: при необходимости прокрутить вверх.
Приведенный ниже код отлично работает для переключения, и 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)
}
)
}
}
С чем я борюсь, так это как сказать One
или Two
, что он был нажат во второй или третий раз? (Как всплывать и прокручивать, это не проблема).
Я видел это: TabView, tabItem: запуск кода при выборе или добавление onTapGesture, но это не объясняет, как запускать код в одном из представлений.
Какие-либо предложения?
Вы можете записывать дополнительные нажатия (с одинаковым значением) в массив. Счетчик массива дает вам количество нажатий на одну и ту же вкладку.
Обновлено: теперь с явной структурой подвида.
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")
}
}
}
только что отредактированный, передайте tapState в подпредставления
Отлично, с вашим окончательным редактированием.
Хотя ответ @ChrisR действительно ответил на мой вопрос, я не мог понять следующий шаг, то есть логику, когда открывать корень или прокручивать вверх в зависимости от количества нажатий для SubView
. После долгих прочтений, проб и ошибок я недавно наткнулся на эту статью: https://notificare.com/blog/2022/11/25/a-better-tabview-in-swiftui/
Вдохновленный этой статьей, но с некоторыми изменениями, я придумал следующее, которое делает именно то, что я искал.
Два основных изменения:
EmptyView
с id
добавляется в качестве первой (но невидимой) строки в List
, которая будет использоваться proxy.scrollTo()
в качестве привязки.@StateObject var appState
, в котором хранятся пути навигации для подпредставлений, я добавил пути как отдельные свойства @State
. Это позволяет избежать предупреждения Update NavigationAuthority bound path tried to update multiple times per frame.
.Надеюсь, это будет полезно для кого-то.
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)")
}
}
}
}
Это хорошая идея, чтобы зафиксировать количество нажатий. Но как мне передать это представлениям внутри
TabView
, чтобы они могли соответственно реагировать?