Simple use case: A list of States with a Recents section that shows those States you have navigated to recently. When a link is tapped, the animation begins but is then aborted presumably as a result of the onAppear handler in the detail view changing the recents property of the model -- which is observed by the framework. Log messages indicate the framework is unhappy, but unclear how to make it happy short of a ~2 second delay before adding to recents...
import SwiftUI
class USState: Identifiable{
typealias ID = String
var id: ID
var name: String
init(_ name: String, id: String){
self.id = id
self.name = name
}
}
/** The model is a small set of us states for example purposes.
It also publishes a recents property which has a lifo stack of states that have been viewed.
*/
class StateModel: ObservableObject{
var states: [USState]
var stateMap: [USState.ID: USState]
@Published var recents = [USState]()
init(){
states = [
USState("California", id: "CA"),
USState("Georgia", id: "GA"),
USState("New York", id: "NY"),
USState("New Jersey", id: "NJ"),
USState("Montana", id: "MT")
]
stateMap = [USState.ID: USState]()
for state in states{
stateMap[state.id] = state
}
}
func addRecent(_ state: USState){
recents.removeAll(where: {$0.id == state.id})
recents.insert(state, at: 0)
}
func allExceptRecent() -> [USState]{
states.filter{ state in
recents.contains{
state.id == $0.id
} == false
}
}
}
/** A simple view to serve as the destination of a state link
*/
struct StateView: View{
@EnvironmentObject var stateModel: StateModel
var usState: USState
var body: some View{
Text(usState.name)
.onAppear{
DispatchQueue.main.async {
withAnimation {
stateModel.addRecent(usState)
}
}
}
}
}
/** A list of states broken into two sections, those that have been recently viewed, and the remainder.
Desired behavior is that when a state is tapped, it should navigate to its respective detail view and update the list of recents.
The issue is that the recents updating appears to confuse SwiftUI and the navigation is aborted.
*/
struct SidebarBounce: View {
@EnvironmentObject var model: StateModel
@SceneStorage("selectionStore") private var selectionStore: USState.ID?
struct Header: View{
var text: String
var body: some View{
Text(text)
.font(.headline)
.padding()
}
}
struct Row: View{
var text: String
var body: some View{
VStack{
HStack{
Text(text)
.padding([.leading, .trailing])
.padding([.top, .bottom], 8)
Spacer()
}
Divider()
}
}
}
var body: some View{
ScrollView{
LazyVStack(alignment: .leading, spacing: 0){
Section(
header: Header(text: "Recent")
){
ForEach(model.recents){place in
NavigationLink(
destination: StateView(usState: place),
tag: place.id,
selection: $selectionStore
){
Row(text: place.name)
}
.id("Recent \(place.id)")
}
}
Section(
header: Header(text: "All")
){
ForEach(model.allExceptRecent()){place in
NavigationLink(
destination: StateView(usState: place),
tag: place.id,
selection: $selectionStore
){
Row(text: place.name)
}
.id("All \(place.id)")
}
}
}
}
}
}
struct BounceWrap: View{
let model = StateModel()
var body: some View{
NavigationView{
SidebarBounce()
.navigationTitle("Aborted Navigation")
Text("Nothing Selected")
}
.environmentObject(model)
}
}
@main
struct DemoApp: App {
var body: some Scene {
WindowGroup {
BounceWrap()
}
}
}
Note: This must be run as an app (iPhone or simulator) rather than in preview.