Its amazing how fast you can prototype in SwiftUI, it seems like magic. At the same time, when you start to get into the details, it can be infuriating…
Let me explain…
After completing the prototype last week and deciding on the key features for the app I started implementing them. One of the very basic ones was having a button on the side bar to create new content. Well, basic in AppKit/UIKit, with SwiftUI on macOS not so much!
Here’s what I was trying to do:
1. From the SideView (the column at the very left in the image above) you would be able to click on a Button that would create a new element.
2. This new element would appear as a new row (see above Added Content in the middle view) and it would become automatically selected.
3. The detail view, the one that shows the detail of the element, would automatically show all contents (see the rightmost view above).
In AppKit this would be trivial, not so much in SwiftUI. Also, because most of the recommendations out there are for iOS and not for macOS, it was quite a fun ride.
Here’s how I ended up doing it:
First, the SideView:
@SceneStorage(“sidebarSelection”) var selection: String?
@SceneStorage(“selectedElementId”) var selectedElementId: Int?
var body: some View {
NavigationView{
List(selection: $selection) {
Button(action: {
let addedElement = coreDataAddsElementHere()
selectedElementId = Int(addedElement.id)
}) {
Text(“+ New Element”)
}
.foregroundColor(.blue)
Label(“Category 1”, systemImage: “someIcon”).tag(tagThatWillBeUsedForSelection)
Label(“Category 2”, systemImage: “someIcon”).tag(tagThatWillBeUsedForSelection2)
Label(“Category 3”, systemImage: “someIcon”).tag(tagThatWillBeUsedForSelection3)
}
ElementList(typeOfList: selection ?? defaultOptionGoesHere)
}
}
You’ll see I’m not using NavigationLink here, if you’re building a macOS app you should only use that for the RowList. To share information between the SideView and the RowList you should use SceneStorage.
Selection is used, as its name implies, to enable the selection of rows in combination with the tag property on each label: when both selection and tag match, the relevant row in the sideBar is selected.
Second, the RowList:
@SceneStorage(“selectedElementId”) var selectedElementId: Int?
var body: some View {
NavigationView{
List(){
ForEach(elements){element in
NavigationLink(
destination: DetailView(contentsOfView: element),
tag: Int(element.id),
selection: $selectedElementId
) {
ElementListRow(title: firstLine, subtitle: otherContent, isPinned: element.pinned)
}
//Default View on Mac
DetailView(contentsOfView: nil)
}
}
}
}
You’ll see we capture the shared selectedElementId, which allows us to select the right row automatically (note the tag to the element id and the selection binding).
Here we can use the NavigationLink as we’re dealing with a row to detail view model.
And finally, the detail view (optional):
@SceneStorage(“selectedElementId”) var selectedElementId: Int?
var body: some View {
if selectedElementId == nil{
Text(“Please select an element or create a new one”)
}else{
[…]
}
}
I use this specifically to make sure when a Core Data object is delated, if it was being shown in the detail view, I can invalidate the view. As before, I leverage the selectedElementId: if the selected element is deleted from any view, that SceneStorage id will be set to nil. The detail view detects it and immediately stops showing the cached data.
***
And that’s it! It’s quite streightforward once you know how to do it, unfortunately most information out there is for iOS so I found myself spending way more time than anticipated.
Hope it helps others, if you have suggestions on how to do this better please do let me know!
Do reach me @MarcMasVi on Twitter or marc.maset@hey.com
Marc