Properly Implementing “Open Recent” Menu for a SwiftUI non Document-Based macOS Music Player

I've already searched extensively on Apple Developer Forums and Stack Overflow, and didn't really find what I need or I missed it.

I'm developing a macOS music player app that uses cuesheet files paired with audio files. The core functionality is working, opening and playing files works without issues, but I'm struggling with implementing proper file handling features due to my limited experience with UI frameworks.

The current state of my app is intentionally simple:

  • Single window interface representing a music player with track list
  • Opening cuesheet files changes the “disc” and updates the window
  • Built with SwiftUI (not AppKit)
  • Not created as a Document-Based app since the user doesn't need to edit, save, or work with multiple documents simultaneously

What I Need to Implement:

  • Open Recent menu that actually works
  • Recent files accessible from Dock menu
  • Opening cuesheet files from Finder
  • Drag-and-drop cuesheet files onto app window (lower priority)

Problems I've Encountered:

I've tried multiple approaches but never achieved full functionality. The closest I got was an “Open Recent” menu that:

  • Only updated after app relaunch
  • Its drop-down kept closing while music was playing

My Questions

  • Is it possible purely in SwiftUI?
  • Is there documentation I'm missing? I feel like I might be overcomplicating this.

I'm open to alternative approaches if my current direction isn't ideal:

  • Should I redesign as Document-Based? Since I apparently need NSDocumentController, would it be better to start with a Document-Based app template and disable unwanted features, and how?
  • Should I mix AppKit with SwiftUI? While SwiftUI has been wonderful for my main window, it's becoming frustrating for other UI parts, especially menus. Would using AppKit for menus and keeping SwiftUI for the main interface be a reasonable approach?

I thought this would be straightforward:

  • Customize the .fileImporter with the proper logic of what to do with the files
  • Call NSDocumentController.shared.noteNewRecentDocumentURL(url) so the Open Recent menu is created a populated with opened files.

Maybe it really is that simple and I've gotten lost down a rabbit hole? UI programming is quite new to me, so I might be missing something obvious.

Any guidance, code examples, or pointing me toward the right documentation would be greatly appreciated!

Note for the forum moderators: I had to remove the extension of a cuesheet file in my text, it got flagged as sensitive. I don't know why...

So, here's solution I found.

The following code works partially. The Open Recent menu is populated properly without quiting the app.

But here's the issue: If the user opens it when the player is playing music, the Open Recent menus closes.

I just found out that, apparently, it was linked to the fact that recentDiscsModel is a @StateObject. When the player is playing music, the PlayerView() is constantly updating.

However, I have no idea how to solve this...

(I use .fileImporter and not NSOpenPanel)

Here's the code :

  • @main struct MyApp: App
@main
struct MyApp: App {

    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    @StateObject private var recentDiscsModel = RecentDiscsModel.shared

    var body: some Scene {
        Window("Player", id: "main-window") {
            PlayerView()
        }
        .windowToolbarStyle(.unified)
        .windowStyle(.hiddenTitleBar)
        .windowResizability(.contentSize)
        .commands {
            CommandGroup(replacing: .newItem) { }
            
            CommandGroup(after: .newItem) {
                Button("Open...") {
                    NotificationCenter.default.post(name: .openDocumentRequested, object: nil)
                }
                .keyboardShortcut("O", modifiers: .command)

                if recentDiscsModel.recentDocs.isEmpty == false {
                    Menu("Open Recent") {
                        ForEach(NSDocumentController.shared.recentDocumentURLs, id: \.self) { url in
                            Button(url.lastPathComponent) {
                                global.insertDisc(at: url)
                            }
                        }
                        Divider()
                        Button("Clear Menu") {
                            NSDocumentController.shared.clearRecentDocuments(nil)
                            recentDiscsModel.refresh()
                        }
                    }
                }
            }

            PlayBackMenu()
         }
    }
}
  • class RecentDiscsModel: ObservableObject
class RecentDiscsModel: ObservableObject {
	
    static let shared = RecentDiscsModel()

    @Published private(set) var recentDocs: [URL] = []

    private init() {
        refresh()
    }

    func refresh() {
        let newDocs = NSDocumentController.shared.recentDocumentURLs
        if newDocs != recentDocs {
            recentDocs = newDocs
        }
    }
}
  • class Globals: ObservableObject
class Globals: ObservableObject {

    static let shared = MCGlobals()
    init() {}
     
    @Published var errorMessage = ""
    @Published var errorDetails = ""
    @Published var showingErrorAlert = false

    
    
    func handleFileImport(result: Result<URL, Error>) {
        switch result {
        case .success(let url):

            guard url.startAccessingSecurityScopedResource() else {
                errorMessage = "Unable to access file"
                errorDetails = "Permission denied"
                showingErrorAlert = true
                return
            }
            
            defer { url.stopAccessingSecurityScopedResource() }


            insertDisc(at: url)
            
        case .failure(let error):
            errorMessage = "Failed to import file"
            errorDetails = error.localizedDescription
            showingErrorAlert = true
        }
    }


    func insertDisc(at url: URL, startPlayback: Bool = false) {
        do {
            try AudioDiscPlayer.shared.insertDisc(at: url, startPlayback: startPlayback)
            
            recentFilesViewModel.addRecentFile(path: url.path)

            NSDocumentController.shared.noteNewRecentDocumentURL(url)

        } catch {

            let nsError = error as NSError
            errorMessage = nsError.localizedDescription
            
            let reason = nsError.localizedFailureReason ?? ""
            let recovery = nsError.localizedRecoverySuggestion ?? ""
            errorDetails = "\n\n\(reason)\n\n\(recovery)".trimmingCharacters(in: .whitespacesAndNewlines)
            showingErrorAlert = true
        }
        RecentDiscsModel.shared.refresh()
    }
}

Properly Implementing “Open Recent” Menu for a SwiftUI non Document-Based macOS Music Player
 
 
Q