У меня есть контролируемое представление вкладок, подобное этому
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 не поддерживает переходы. Но TabView легко заменить на ZStack + пользовательскую панель вкладок и использовать if/else для переключения контента. Затем вы можете использовать любой переход, который вам нравится.
@BenzyNeez Я делал это заранее, но проблема в том, что оно не сохраняет состояние представления, которое вы «оставили», то есть, если оно было прокручено, например.
Чтобы сохранить состояние, оно должно оставаться в памяти. Таким образом, вместо использования if/else вы можете просто изменить непрозрачность, чтобы сделать одно представление видимым, а другие невидимыми. Это по-прежнему позволит переход непрозрачности, но не слайд-переход. Для раздвижных панелей этот ответ может помочь.
@BenzyNeez Не повредит ли это производительности? Это одна из причин, по которой я решил использовать TabView {}: у меня сложилось впечатление, что он каким-то образом разгружает представление, но сохраняет его состояние. Если это не так, я действительно мог бы просто использовать ZStack и переключить непрозрачность/отключить представления.
Возможно, вы правы насчет TabView сохранения состояния, при этом разгружая просмотры. Я попробовал тест с некоторым List в качестве дочернего контента, и положение прокрутки сохранилось, хотя isAppear и isDisappear вызываются при переключении. Однако я не знаю, в какой степени на самом деле разгружаются представления и освобождаются ресурсы.





Если вы хотите иметь возможность указывать 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 Нет, потому что модификатор transition анимируется только тогда, когда представление исчезает/появляется. Вы можете легко изменить это, чтобы сохранить состояния просмотра, используя .opacity для управления видимостью вместо if view.id(as: Selection.self) == selectedTab. Но тогда вы ограничены переходом непрозрачности. Другими словами, вы не сможете использовать встроенные Transition и вам придется создавать собственные анимации.
Спасибо, принял это, так как решение действительно сработало, но в конце концов решил использовать TabView в том виде, в каком он есть. Кажется, он предлагает множество оптимизаций и сохранения состояния из коробки, которые я мог бы воспроизвести с помощью условного рендеринга или непрозрачности + zstack для пример
TabViewпросто используетUITabBarControllerпод капотом, чтобы вы могли проанализировать это и сделать что-то подобное. Или вы можете написать своюUIViewControllerRepresentableоберткуUITabBarController. Или создайте свой собственный вид вкладок, используя SwiftUI. Есть много вариантов.