Hello, community and Apple engineers. I need your help.
Our app has the following issue: NavigationStack pushes a view twice if the NavigationStack is inside TabView and NavigationStack uses a navigation path of custom Hashable elements.
Our app works with issues in Xcode 18 Beta 13 + iOS 18.0. The same issue happened on previous beta versions of Xcode 18.
The issue isn’t represented in iOS 17.x and everything worked well before iOS 18.0 beta releases.
I was able to represent the same issue in a clear project with two simple views. I will paste the code below.
Several notes:
We use a centralised routing system in our app where all possible routes for navigation path are implemented in a View extension called withAppRouter().
We have a enum RouterDestination that contains all possible routes and is resolved in withAppRouter() extension.
We use Router class that contains @Published var path: [RouterDestination] = [] and this @Published property is bound to NavigationStack. In the real app, we need to have an access to this path property for programmatic navigation purposes.
Our app uses @ObservableObject / @StateObject approach.
import SwiftUI
struct ContentView: View {
@StateObject private var router = Router()
var body: some View {
TabView {
NavigationStack(path: $router.path) {
NavigationLink(value: RouterDestination.next, label: {
Label("Next", systemImage: "plus.circle.fill")
})
.withAppRouter()
}
}
}
}
enum RouterDestination: Hashable {
case next
}
struct SecondView: View {
var body: some View {
Text("Screen 2")
}
}
class Router: ObservableObject {
@Published var path: [RouterDestination] = []
}
extension View {
func withAppRouter() -> some View {
navigationDestination(for: RouterDestination.self) { destination in
switch destination {
case .next:
return SecondView()
}
}
}
}
Below you can see the GIF with the issue:
What I tried to do:
Use iOS 17+ @Observable approach. It didn’t help.
Using @State var path: [RouterDestination] = [] directly inside View seems to help. But it is not what we want as we need this property to be @Published and located inside Router class where we can get an access to it, and use for programmatic navigation if needed.
I ask Apple engineers to help with that, please, and if it is a bug of iOS 18 beta, then please fix it in the next versions of iOS 18.0
Post
Replies
Boosts
Views
Activity
Our app has an architecture based on ViewModels.
Currently, we are working on migrating from the ObservableObject protocol to the Observable macro (iOS 17+).
The official docs about this are available here: https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro
Our ViewModels that were previously annotated with @StateObject now use just @State, as recommended in the official docs.
Some of our screens (a screen is a SwiftUI view with a corresponding ViewModel) are presented modally. We expect that after dismissing a SwiftUI view that was presented modally, its corresponding ViewModel, which is owned by this view (via the @State modifier), will be deinitialized. However, it seems there is a memory leak, as the ViewModel is not deinitialized after a modal view is dismissed.
Here's a simple code where ModalView is presented modally (through the .sheet modifier), and ModalViewModel, which is a @State of ModalView, is never deinitialized.
import SwiftUI
import Observation
@Observable
final class ModalViewModel {
init() {
print("Simple ViewModel Inited")
}
deinit {
print("Simple ViewModel Deinited") // never called
}
}
struct ModalView: View {
@State var viewModel: ModalViewModel = ModalViewModel()
let closeButtonClosure: () -> Void
var body: some View {
ZStack {
Color.yellow
.ignoresSafeArea()
Button("Close") {
closeButtonClosure()
}
}
}
}
struct ContentView: View {
@State var presentSheet: Bool = false
var body: some View {
Button("Present sheet modally") {
self.presentSheet = true
}
.sheet(isPresented: $presentSheet) {
ModalView {
self.presentSheet = false
}
}
}
}
#Preview {
ContentView()
}
Is this a bug in the iOS 17 beta version or intended behavior? Is it possible to build a relationship between the View and ViewModel in a way where the ViewModel will be deinitialized after the View is dismissed?
Thank you in advance for the help.