import SwiftUI
// MARK: - Data Section
enum Mode: String, CaseIterable, Identifiable {
case home = "Home"
case browse = "Browse"
case radio = "Radio"
var id: String { rawValue }
var icon: String {
switch self {
case .home:
return "house"
case .browse:
return "square.grid.2x2"
case .radio:
return "dot.radiowaves.left.and.right"
}
}
}
enum Library: String, CaseIterable, Identifiable {
case recentlyAdded = "Recently Added"
case artists = "Artists"
case albums = "Albums"
case songs = "Songs"
var id: String { rawValue }
var icon: String {
switch self {
case .recentlyAdded:
return "clock"
case .artists:
return "music.mic"
case .albums:
return "square.stack"
case .songs:
return "music.note"
}
}
}
@Observable
class MusicAppModel {
var selectedLibrary: String? { didSet {
if selectedLibrary != nil {
// we can't have multiple selections in the Sidebar
selectedMode = nil
print("selectedLibrary = \(selectedLibrary ?? "None")")
}
}}
var selectedMode: String? { didSet {
if selectedMode != nil {
// we can't have multiple selections in the Sidebar
selectedLibrary = nil
print("selectedMode = \(selectedMode ?? "None")")
}
}}
var displayedIcon: String?
// used for testing event propagation on sidebar selection property change
// can these be called using didSet for their respective properties with the new @Observable? Not sure
// For now these are being called when the values are detected as changed in the corresponding destination view
func onSelectedLibraryChange(newLibrary: Library) {
selectedLibrary = newLibrary.rawValue
displayedIcon = newLibrary.icon
}
// used for testing event propagation on sidebar selection property change
func onSelectedModeChange(newMode: Mode) {
print("mode change = \(newMode)")
selectedMode = newMode.rawValue
displayedIcon = newMode.icon
}
}
// MARK: - View Section
// your main WindowGroup() should call this view
struct ContentView: View {
@State private var appModel = MusicAppModel()
var body: some View {
SidebarExampleUsingUpdatedNavLink()
.environment(appModel)
}
}
struct ModeContentView: View {
@Environment(MusicAppModel.self) private var appModel
var mode: Mode
var body: some View {
HStack {
if let icon = appModel.displayedIcon {
Image(systemName: icon)
}
Text("Selected Mode: \(mode.rawValue)")
}
// onChange is not called the first time a mode is selected - must use .onAppear
.onChange(of: mode) { oldValue, newValue in
appModel.onSelectedModeChange(newMode: newValue)
}
.onAppear {
appModel.onSelectedModeChange(newMode: mode)
}
}
}
struct LibraryContentView: View {
@Environment(MusicAppModel.self) private var appModel
var library: Library
var body: some View {
HStack {
// this shows that our app model is getting property updates when selection changes
if let icon = appModel.displayedIcon {
Image(systemName: icon)
}
Text("Selected Library: \(library.rawValue)")
}
// onChange is not called the first time a library is selected - must use .onAppear
.onChange(of: library) { oldValue, newValue in
appModel.onSelectedLibraryChange(newLibrary: newValue)
}
.onAppear {
appModel.onSelectedLibraryChange(newLibrary: library)
}
}
}
struct SidebarExampleUsingUpdatedNavLink: View {
@Environment(MusicAppModel.self) private var appModel
var body: some View {
@Bindable var appModel = appModel
NavigationSplitView(sidebar: {
List {
Section("Mode", content: {
ForEach(Mode.allCases) {
mode in
NavigationLink(destination: {
ModeContentView(mode: mode)
.navigationTitle(mode.rawValue)
}, label: {
HStack {
Image(systemName: mode.icon)
Text(mode.rawValue)
Spacer()
}
})
}
})
Section("Libarary", content: {
ForEach(Library.allCases) {
library in
NavigationLink(destination: {
LibraryContentView(library: library)
.navigationTitle(library.rawValue)
}, label: {
HStack {
Image(systemName: library.icon)
Text(library.rawValue)
Spacer()
}
})
}
})
}
}, detail: {
VStack {
Text("Please select an item in the sidebar!")
.navigationTitle("Sidebar Selection Binding")
}
})
}
}
#Preview {
struct AppleMusicExamplePreview: View {
var body: some View {
ContentView()
.preferredColorScheme(.dark)
.frame(width: 800, height: 800)
}
}
return AppleMusicExamplePreview()
}
Post
Replies
Boosts
Views
Activity
I don't think I did a good job at explaining the actual issue. I think I have come up with a good solution that works for the new @Observable macro and also does not rely on the deprecated NavigationLink(tag, selection) API.
The issue is this:
It is common to show multiple lists in multipanel sidebar apps (i.e., Apple Music, Apple Mail) - and one way of keeping track of selection changes of items in the sidebar was to use the now deprecated NavigationLink(tag, selection) API - as this could be used to bind to a variable in your appModel that keeps track of user selections in the Sidebar.
Now that that version of the NavigationLink API has been deprecated, it was not clear how to keep track of the current selected item in multi-section sidebar lists. (Like Apple Music, Apple Mail, etc). Your example is not helpful because it doesn't show how you would create a binding to some data to keep track of the currently selected item in the sidebar.
The NavigationCookbook SwiftUI sample app is not helpful either because it doesn't show use of a multi-section sidebar. It uses the List(selection, content) API and uses a binding variable for selection to keep track of the selected item in the sidebar - but doesn't show how this could be accomplished with multiple unrelated lists in the Sidebar.
The problem with multi section sidebar items (especially when there are multiple sections of completely unrelated data (i.e.,Apple Music App sidebar) - is you can no longer use List(selection, content) as it seems Lists within Lists are not supported?
I am not sure if I am missing something simple, but it seems like the only way to keep track of the currently selected sidebar item with modern @Observable data and Swift Concurrency framework is to use the Destination view to update the state variable for keeping track of what is selected in the Sidebar. You have to use both .onAppear{} and .onChange(of:) to track these changes. I am not sure if this is by design - but there really is no other way to keep track of the selected Sidebar item. The downside of this solution is that it requires there to be a Destination view for Sidebar items - you can't seeminly have a Sidebar item that just changes state and does not launch a view.
I will post a full working skeleton below that I assume is the preferred way moving forward with the now deprecated NavigationLink APIs and the new @Observable macro with their modified getters and setters.
[quote='788783022, DTS Engineer, /thread/755911?answerId=788783022#788783022']
You could engage with the Syphon community to rework or extend the protocol into something that’s compatible with the App Sandbox.
[/quote]
I wish, but not gonna happen. It is a very established protocol / framework that all of the big name programs already use (Processing, Max/MSP, TouchDesigner, Resolume, BlackMagic, etc).
The first idea seems a little too obtuse for the users. If I have to self distribute, I might as well self distribute the actual "Pro" version of the app.
The key piece that I was missing ended up being that payloads are not allowed in NSDistributedNotificationCenter. Bummer.
Thanks a bunch for your time and help!
[quote='788593022, DTS Engineer, /thread/755911?answerId=788593022#788593022']
So who sends the notification? The server, when it start ‘advertising’? Or the clients when they start ‘browsing’?
[/quote]
Both. When a server instance is started, it sends a ServerAnnounce notification (this has a payload). When a client is started, it sends a ServerAnnounceRequest notification - which requests for all servers to send out ServerAnnounce notifications. Clients can then build a list of all servers available.
[quote='788593022, DTS Engineer, /thread/755911?answerId=788593022#788593022']
The App Sandbox allows distributed notifications but disallows any payload. See the discussion in Protecting user data with App Sandbox.
[/quote]
I believe this is the fundamental problem, as the CFMessagePort name is part of the ServerAnnounce payload - it is how clients discover what CFMessagePort to connect to for each server.
And matches what I see empirically - if I add an App Group and change the naming convention of the CFMessagePort to match the App Group - I no longer see the "Permission Denied" error, however, because there is no payload, the client has no way of discovering the CFMessagePort name that my app (a server) is using.
[quote='788593022, DTS Engineer, /thread/755911?answerId=788593022#788593022']
Oh, one last thing: Are you concerned about sandboxing because you want to ship on the Mac App Store? Or just because it’s the right thing to do?
[/quote]
My app is already shipping on the Mac App Store, and Syphon support is one of the most requested features among VJ enthusiasts and professionals. Keeping it on the Mac App Store is a high priority, but it looks like the Sandbox restriction of no payload for distributed notifications is a big hurdle. If the App Store didn't require a sandbox, I would not be using the Sandbox at all.
[quote='788466022, DTS Engineer, /thread/755911?answerId=788466022#788466022']
It seems like Syphon is a peer-to-peer thing, that is, there’s no centralised daemon or agent that the user has to install. Is that correct?
If so, how do Syphon peers discover other Syphon peers?
[/quote]
I am not too familiar with the internals of Syphon - but after looking over the source code (it's open source) - it looks like it is using NSDistributedNotificationCenter as a centralized messaging system for discovery and IPC setup. In Syphon lingo, a server is an app that publishes frames - a client is an app that can consume frames from any of the servers currently running.
I believe it goes something like this:
What I have found is that Syphon clients are sensitive to the CFMessagePort naming convention, i.e., if you mess with this naming convention at all, discovery stops working.
Hi Quinn, now that the weird build issue is resolved (though I'm still not sure how I got into that state) - do you have any suggestions / examples of how to use CFMessage ports in an Sandbox App other than the App Group naming convention (I can't change the port naming convention, as it is a long established naming convention of Syphon, which is supported by hundreds of video apps).
I have a sample (non sandboxed) app that simply publishes SpriteKit frames to a Syphon Server instance. Is there anyway possible to do this in a Sandbox App?
Sample Project
That confirms that the sandbox is enabled. I’m not sure what’s going with your project to build it this way, but the presence of that entitlement explains the runtime behaviour you’re seeing.
Yes, I saw that.
Anyway, I think it might be an Xcode bug. I looked through the build settings, and in the CodeSigning section of the Build Settings, the Enable App Sandbox setting was set to true, even though I had deleted that Entitlement in the Signing & Capabilities project tab. After setting the build setting of "Enable App Sandbox" to No - it is no longer building as a Sandbox App, and export of the Metal Texture is working perfectly.
Cheers
Oh, and just for clarity, here is info on my developer machine:
macBook Pro: M1 Max
Xcode: Version 15.3 (15E204a)
macOS: Sonoma 14.4.1
Here is the output of codesign -d --entitlements:
[Dict]
[Key] com.apple.application-identifier
[Value]
[String] R6FVYGV9JW.com.alloneword.macOS.Euler
[Key] com.apple.developer.icloud-container-identifiers
[Value]
[Array]
[String] iCloud.com.alloneword.eulervs
[Key] com.apple.developer.icloud-services
[Value]
[Array]
[String] CloudKit
[Key] com.apple.developer.team-identifier
[Value]
[String] R6FVYGV9JW
[Key] com.apple.security.app-sandbox
[Value]
[Bool] true
[Key] com.apple.security.files.user-selected.read-write
[Value]
[Bool] true
[Key] com.apple.security.get-task-allow
[Value]
[Bool] true