Search code examples
swiftmacosswiftuiswift5macos-monterey

How do I cleanly delete elements from a Sidebar List in SwiftUI on macOS


I would like to give users the option to delete List elements from a SwiftUI app's sidebar in macOS.

Here's what I currently have:

import Foundation
import SwiftUI

@main
struct FoobarApp: App {

    @StateObject private var modelData = ModelData()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(modelData)
        }
        .commands {

        }
    }
}

class ModelData: ObservableObject {
    @Published var myLinks = [URL(string: "https://google.com")!, URL(string: "https://apple.com")!, URL(string: "https://amazon.com")!]
}


struct ContentView: View {

    @EnvironmentObject var modelData: ModelData

    @State private var selected: URL?

    var body: some View {
        NavigationView {

            List(selection: $selected) {
                Section(header: Text("Bookmarks")) {

                    ForEach(modelData.myLinks, id: \.self) { url in
                        NavigationLink(destination: Text("Viewing detailed data for: \(url)") ) {
                            Text(url.absoluteString)
                        }
                        .tag(url)
                    }

                }
            }
            .onDeleteCommand {
                if let selection = selected {
                    modelData.myLinks.remove(at: modelData.myLinks.firstIndex(of: selection)!)
                }
            }
            .toolbar {
                Button("Selected") {
                    print(selected ?? "Nothing selected")
                }
            }

            Text("Choose a link")

        }
        .frame(minWidth: 200, minHeight: 500)
    }
}

When I select one of the sidebar links and press the delete button on my keyboard, the link does get deleted, but the detail view doesn't always get cleared.

Here's a gif of what I'm referring to:

gif of weird behavior

I'd like for the detail view to revert back to the default text, Choose a link, after the user deletes a link. Is there a way to fix this?

Also, I noticed that ContentView.selected doesn't get cleared after a link gets deleted. Is this expected? (I included a Selected button which prints the contents of this variable)

I'm using macOS 12.2.1 & Xcode 13.2.1.

Thanks in advance.


Solution

  • The reason this does not work is because your Destinationview to the right does not get updated when the State in your sidebar changes. Move the logic to the viewmodel and create a designated View that holds the UI for the Details.

    1. move the selected var to your viewmodel:

      class ModelData: ObservableObject {
       @Published var myLinks = [
           URL(string: "https://google.com")!,
           URL(string: "https://apple.com")!,
           URL(string: "https://amazon.com")!]
       @Published var selected: URL?
      }
      
    2. create a new View for destination:

      struct DestinationView: View{
       @EnvironmentObject var modelData: ModelData
       var body: some View{
      
           if let selected = modelData.selected{
               Text("Viewing detailed data for: \(selected)")
           }else{
               Text("Choose a link")
           }
       }
      }
      
    3. change navigationview destination:

      NavigationLink(destination: DestinationView() ) {
          Text(url.absoluteString)
      }
      
    4. change your ondelete modifier:

       if let selection = modelData.selected {
          modelData.myLinks.remove(at: modelData.myLinks.firstIndex(of: selection)!)
       }
       modelData.selected = nil