Switching tabs in TabView "resets" matchedGeometryEffect() in one of the tab's views

Viewed 200

I have a three-tab tab view, and in the first tab another tab view, this time set to PageTabViewStyle(). In this swipeable tab view is TabHeader, which has all the tab names, or "categories", in an HStack, and one capsule under the tab names which moves according to the selected tab using matchedGeometryEffect() – and it works like a charm. Here's some of my code:

// The parent view
@StateObject private var tabModel = TabBarViewModel()
// [...]
TabView(selection: $tabModel.selectedTab) {
    History()
        .tag(Tab.history) // the tab is actually hidden since I'm using a custom tab bar, which is why there aren't any `tabItem`
    Text("Tap view")
        .tag(Tab.tap)
    Text("Profile view")
        .tag(Tab.profile)
}

With the History view as:

@State private var currentCategory: Category

init() {
    _currentCategory = State(initialValue: categories[0]) // user makes their own categories, so can't use enum
}
// [...]
VStack(spacing: 0) {
    NavigationBar(title: Text("History")) { // Custom navigation bar
        TabHeader(categories, selection: $currentCategory, name: \.name)
    }
    TabView(selection: $currentCategory) {
        ForEach(categories, id: \.self) { category in
            HistoryList(category: category)
                .tag(category)
        }
    }
    .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}

TabHeader as:

@Binding var currentTab: Category
@Namespace private var animation
// [...]
ScrollView(.horizontal, showsIndicators: false) {
    HStack(spacing: 20) {
        ForEach(tabs, id: \.self) { tab in
            VStack(alignment: .leading, spacing: 5) {
                Button {
                    withAnimation {
                        currentTab = tab
                    }
                } label: {
                    Text(tabNames[tab] ?? "")
                        .foregroundColor(currentTab == tab ? .blue : .gray)
                }
                TabCapsule(for: tab, current: currentTab, animation: animation)
            }
        }
    }
}
.animation(.linear(duration: 0.1))

And finally, TabCapsule:

let tab: Category
let currentTab: Category
let animation: Namespace.ID
// [...]
if currentTab == tab {
    Capsule()
        .frame(width: 20, height: 2)
        .foregroundColor(.blue)
        .matchedGeometryEffect(id: "Capsule", in: animation, properties: .position)
} else {
    Capsule()
        .frame(width: 20, height: 2)
        .hidden()
}

The History tab view and its matchedGeometryEffect() capsule work perfectly. However, it works less well when incorporated into the parent tab view. Indeed, when I switch from another tab - say, the "Tap" or "Profile" tab (as seen in the parent view) – to the History tab, and then swipe once from right to left to go to another category, the capsule's movement is just once choppy, almost non-existent - just the usual fade in fade out. However, when I swipe again to another category, or back to the previous category, the movement is smooth and normal, and stays smooth. It seems like coming back to the History tab "resets" the matchedGeometryEffect().

I thought it might be that SwiftUI destroys the History view when switching to another tab. But no, the category and even position in the ScrollView stays the same. I also tried moving the currentCategory state variable into its own ObservableObject and use a StateObject, but it made no difference.

What do you think is causing this behaviour?

0 Answers
Related