We’ve had an application on the AppStore for many years whose navigation bar styling has remained the same - white text & icons on top of a dark blue background. When the same application runs on iOS 18, this styling is incorrect (see screenshots).
On iOS 18, after a “back navigation” completes, the navigation bar title text color changes to black (when the device is in light mode) and the back button changes to the system blue. This occurs even though the NavigationStack's root view and all destination views are styled with:
.toolbarColorScheme(.dark, for: .navigationBar)
Has anyone else seen this behavior?
I've submitted FB14926996 with a movie and a sample application demonstrating the issue.
Post
Replies
Boosts
Views
Activity
I'm trying to manage state restoration in our application. I failed to get NavigationView/NavigationLink working in pre-iOS 16 SwiftUI. I'm now trying to get it working with NavigationStack/NavigationLink/navigationDestination with iOS 16. I've built a sample application that demonstrates the issue:
import SwiftUI
@main
struct PushMultipleViews7App: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
enum Route: Hashable, Codable {
case view1
case view2
case view3
case view4
}
struct ContentView: View {
@State
private var navigationPath: [Route]
@Environment(\.scenePhase)
private var scenePhase
init() {
guard let data = UserDefaults
.standard
.data(
forKey: "NavigationPath"
),
let navigationPath = try? JSONDecoder()
.decode(
[Route].self,
from: data
)
else {
self._navigationPath = State(
initialValue: []
)
print("Failed to read NavigationPath")
return
}
print("Read:",navigationPath)
self._navigationPath = State(
initialValue: navigationPath
)
}
var body: some View {
VStack {
NavigationStack(
path: $navigationPath
)
{
View1()
.navigationDestination(
for: Route.self
)
{ route in
switch route {
case .view1:
View1()
case .view2:
View2()
case .view3:
View3()
case .view4:
View4()
}
}
}
}
.onChange(
of: scenePhase
)
{ scenePhase in
guard case .inactive = scenePhase
else {
return
}
guard let data = try? JSONEncoder()
.encode(
navigationPath
)
else {
print("Failed to write NavigationPath")
return
}
print("Writing:",navigationPath)
UserDefaults
.standard
.setValue(
data,
forKey: "NavigationPath"
)
}
}
}
struct View1: View {
var body: some View {
VStack {
Text("Hello View1!")
NavigationLink(
value: Route.view2,
label: {
Text("Navigate to View2")
}
)
}
.navigationTitle("View 1")
}
}
struct View2: View {
var body: some View {
VStack {
Text("Hello View2!")
NavigationLink(
value: Route.view3,
label: {
Text("Navigate to View3")
}
)
}
.navigationTitle("View 2")
}
}
struct View3: View {
var body: some View {
VStack {
Text("Hello View3!")
NavigationLink(
value: Route.view4,
label: {
Text("Navigate to View4")
}
)
}
.navigationTitle("View 3")
}
}
struct View4: View {
var body: some View {
Text("Hello, View4!")
}
}```
To replicate the issue: from Xcode 14, run the app above in your favorite iOS 16 phone simulator. Tap on the NavigationLinks to navigate to View 4. Navigate to the Home Screen (put the app into the background). Wait a few seconds for UserDefaults to catch up. "Swipe up" to kill the app. Relaunch the app. View4 should be displaying...which is the expected behavior. Tap the "View 3" button (the "back" button) to display View3. As before, navigate to the Home Screen, wait a few seconds then "swipe up" to kill the app. Relaunch the app. Instead of displaying "View3", the app should be displaying View1. Evidence can be seen in the console when writing the navigation path to UserDefaults - it wrote "[]".
That's just one variation among lots of strange behavior I've seen trying to restore a NavigationStack's navigation path.
I've filed: FB10398702
I've tried several variations of this application: using @AppStorage and @SceneStorage instead of UserDefaults. I've also tried the sample code accompanying NavigationPath in Apple's documentation (though also based on UserDefaults).
Am I doing something incorrectly or is this a bug?
Thanks,
--David
We need to trigger a side effect on a publisher when the first subscriber subscribes and the last subscriber unsubscribes.
We're looking for the Combine equivalent of "doOnSubscribe" and "doOnUnsubscribe" familiar to ReactiveX users. We've used ".handleEvents", but we've been informed that it should only be used for debugging as it may not be thread safe.
Here's a solution for "doOnSubscribe" that utilizes "prepend". But to implement "doOnUnsubscribe", you cannot replace "prepend" with "append" as the upstream will never be cancelled without the side-effect - when the last subscriber unsubscribes, we need to trigger the side effect that ultimately tells the upstream publisher to complete.
extension Publishers {
public struct OnSubscribePublisher<P: Publisher>: Publisher where P.Failure == Never {
public typealias Output = P.Output
public typealias Failure = P.Failure
private let _transform: AnyPublisher<Output,Failure>
fileprivate init(
publisher: P,
work: @escaping () -> Void
)
{
self._transform = Deferred{ publisher }
.map { Optional($0) }
.prepend(
Deferred {
Just(
nil
)
.map { (_: Output?) in
work()
return nil
}
}
)
.compactMap { $0 }
.eraseToAnyPublisher()
}
public func receive<S>(
subscriber: S
) where
S: Subscriber,
S.Failure == Failure,
S.Input == Output
{
_transform
.receive(
subscriber: subscriber
)
}
}
}