Можно ли добавить собственный переход к дочерним элементам TabView при изменении страницы?

У меня есть контролируемое представление вкладок, подобное этому

    TabView(selection: $activeTab) {
      IntroductionView().tag(1)
      HomeView().tag(2)
      SettingsView().tag(3)
      ProfileView().tag(4)
    }

Я хочу добавить переход к каждому виду при изменении activeTab, скажем, простом перекрестном затухании непрозрачности?

Пробовал что-то вроде

    TabView(selection: $activeTab) {
      IntroductionView().tag(1).transition(.opacity)
      HomeView().tag(2).transition(.opacity)
      SettingsView().tag(3).transition(.opacity)
      ProfileView().tag(4).transition(.opacity)
    }
    .animation(.smooth, value: activeTab)

Но, похоже, это не имеет никакого эффекта. Есть несколько ответов, например этот, которые добавляют какую-то анимацию прокрутки/слайда через .tabViewStyle(.page(indexDisplayMode: .never)) модификатор, но это не совсем то, что мне нужно.

Возможно, есть способ использовать/расширить UIKit, чтобы добавить переход с перекрестным затуханием?

TabView просто использует UITabBarController под капотом, чтобы вы могли проанализировать это и сделать что-то подобное. Или вы можете написать свою UIViewControllerRepresentable обертку UITabBarController. Или создайте свой собственный вид вкладок, используя SwiftUI. Есть много вариантов.
Sweeper 18.05.2024 14:33

Нативный TabView не поддерживает переходы. Но TabView легко заменить на ZStack + пользовательскую панель вкладок и использовать if/else для переключения контента. Затем вы можете использовать любой переход, который вам нравится.

Benzy Neez 18.05.2024 17:54

@BenzyNeez Я делал это заранее, но проблема в том, что оно не сохраняет состояние представления, которое вы «оставили», то есть, если оно было прокручено, например.

Ilja 18.05.2024 18:09

Чтобы сохранить состояние, оно должно оставаться в памяти. Таким образом, вместо использования if/else вы можете просто изменить непрозрачность, чтобы сделать одно представление видимым, а другие невидимыми. Это по-прежнему позволит переход непрозрачности, но не слайд-переход. Для раздвижных панелей этот ответ может помочь.

Benzy Neez 18.05.2024 18:37

@BenzyNeez Не повредит ли это производительности? Это одна из причин, по которой я решил использовать TabView {}: у меня сложилось впечатление, что он каким-то образом разгружает представление, но сохраняет его состояние. Если это не так, я действительно мог бы просто использовать ZStack и переключить непрозрачность/отключить представления.

Ilja 18.05.2024 18:49

Возможно, вы правы насчет TabView сохранения состояния, при этом разгружая просмотры. Я попробовал тест с некоторым List в качестве дочернего контента, и положение прокрутки сохранилось, хотя isAppear и isDisappear вызываются при переключении. Однако я не знаю, в какой степени на самом деле разгружаются представления и освобождаются ресурсы.

Benzy Neez 18.05.2024 21:09
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
59
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Если вы хотите иметь возможность указывать Transition для каждой из вкладок с помощью модификатора представления, как вы это сделали в примере кода, лучше всего написать свой собственный TabView.

Вот адаптированная версия пользовательского «представления вкладок», которое я написал здесь, благодаря которому вкладки больше похожи на вкладки. Я также добавил модификатор tabTransition, чтобы вы могли выбрать Transition.

struct CustomTabView<Content: View, Selection: Hashable>: View {
    
    @Binding var selectedTab: Selection
    
    @ViewBuilder let content: () -> Content
    
    var body: some View {
        Extract(content) { views in
            ForEach(views) { view in
                if view.id(as: Selection.self) == selectedTab {
                    view
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                        .transition(view[TabTransitionTrait.self])
                }
            }
        }
        .safeAreaInset(edge: .bottom) {
            HStack {
                Spacer()
                ExtractMulti(content) { views in
                    ForEach(views) { view in
                        Group {
                            if let label = view[CustomTabItemTrait.self] {
                                label
                            } else {
                                Text("Unnamed")
                            }
                        }
                        .onTapGesture {
                            if let selection = view.id(as: Selection.self) {
                                selectedTab = selection
                            }
                        }
                        .foregroundStyle(
                            view.id(as: Selection.self) == selectedTab ?
                                AnyShapeStyle(Color.accentColor) : AnyShapeStyle(.opacity(1))
                        )
                        Spacer()
                    }
                }
            }
        }
    }
}

extension View {
    func customTabItem<Content: View>(@ViewBuilder content: () -> Content) -> some View {
        _trait(CustomTabItemTrait.self, AnyView(content()))
    }
}

struct CustomTabItemTrait: _ViewTraitKey {
    static let defaultValue: AnyView? = nil
}

extension View {
    func tabTransition<T: Transition>(_ transition: T) -> some View {
        _trait(TabTransitionTrait.self, AnyTransition(transition))
    }
}

struct TabTransitionTrait: _ViewTraitKey {
    static let defaultValue: AnyTransition = .identity
}

// MARK: View Extractor - https://github.com/GeorgeElsham/ViewExtractor

public struct Extract<Content: View, ViewsContent: View>: View {
    let content: () -> Content
    let views: (Views) -> ViewsContent

    public init(_ content: Content, @ViewBuilder views: @escaping (Views) -> ViewsContent) {
        self.content = { content }
        self.views = views
    }

    public init(@ViewBuilder _ content: @escaping () -> Content, @ViewBuilder views: @escaping (Views) -> ViewsContent) {
        self.content = content
        self.views = views
    }

    public var body: some View {
        _VariadicView.Tree(
            UnaryViewRoot(views: views),
            content: content
        )
    }
}

fileprivate struct UnaryViewRoot<Content: View>: _VariadicView_UnaryViewRoot {
    let views: (Views) -> Content

    func body(children: Views) -> some View {
        views(children)
    }
}

public struct ExtractMulti<Content: View, ViewsContent: View>: View {
    let content: () -> Content
    let views: (Views) -> ViewsContent

    public init(_ content: Content, @ViewBuilder views: @escaping (Views) -> ViewsContent) {
        self.content = { content }
        self.views = views
    }

    public init(@ViewBuilder _ content: @escaping () -> Content, @ViewBuilder views: @escaping (Views) -> ViewsContent) {
        self.content = content
        self.views = views
    }

    public var body: some View {
        _VariadicView.Tree(
            MultiViewRoot(views: views),
            content: content
        )
    }
}

fileprivate struct MultiViewRoot<Content: View>: _VariadicView_MultiViewRoot {
    let views: (Views) -> Content

    func body(children: Views) -> some View {
        views(children)
    }
}

public typealias Views = _VariadicView.Children

Пример использования:

struct ContentView: View {
    @State var selectedTab = 0
    var body: some View {
        CustomTabView(selectedTab: $selectedTab.animation()) {
            Color.blue
                .id(0) // you must use .id instead of .tag to specify the selection value
                .customTabItem {
                    Label("Baz", systemImage: "circle")
                }
                .tabTransition(.push(from: .leading))
            
            Color.yellow
                .id(1)
                .customTabItem {
                    Label("Foo", systemImage: "globe")
                }
                .tabTransition(.push(from: .bottom))
            
            Color.green
                .id(2)
                .customTabItem {
                    Label("Bar", systemImage: "rectangle")
                }
                .tabTransition(.opacity)
        }
    }
}

При этом не сохраняются состояния каждой вкладки, например положение прокрутки. Если это нежелательно, вы не можете использовать модификатор transition (и, следовательно, встроенные Transition). Вам нужно будет сделать свою собственную анимацию для переходов. Например, анимацию непрозрачности можно реализовать с помощью .opacity:

// in CustomTabView.body
ZStack {
    ExtractMulti(content) { views in
        ForEach(views) { view in
            view
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .opacity(view.id(as: Selection.self) == selectedTab ? 1 : 0)
        }
    }
}
.safeAreaInset(edge: .bottom) {
    // the rest of the tab bar goes here...
}

Для движущегося перехода вы можете присвоить каждой вкладке значок .offset в зависимости от того, выбрана она (view.id(as: Selection.self) == selectedTab) или нет, и так далее.

Я попробую, спасибо. На первый взгляд, у меня есть один дополнительный вопрос: сохранится ли это состояние просмотра, например положение прокрутки, когда пользователь уходит и возвращается на вкладку?

Ilja 18.05.2024 18:13

@Ilja Нет, потому что модификатор transition анимируется только тогда, когда представление исчезает/появляется. Вы можете легко изменить это, чтобы сохранить состояния просмотра, используя .opacity для управления видимостью вместо if view.id(as: Selection.self) == selectedTab. Но тогда вы ограничены переходом непрозрачности. Другими словами, вы не сможете использовать встроенные Transition и вам придется создавать собственные анимации.

Sweeper 20.05.2024 09:00

Спасибо, принял это, так как решение действительно сработало, но в конце концов решил использовать TabView в том виде, в каком он есть. Кажется, он предлагает множество оптимизаций и сохранения состояния из коробки, которые я мог бы воспроизвести с помощью условного рендеринга или непрозрачности + zstack для пример

Ilja 20.05.2024 09:20

Другие вопросы по теме