I figured out the correct answer, and it is all to do with PreferenceKeys. Given that the accent color lives in the environment (according to the WWDC talks, anyway) it seems a rather roundabout way of changing this, but needs must and all that.
It ultimately took four pieces:
- A @State variable in the root view (containing the NavigationView) whose value would be passed to the .accentColor() modifier on the navigation view.
- A PreferenceKey type with a value matching the parameter for .accentColor() (e.g. a Color?).
- An onPreferenceChange() modifier on the navigation view that resets the state variable to the preference value.
- A .preference(key:value:) modifier on the pushed view setting the new color.
With this, when the detail view is pushed it will set the preference, the top-level view will react by updating its state, and SwiftUI will respond to that by re-fetching the top level view, which will get the new accent color. When you pop the detail view, the preference value will be reset to the value used on the root view, which is the default (nil), so the accent color resets. Of course, everything animates nicely. Yay!
Note that this works for iPhone where the detail view replaces the master list. On iPad, or another place where the master and detail views coexist, you'd likely have to detect that situation somehow and behave differently. Maybe the presentationMode value in the environment would do that?
Sample code, being a slightly modifed Master-Detail project template:
import SwiftUI
private let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .medium
return dateFormatter
}()
struct AccentColorPreferenceKey: PreferenceKey {
typealias Value = Color?
static func reduce(value: inout Color?, nextValue: () -> Color?) {
guard let next = nextValue() else { return }
value = next
}
}
struct ContentView: View {
@State private var dates = [Date]()
@State private var navAccentColor: Color? = nil
var body: some View {
NavigationView {
MasterView(dates: $dates)
.navigationBarTitle(Text("Master"))
.navigationBarItems(
leading: EditButton(),
trailing: Button(
action: {
withAnimation { self.dates.insert(Date(), at: 0) }
}
) {
Image(systemName: "plus")
}
)
DetailView()
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.accentColor(navAccentColor)
.onPreferenceChange(AccentColorPreferenceKey.self) {
self.navAccentColor = $0
}
}
}
struct MasterView: View {
@Binding var dates: [Date]
var body: some View {
List {
ForEach(dates, id: \.self) { date in
NavigationLink(
destination: DetailView(selectedDate: date)
) {
Text("\(date, formatter: dateFormatter)")
}
}.onDelete { indices in
indices.forEach { self.dates.remove(at: $0) }
}
}
}
}
struct DetailView: View {
var selectedDate: Date?
var body: some View {
ZStack {
Rectangle()
.fill(Color(red: 0.3, green: 0.3, blue: 1.0))
.edgesIgnoringSafeArea(.all)
Group {
if selectedDate != nil {
Text("\(selectedDate!, formatter: dateFormatter)")
.foregroundColor(.accentColor)
} else {
Text("Detail view content goes here")
}
}
}
.preference(key: AccentColorPreferenceKey.self, value: Color.white)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}