|
I'm only a few months into writing my first SwiftUI MacOS app. One of my hardest issues was how to connect MacOS menu items in a document-based app. I needed to have the MenuBar items be configured FROM the currrently active Document, and direct their action requests TO that same document. I found numerous sources that describe using focusedValue, but I found those to be limiting, syntacticly ugly and obscure. I finally found a mechanism that is simple, easy to explain and (most of all) very functional. I just want to share in case it helps someone else. The example here is a direct edit of the Document-Based App template from Xcode. The sample (full listing at the end) allows you to open multiple text documents and provides a button in each window that opens a sheet on the window. If you open several documents at once, each of them gets its own button and they each can show the sheet or not. Simple so far. The real deal is that from the App level I can create a menu bar button that triggers showing the Sheet only on the currently selected document.
This contains a @Published property showSheet. All code that would normally reference a simple @State property now must refer to docState.showSheet. This may seem like making things more complicated, but it has its advantages. In the ContentView we create the docState with
and put it into the environment using
Now any subview can import the global state values with
The subview can use "docState.showSheet.toggle()" to show the sheet as well. But the environment is only accessible from sub-views and can't be accessed at the application level, that is, by the parent of ContentView. We can get around this by the final part of this method . We export a focused-scene-dependent reference to the docState. Here's how... In ContentView, we add the modifier
This looks just like exporting to the environment. Apple's documentation on this is sketchy, but I figure it works like this... The top level application maintains a collection of references to objects, keyed by the scene (i.e. document ContentView) they came from. When someone asks for the "current" object, they are handed the object that came from the currently focused Scene. You can see this in the App document. That includes this line:
That tells the top-level app to create an optional object "docState" of the given class. It's optional because there won't be such an object if no document window is active - docState will be nil. All references to this structure must unwrap the value before usage. Now that the App level has access the the currrent document's state, we can use it, for example, to create document specific menus. In this sample, I added a DocMenu item to the menuBar, with a Button that activates the sheet. Furthermore, I disable that button if there is no active document window.
That's it! We have connected the App level code to the currently active document, using shared state variables. Activating the menu button Show Sheet will open a sheet ONLY on the currently selected document. And if you close all the documents, the Show Sheet button will be grayed-out. What next? The sky is the limit!
All of this happens pretty much automatically, once you have exported the document state to the app. It is making my app a lot better. Regards to HWS for posting so much useful information. Hopefully this will give someone else the ability to make a cleaner MacOS app. Tom Coates :::/ Here are the complete versions of the sample code. I would highlight the changes from Xcode's template, but I'm still figuring out the editing tools here.
|
SPONSORED Still waiting on your CI build? Speed it up ~3x with Blaze - change one line, pay less, keep your existing GitHub workflows. First 25 HWS readers to use code HACKING at checkout get 50% off the first year. Try it now for free!
Sponsor Hacking with Swift and reach the world's largest Swift community!
You need to create an account or log in to reply.
All interactions here are governed by our code of conduct.
Link copied to your pasteboard.