Any view that is content for the tabViewBottomAccessory API fails to retain its state as of the last couple of 26.1 betas (and RC). The loss of state happens (at least) when the currently selected tab is switched (filed as FB20901325).
Here's code to reproduce the issue:
struct ContentView: View {
@State private var selectedTab = TabSelection.one
enum TabSelection: Hashable {
case one, two
}
var body: some View {
TabView(selection: $selectedTab) {
Tab("One", systemImage: "1.circle", value: .one) {
BugExplanationView()
}
Tab("Two", systemImage: "2.circle", value: .two) {
BugExplanationView()
}
}
.tabViewBottomAccessory {
AccessoryView()
}
}
}
struct AccessoryView: View {
@State private var counter = 0 // This guy's state gets lost (as of iOS 26.1)
var body: some View {
Stepper("Counter: \(counter)", value: $counter)
.padding(.horizontal)
}
}
struct BugExplanationView: View {
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("(1) Manipulate the counter state")
Text("(2) Then switch tabs")
Text("BUG: The counter state gets unexpectedly reset!")
}
.multilineTextAlignment(.leading)
}
}
}
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Filed in Feedback as FB20772137
Zoom transition originating from inside tabViewBottomAccessory, when the binding passed to fullScreenCover's item is a Binding other than a "$-synthesized" binding, the animation fails with the following error (and crucially fails to perform the desired animation):
Starting a zoom transition from a nil view will trigger a fallback transition. To get the best possible teansition, be sure to provide a view that's visible and in a window.
What I want to do is pass a binding to a property inside an ObservableObject (or @Observable, but it doesn't matter) to hold the item representing the presentation. But this stopped working as of 26.1b4. It worked in 26.1b3 and in 26.0 (and 26.0.1)
Here's the gist of code that will reproduce the issue (I've omitted irrelevant details in the interest of brevity):
struct ContentView: View {
@Binding var presentation: PresentationDestination?
@Namespace private var animation
var body: some View {
// Omitted TabView stuff…
.tabViewBottomAccessory {
miniPlayer
.matchedTransitionSource(
id: "player",
in: animation
)
}
.fullScreenCover(
item: $presentation,
content: { _ in
fullScreenPlayer
.navigationTransition(
.zoom(
sourceID: "player",
in: animation
)
)
})
}
As you can see, ContentView takes a Binding to the presentation, but it matters how this binding is constructed.
This works:
@State private var presentation: PresentationDestination
…
ContentView(presentation: $presentation)
This fails (as does ObservableObject with @Published):
@Observable
class Router2 {
var presentation: PresentationDestination?
}
…
@State private var router2 = Router2()
…
ContentView(presentation: $router2.presentation)
Also, this fails:
@State private var presentation: PresentationDestination
…
ContentView(
presentation: .init(get: {
presentation
}, set: { newValue in
presentation = newValue
})
)
These differences are unexpected, of course. I consider this a regression in 26.1b4
I should add that if I move the source of the transition to somewhere outside tabViewBottomAccessory things seem to work fine.