I have an array of sessions. Each session has an array of records. There are sessions in NavigationSplitView in sidebar. There is a table of records in detail view. When user click on a session new table is shown in detail view. I want to keep record selection for every table/session. For now if I select 5th row in 1st session and then switch to 2nd session 5th row gets selected.
struct ContentView: View {
@ObservedObject var model: Model
@State var selection: TestSession.ID?
var body: some View {
if model.sessions.count > 0 {
NavigationSplitView(sidebar: {
List(selection: $selection) {
ForEach(model.sessions, id: \.id) { session in
Text("\(session.name)")
}
}
}, detail: {
if selection != nil {
let session = model[selection!]
if session != nil {
SessionView(model: model, session: session!, selection: session!.selectedRecord)
}
}
})
} else {
Text("No sessions")
}
}
}
struct SessionView: View {
@ObservedObject var model: Model
var session: TestSession
@State var selection: TestRecord.ID?
var body: some View {
VStack {
Text("Session \(session.id) - \(session.name) - \(session.records.count)")
Text("Selected row: \(session.selectedRecord)")
Table(of: TestRecord.self, selection: $selection) {
TableColumn("Id") { rec in
Text("\(String(rec.id))")
.font(.system(.body, design: .monospaced))
}
TableColumn("Value") { rec in
Text("\(String(rec.value))")
.font(.system(.body, design: .monospaced))
}
} rows: {
ForEach(session.records) { rec in
TableRow(rec)
}
}
Spacer()
}
.onChange(of: selection) { oldValue, newValue in
model.setSelectedRecord(selection, for: session.id)
}
}
}
class Model: ObservableObject {
@Published var sessions: [TestSession] = []
init() {
let n = Int.random(in: 5..<10)
for i in 0..<n {
var s = TestSession(name: "session \(i + 1)")
let m = Int.random(in: 10..<20)
for j in 0..<m {
s.addRecord("\(j + 1)")
}
self.sessions.append(s)
}
}
subscript(index: UUID) -> TestSession? {
for i in 0..<self.sessions.count {
if self.sessions[i].id == index {
return self.sessions[i]
}
}
return nil
}
func setSelectedRecord(_ index: Int?, for session: TestSession.ID) {
for i in 0..<self.sessions.count {
if self.sessions[i].id == session {
return self.sessions[i].selectedRecord = index
}
}
}
}
struct TestSession: Identifiable
{
let id = UUID()
let name: String
var records: [TestRecord] = []
var selectedRecord: Int?
init(name: String) {
self.name = name
}
mutating func addRecord(_ value: String) {
let r = TestRecord(id: self.records.count, value: String(self.records.count))
self.records.append(r)
}
}
struct TestRecord: Identifiable {
let id: Int
let value: String
}
I save selected row in session structure but cannot restore it when table is loaded.
When you do
SessionView(model: model, session: session!, selection: session!.selectedRecord)
You are initialising a @State
of SessionView
(selection
) from outside. This almost always does not do what you intend to do. Initialising @State
only works once throughout a view's lifetime. After that, it will stay unchanged until something in SessionView
changes it. Changing the navigation split view side bar list selection won't change it.
This is why you should always initialise @State
inline, using a property initialiser. This is also why @State
should be marked private
, so that you don't accidentally initialise it via a parameter.
Since selection
comes from the outside and SessionView
can change it, it should be a @Binding
, not @State
.
struct SessionView: View {
@Binding var selection: TestRecord.ID?
...
}
Instead of setSelectedRecord
, move that logic into the setter of the subscript
. This allows you to form a Binding
from the subscript.
// in Model...
subscript(index: UUID) -> TestSession? {
get {
sessions.first(where: { $0.id == index })
}
set {
if let newValue, let i = sessions.firstIndex(where: { $0.id == newValue.id }) {
sessions[i] = newValue
}
}
}
// in ContentView, this is how you would pass the Binding
detail: {
if let selection, let session = Binding($model[selection]) {
SessionView(model: model, session: session.wrappedValue, selection: session.selectedRecord)
}
}
You can now remove the .onChange(of: selection)
modifier as well.
Alternatively, you can add a non-@State
property to SessionView
:
@State private var selection: TestRecord.ID?
let selectedRecord: TestRecord.ID? // ContentView can pass session.selectedRecord to this
detail: {
if let selection, let session = model[selection] {
SessionView(model: model, session: session, selectedRecord: session.selectedRecord)
}
}
Then you can use onChange
in SessionView
to manually update this:
.onChange(of: selectedRecord) { _, newValue in
selection = newValue
}
.onChange(of: selection) { _, newValue in
model.setSelectedRecord(newValue, for: session.id)
}