I am working on a couple of Multiplatform apps that use a very similar 3 column layout - sidebar menu (or tabs for sizeClassHorizontal == .compact
), with a master list under each menu option and detail view based upon master list selection.
The apps use Core Data framework and NSPersistentCloudKitContainer
for backup and syncing across a user's devices. The apps use Codegen = Class Definition and I write extensions for each entity to prepare convenience methods, properties, comply with generic protocol conformance.
I am learning how to retrofit sectioned fetch requests into each master List
. Generally this is straightforward and I'm really happy with the result.
However, one particular master list is based upon an hierarchical data structure and I have investigated using the children:
parameter in List
to successfully create an hierarchical list.
So to be clear, the following code is based upon
@FetchRequest var gardens: FetchedResults<Garden>
(not a sectioned fetch request) and works well...
extension Garden {
var arrayGardenChildren: [Garden]? {
let setGardenChildren = self.children as? Set<Garden> ?? []
return setGardenChildren.sorted(by: { $0.name ?? " NO NAME" < $1.name ?? " NO NAME" })
}
}
and
@FetchRequest(sortDescriptors: [
SortDescriptor(\.name, order: .forward),
SortDescriptor(\.timeStampCreated, order: .reverse)
],
animation: .default
) var gardens: FetchedResults<Garden>
and
private func arrayGardens() -> [Garden] {
return Array(gardens)
}
and
List(arrayGardens(), children: \.arrayGardenChildren) { garden in
NavigationLink(destination: GardenDetail(...),
tag: garden.uuID!.uuidString,
selection: $appData.selectedGardens
) {
GardenRow(garden: garden,
selected: garden.uuID?.uuidString == appData.selectedGardens)
}
}
... perhaps obviously from this code, I am forced to convert the FetchedResults
to an Array
and the entity's children from an NSSet
to a sorted Array
- no big deal - a little extra code but it works fine and I'm happy.
On first impressions, the lists look great on macOS and iOS, however there are a few downsides to this implementation:
- expansion state cannot be set as
@SceneStorage
or therefore controlled as per the sidebar menu in the Apple Developer "Garden" sample app for the WWDC21 code-along "Building a Great Mac App with SwiftUI"; - for both iOS and macOS, there are no section headers, which are my preference when the list becomes longer;
- for both iOS and macOS, the bottom level items in each hierarchy tree include a disclosure chevron despite that those items have no children;
- on iOS, due to the navigation link for the detail view, there are double chevrons at the trailing edge of each row... the inner chevron is the link to the detail view and the outer chevron is the disclosure indicator. This appearance is not ideal and there are a few of ways I can adjust the UI to suit, but include the raw / unedited constraint here for clarity.
So I have continued experimenting with a sectioned fetch request.
As a starting point, the following code works well...
extension Garden {
@objc var sectionNameParentNil: String {
return self.name?.filter({ _ in self.parent == nil }) ?? "NO NAME"
}
}
where each instance of the entity Garden
has two relationships that allow the parent child arrangement, a To-One relationship named parent
and a To-Many relationship named children
(both with destination = Garden
).
...and...
SectionedFetchRequest(sectionIdentifier: \.sectionNameParentNil,
sortDescriptors: [
SortDescriptor(\.name, order: .forward),
SortDescriptor(\.timeStampCreated, order: .reverse)
],
animation: .default)
) var gardens: SectionedFetchResults<String, Garden>
...and...
List {
ForEach(gardens) { section in
Section(header: Text(section.id)) {
ForEach(section) { garden in
NavigationLink(destination: GardenDetail(...),
tag: garden.uuID!.uuidString,
selection: $appData.selectedGardens
) {
GardenRow(garden: garden,
selected: garden.uuID?.uuidString == appData.selectedGardens)
}
}
}
}
}
This is a good start and works well but only presents the top level elements of the hierarchical data set (where parent = nil).
I want to be able to include all children for each top level "Garden" and also mimic the expandable / collapsable function demonstrated in the above noted sample app.
I've done some googling and read a few articles, including "Displaying recursive data using OutlineGroup in SwiftUI" by one of my favourite SwiftUI bloggers.
Based upon my research and reading, I begun by replacing the second ForEach
line in the above block with...
OutlineGroup(section, children: \Garden.children ?? nil) { garden in
The compiler complains
"Cannot convert value of type 'ReferenceWritableKeyPath<Garden, NSSet?>?' to expected argument type 'KeyPath<SectionedFetchResults<String, Garden>.Section.Element, SectionedFetchResults<String, Garden>.Element?>' (aka 'KeyPath<Garden, Optional<SectionedFetchResults<String, Garden>.Section>>')".
My compiler interpretation skills are intermediate at best, but this is beyond me.
I've attempted a few different combinations to attempt to provide a suitable type, but all my attempts to date have caused compiler errors that I am not able to resolve. I'll try to include a few more below.
OutlineGroup(section, children: \.arrayGardenChildren) { garden in
causes the following compiler error
Key path value type '[Garden]?' cannot be converted to contextual type 'SectionedFetchResults<String, Garden>.Element?' (aka 'Optional<SectionedFetchResults<String, Garden>.Section>')
So from these messages I somehow need to provide a key path for SectionedFetchResults<String, Garden>.Element
.
The question for me is how?
Any suggestions please?