I have a simple NavigationSplitView
where the user can select from a list of people, and then a disclouse group of their favorite books appears in the content column and they then can select the book they're currently reading. However, when I pass a @State
property called selectedBook
as a binding to the disclosure group view struct, it doesn't update when I select a new reader. Here is a video of it: https://youtube.com/shorts/J96uPqx9_rU?feature=share and the code:
import SwiftUI
import SwiftData
@Model
class Person {
var name: String = ""
@Relationship(deleteRule: .cascade, inverse: \Book.customer) var books: [Book]?
init(name: String) {
self.name = name
}
}
@Model
class Book {
var name: String = ""
var customer: Person?
init(name: String) {
self.name = name
}
}
struct MainView: View {
@Environment(\.modelContext) private var modelContext
@Query private var queriedPersons: [Person]
@State private var columnVisibility: NavigationSplitViewVisibility = .automatic
@State private var selectedPerson: Person?
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
List(selection: $selectedPerson) {
ForEach(queriedPersons) { customer in
NavigationLink(customer.name, value: customer)
}
}
.listStyle(.insetGrouped)
}
content: {
if let selectedPerson = selectedPerson {
PersonView(selectedPerson: selectedPerson)
}
}
detail: {
EmptyView()
}
.task {
do {
try modelContext.delete(model: Person.self)
let billPerson = Person(name: "Bill")
modelContext.insert(billPerson)
billPerson.books = [
Book(name: "Catcher in the Rye"),
Book(name: "Liar's Poker"),
Book(name: "The Odyssey")
]
let mikePerson = Person(name: "Mike")
modelContext.insert(mikePerson)
mikePerson.books = [
Book(name: "Red Dragon"),
Book(name: "Steve Jobs"),
Book(name: "The Great Gatsby")
]
} catch {
fatalError(error.localizedDescription)
}
}
.onFirstAppear {
columnVisibility = .all
}
}
}
struct PersonView: View {
private(set) var selectedPerson: Person
@State private var selectedBook: Book?
var body: some View {
List {
BooksDisclosure(books: selectedPerson.books, selectedBook: $selectedBook)
}
.listStyle(.insetGrouped)
}
}
struct BooksDisclosure: View {
private(set) var books: [Book]?
@Binding var selectedBook: Book?
@Query private var queriedBooks: [Book]
@State var disclosureIsExpanded: Bool = false
private var filteredAndSortedBooks: [Book] {
queriedBooks.filter { book in
books?.contains(book) == true
}
.sorted { lhs, rhs in
lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
}
}
var body: some View {
DisclosureGroup(isExpanded: $disclosureIsExpanded) {
ForEach(filteredAndSortedBooks) { book in
HStack {
Button(action: {
if selectedBook != book {
selectedBook = book
}
if disclosureIsExpanded != true {
disclosureIsExpanded = false
}
}) {
Text(book.name)
}
.buttonStyle(PlainButtonStyle())
if selectedBook == book {
Image(systemName: "checkmark")
}
Spacer()
}
}
} label: {
Text("Currently Reading: ")
if let selectedBook = selectedBook {
Text(selectedBook.name)
} else {
Text("None")
}
}
.onAppear {
selectedBook = filteredAndSortedBooks.first
}
}
}
My first thought was that it was the thing from Apple's release notes:
Conditional views in columns of NavigationSplitView fail to update on some state changes. (91311311) Workaround: Wrap the contents of the column in a ZStack.
I've tried this though at numerous points in the code and it's made no difference. From what I've read, I think it's possible that I may need to use @Bindable
instead of @Binding
for selectedBook
in BooksDisclosure
(apparently post iOS 17 non-primitive types need to be passed as @Bindable
instead of a @Binding
), however when I change it, all hell breaks loose with the compiler. Apparently it can't be an optional anymore and don't understand the proper syntax to be able to convert it from an optional to a non optional and it's throwing errors when I try to assign to it (yet wrappedValue
doesn't work). Not even sure I'm heading into right direction with this.
So was just wondering in general if anyone has any insight into why this isn't working now after I switch to NavigationSplitView
when it was working fine on a NavigationStack
.
I think the confusion occurred for me because I switched from a NavigationStack
to a NavigationSplitView
, and now PersonView
would become omnipresent as opposed to being reloaded everytime it was pushed on the stack, so I had to make sure that I was updating selectedBook
when selectedPerson
changed. So now I'm passing selectedPerson
to BooksDisclosure
, and adding this to the DisclosureGroup
:
.onChange(of: self.selectedPerson){
selectedBook = filteredAndSortedBooks.first
}
Maybe should've included this in my MRE, but in the actual code Book
has a property called lastSelectedDate
that gets updated whenever a Book
gets selected, and this is how I'm determining what Book
to set as selected when a Person
is selected (as opposed to selecting the first in the array which is weak from a UI perspective).