I have a controlled Tab view like this
TabView(selection: $activeTab) {
IntroductionView().tag(1)
HomeView().tag(2)
SettingsView().tag(3)
ProfileView().tag(4)
}
I want to add transition to each of the views when activeTab
changes, say simple cross fade of opacity?
Tried something along the lines of
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)
But this seems to have no effect. There are some answers like this one that add some kind of scrollable / slide animation via .tabViewStyle(.page(indexDisplayMode: .never))
modifier, but that's not exactly what I'm after.
Perhaps there is a way to utilize / extend UIKit to add a cross fade transition?
If you want to be able to specify a Transition
for each of the tabs using a view modifier, like you did in the example code, it is best to write your own TabView
.
Here is an adapted version of the custom "tab view" I wrote here, that makes the tabs look more like tabs. I have also added a tabTransition
modifier to allow you to choose a 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
Example usage:
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)
}
}
}
This does not preserve the states of each tab, like scroll position. If this is undesirable, you cannot use the transition
modifier (and hence the built-in Transition
s). You would need to do your own animation for the transitions. For example, an opacity animation would be implemented with .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...
}
For a moving transition, you might give each tab an .offset
depending on whether it is selected (view.id(as: Selection.self) == selectedTab
) or not, and so on.