diff --git a/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj b/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj index be77e42c51..c0220aaae5 100644 --- a/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj +++ b/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -31,11 +31,10 @@ E283F0B1274103A30037F199 /* ControlRoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E283F0B0274103A30037F199 /* ControlRoomView.swift */; }; E283F0B3274103C80037F199 /* NumPadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E283F0B2274103C80037F199 /* NumPadView.swift */; }; E283F0B5274103E00037F199 /* TransportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E283F0B4274103E00037F199 /* TransportView.swift */; }; - E284EBB626AE018F0016AA0F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E284EBB526AE018F0016AA0F /* AppDelegate.swift */; }; + E284EBB626AE018F0016AA0F /* HUITestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E284EBB526AE018F0016AA0F /* HUITestApp.swift */; }; E284EBB826AE018F0016AA0F /* HUIClientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E284EBB726AE018F0016AA0F /* HUIClientView.swift */; }; E284EBBA26AE01900016AA0F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E284EBB926AE01900016AA0F /* Assets.xcassets */; }; E284EBBD26AE01900016AA0F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E284EBBC26AE01900016AA0F /* Preview Assets.xcassets */; }; - E284EBC026AE01900016AA0F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E284EBBE26AE01900016AA0F /* Main.storyboard */; }; E284EBCC26AE01EA0016AA0F /* Buttons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E284EBCB26AE01EA0016AA0F /* Buttons.swift */; }; E284EBD226AE01F00016AA0F /* HUISurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E284EBCF26AE01F00016AA0F /* HUISurfaceView.swift */; }; E284EBD326AE01F00016AA0F /* MixerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E284EBD026AE01F00016AA0F /* MixerView.swift */; }; @@ -71,11 +70,10 @@ E283F0B2274103C80037F199 /* NumPadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumPadView.swift; sourceTree = ""; }; E283F0B4274103E00037F199 /* TransportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportView.swift; sourceTree = ""; }; E284EBB226AE018F0016AA0F /* HUITest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HUITest.app; sourceTree = BUILT_PRODUCTS_DIR; }; - E284EBB526AE018F0016AA0F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + E284EBB526AE018F0016AA0F /* HUITestApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUITestApp.swift; sourceTree = ""; }; E284EBB726AE018F0016AA0F /* HUIClientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUIClientView.swift; sourceTree = ""; }; E284EBB926AE01900016AA0F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; E284EBBC26AE01900016AA0F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - E284EBBF26AE01900016AA0F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; E284EBC126AE01900016AA0F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E284EBC226AE01900016AA0F /* HUITest.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HUITest.entitlements; sourceTree = ""; }; E284EBCB26AE01EA0016AA0F /* Buttons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = ""; }; @@ -171,14 +169,13 @@ E284EBB426AE018F0016AA0F /* HUITest */ = { isa = PBXGroup; children = ( - E284EBB526AE018F0016AA0F /* AppDelegate.swift */, E246E3C428EBF59600BB786D /* HUIHostView */, E284EBCE26AE01F00016AA0F /* HUISurfaceView */, E284EBCA26AE01EA0016AA0F /* Control Views */, + E284EBB526AE018F0016AA0F /* HUITestApp.swift */, E284EBD726AE02090016AA0F /* HUISwitch Wrapper.swift */, E232239E2914F596005F0C12 /* Utilities.swift */, E21628E326E2DFF30022B66F /* Logger.swift */, - E284EBBE26AE01900016AA0F /* Main.storyboard */, E284EBC126AE01900016AA0F /* Info.plist */, E284EBB926AE01900016AA0F /* Assets.xcassets */, E284EBC226AE01900016AA0F /* HUITest.entitlements */, @@ -254,8 +251,9 @@ E284EBAA26AE018F0016AA0F /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1240; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E284EBB126AE018F0016AA0F = { CreatedOnToolsVersion = 12.4; @@ -289,7 +287,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - E284EBC026AE01900016AA0F /* Main.storyboard in Resources */, E284EBBD26AE01900016AA0F /* Preview Assets.xcassets in Resources */, E284EBBA26AE01900016AA0F /* Assets.xcassets in Resources */, ); @@ -321,7 +318,7 @@ E283F0A7274102A60037F199 /* FaderView.swift in Sources */, E246E3CA28EC67D900BB786D /* SwiftUI Extensions.swift in Sources */, E246E3C828EC679D00BB786D /* MomentaryPressView.swift in Sources */, - E284EBB626AE018F0016AA0F /* AppDelegate.swift in Sources */, + E284EBB626AE018F0016AA0F /* HUITestApp.swift in Sources */, E284EBD426AE01F00016AA0F /* TopView.swift in Sources */, E284EBD326AE01F00016AA0F /* MixerView.swift in Sources */, E283F0932740AF7E0037F199 /* RightSideView.swift in Sources */, @@ -338,22 +335,12 @@ }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXVariantGroup section */ - E284EBBE26AE01900016AA0F /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - E284EBBF26AE01900016AA0F /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin XCBuildConfiguration section */ E284EBC326AE01900016AA0F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -388,6 +375,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -402,7 +390,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -416,6 +404,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -450,6 +439,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -458,7 +448,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; @@ -564,7 +554,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/Advanced/HUITest/HUITest.xcodeproj/xcshareddata/xcschemes/HUITest.xcscheme b/Examples/Advanced/HUITest/HUITest.xcodeproj/xcshareddata/xcschemes/HUITest.xcscheme index 84ca90898c..c3a35446e9 100644 --- a/Examples/Advanced/HUITest/HUITest.xcodeproj/xcshareddata/xcschemes/HUITest.xcscheme +++ b/Examples/Advanced/HUITest/HUITest.xcodeproj/xcshareddata/xcschemes/HUITest.xcscheme @@ -1,6 +1,6 @@ + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default - - - - - - - Left to Right - - - - - - - Right to Left - - - - - - - - - - - Default - - - - - - - Left to Right - - - - - - - Right to Left - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostHelper.swift b/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostHelper.swift index aa8344cc99..5631bdb7e9 100644 --- a/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostHelper.swift +++ b/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostHelper.swift @@ -12,7 +12,7 @@ import SwiftUI class HUIHostHelper: ObservableObject { // MARK: MIDI - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager static let kHUIInputConnectionTag = "HUIHostInputConnection" static let kHUIOutputConnectionTag = "HUIHostOutputConnection" @@ -24,7 +24,7 @@ class HUIHostHelper: ObservableObject { @Published var model: HUIHostModel = .init() - init(midiManager: MIDIManager) { + init(midiManager: ObservableMIDIManager) { huiHost = HUIHost() setupSingleBank(midiManager: midiManager) @@ -49,7 +49,7 @@ class HUIHostHelper: ObservableObject { } } - func setupSingleBank(midiManager: MIDIManager) { + func setupSingleBank(midiManager: ObservableMIDIManager) { guard huiHost.banks.isEmpty else { return } huiHost.addBank( diff --git a/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostView.swift b/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostView.swift index 50c23ea040..3b3e9e4786 100644 --- a/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostView.swift +++ b/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostView.swift @@ -10,13 +10,13 @@ import MIDIKitIO import SwiftUI struct HUIHostView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @StateObject var huiHostHelper: HUIHostHelper /// Convenience accessor for first HUI bank. private var huiBank0: HUIHostBank? { huiHostHelper.huiHost.banks.first } - init(midiManager: MIDIManager) { + init(midiManager: ObservableMIDIManager) { // set up HUI Host object _huiHostHelper = StateObject(wrappedValue: HUIHostHelper(midiManager: midiManager)) } diff --git a/Examples/Advanced/HUITest/HUITest/HUISurfaceView/HUIClientView.swift b/Examples/Advanced/HUITest/HUITest/HUISurfaceView/HUIClientView.swift index fe275b7d56..41c6764d6d 100644 --- a/Examples/Advanced/HUITest/HUITest/HUISurfaceView/HUIClientView.swift +++ b/Examples/Advanced/HUITest/HUITest/HUISurfaceView/HUIClientView.swift @@ -9,13 +9,13 @@ import MIDIKitIO import SwiftUI struct HUIClientView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @StateObject private var huiSurface: HUISurface static let kHUIInputName = "MIDIKit HUI Input" static let kHUIOutputName = "MIDIKit HUI Output" - init(midiManager: MIDIManager) { + init(midiManager: ObservableMIDIManager) { // set up HUI Surface object _huiSurface = { let huiSurface = HUISurface() @@ -77,7 +77,7 @@ struct HUIClientView: View { #if DEBUG struct HUIClientView_Previews: PreviewProvider { - static let midiManager = MIDIManager(clientName: "Preview", model: "", manufacturer: "") + static let midiManager = ObservableMIDIManager(clientName: "Preview", model: "", manufacturer: "") static var previews: some View { HUIClientView(midiManager: midiManager) } diff --git a/Examples/Advanced/HUITest/HUITest/HUITestApp.swift b/Examples/Advanced/HUITest/HUITest/HUITestApp.swift new file mode 100644 index 0000000000..a762b29f7a --- /dev/null +++ b/Examples/Advanced/HUITest/HUITest/HUITestApp.swift @@ -0,0 +1,73 @@ +// +// HUITestApp.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +import MIDIKitIO +import SwiftUI + +@main +struct HUITestApp: App { + @Environment(\.openWindow) private var openWindow + + @ObservedObject private var midiManager = ObservableMIDIManager( + clientName: "HUITest", + model: "HUITest", + manufacturer: "MyCompany" + ) + + let huiHostWidth: CGFloat = 300 + let huiHostHeight: CGFloat = 600 + + let huiSurfaceWidth: CGFloat = 1180 + let huiSurfaceHeight: CGFloat = 920 + + init() { + do { + try midiManager.start() + } catch { + Logger.debug("Error setting up MIDI.") + } + } + + var body: some Scene { + Window("HUI Host", id: WindowID.huiHost) { + HUIHostView(midiManager: midiManager) + .frame(width: huiHostWidth, height: huiHostHeight) + .environmentObject(midiManager) + } + .windowResizability(.contentSize) + .defaultPosition(UnitPoint(x: 0.25, y: 0.4)) + + Window("HUI Surface", id: WindowID.huiSurface) { + HUIClientView(midiManager: midiManager) + .frame(width: huiSurfaceWidth, height: huiSurfaceHeight) + .environmentObject(midiManager) + } + .windowResizability(.contentSize) + .defaultPosition(UnitPoint(x: 0.5, y: 0.4)) + + .onSceneBody { + onAppLaunch() + } + } + + private func onAppLaunch() { + openWindow(id: WindowID.huiHost) + openWindow(id: WindowID.huiSurface) + + orderAllWindowsFront() + } + + private func orderAllWindowsFront() { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) { + NSApp.windows.forEach { $0.makeKeyAndOrderFront(self) } + } + } +} + +enum WindowID { + static let huiHost = "huiHost" + static let huiSurface = "huiSurface" +} diff --git a/Examples/Advanced/HUITest/HUITest/Utilities.swift b/Examples/Advanced/HUITest/HUITest/Utilities.swift index 09733befa3..25f5810259 100644 --- a/Examples/Advanced/HUITest/HUITest/Utilities.swift +++ b/Examples/Advanced/HUITest/HUITest/Utilities.swift @@ -5,6 +5,7 @@ // import Foundation +import SwiftUI /// Formatter that limits character length. class MaxLengthFormatter: Formatter { @@ -59,3 +60,11 @@ class MaxLengthFormatter: Formatter { partialString.count <= maxCharLength } } + +extension Scene { + /// Scene modifier to run arbitrary code when the scene's body is evaluated. + public func onSceneBody(_ block: @escaping () -> Void) -> some Scene { + DispatchQueue.main.async { block() } + return self + } +} diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj index b28f700565..58985e77df 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj @@ -22,7 +22,6 @@ E26A25C326C5873B00FFCF40 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26A25BF26C5873B00FFCF40 /* Log.swift */; }; E26A25C426C5873B00FFCF40 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26A25C026C5873B00FFCF40 /* ContentView.swift */; }; E26A25C926C5875A00FFCF40 /* OTCore in Frameworks */ = {isa = PBXBuildFile; productRef = E26A25C826C5875A00FFCF40 /* OTCore */; }; - E26A25CD26C5876400FFCF40 /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = E26A25CC26C5876400FFCF40 /* MIDIKit */; }; E29AC90C285BEFB1009D1C2C /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = E29AC90B285BEFB1009D1C2C /* MIDIKit */; }; E2B099132880AE3800625201 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2B099122880AE3800625201 /* Images.xcassets */; }; E2E8BD89279F8DF4007A1AF0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E2E8BD88279F8DF3007A1AF0 /* Main.storyboard */; }; @@ -46,6 +45,7 @@ E26A25C026C5873B00FFCF40 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; E2B099122880AE3800625201 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; E2B38F2026C63918008770A6 /* MIDIEventLogger.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MIDIEventLogger.entitlements; sourceTree = ""; }; + E2CEA4972AFE44FE00BE92F7 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; E2E8BD88279F8DF3007A1AF0 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ @@ -55,7 +55,6 @@ buildActionMask = 2147483647; files = ( E29AC90C285BEFB1009D1C2C /* MIDIKit in Frameworks */, - E26A25CD26C5876400FFCF40 /* MIDIKit in Frameworks */, E26A25C926C5875A00FFCF40 /* OTCore in Frameworks */, E26604E029888DA500177FC9 /* SwiftRadix in Frameworks */, ); @@ -83,6 +82,7 @@ E26A25A326C586F600FFCF40 = { isa = PBXGroup; children = ( + E2CEA4972AFE44FE00BE92F7 /* README.md */, E26A25AE26C586F600FFCF40 /* MIDIEventLogger */, E26A25AD26C586F600FFCF40 /* Products */, ); @@ -99,11 +99,11 @@ E26A25AE26C586F600FFCF40 /* MIDIEventLogger */ = { isa = PBXGroup; children = ( + E26604F12988982900177FC9 /* Views */, E26A25BE26C5873B00FFCF40 /* AppDelegate.swift */, E24FD2A3298734DA00E076A7 /* MIDIHelper.swift */, E24FD2A72987362000E076A7 /* Constants.swift */, E26A25BF26C5873B00FFCF40 /* Log.swift */, - E26604F12988982900177FC9 /* Views */, E216886026C5E5A400BF7959 /* Info.plist */, E2E8BD88279F8DF3007A1AF0 /* Main.storyboard */, E2B38F2026C63918008770A6 /* MIDIEventLogger.entitlements */, @@ -130,7 +130,6 @@ name = MIDIEventLogger; packageProductDependencies = ( E26A25C826C5875A00FFCF40 /* OTCore */, - E26A25CC26C5876400FFCF40 /* MIDIKit */, E29AC90B285BEFB1009D1C2C /* MIDIKit */, E26604DF29888DA500177FC9 /* SwiftRadix */, ); @@ -146,7 +145,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1300; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1500; TargetAttributes = { E26A25AB26C586F600FFCF40 = { CreatedOnToolsVersion = 13.0; @@ -217,6 +216,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -251,6 +251,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -279,6 +280,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -313,6 +315,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -428,7 +431,7 @@ repositoryURL = "https://github.com/orchetect/SwiftRadix.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.0; + minimumVersion = 1.3.1; }; }; E26A25C726C5875A00FFCF40 /* XCRemoteSwiftPackageReference "OTCore" */ = { @@ -436,7 +439,7 @@ repositoryURL = "https://github.com/orchetect/OTCore"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.4.8; + minimumVersion = 1.4.13; }; }; E29AC90A285BEFB1009D1C2C /* XCRemoteSwiftPackageReference "MIDIKit" */ = { @@ -444,7 +447,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -460,10 +463,6 @@ package = E26A25C726C5875A00FFCF40 /* XCRemoteSwiftPackageReference "OTCore" */; productName = OTCore; }; - E26A25CC26C5876400FFCF40 /* MIDIKit */ = { - isa = XCSwiftPackageProductDependency; - productName = MIDIKit; - }; E29AC90B285BEFB1009D1C2C /* MIDIKit */ = { isa = XCSwiftPackageProductDependency; package = E29AC90A285BEFB1009D1C2C /* XCRemoteSwiftPackageReference "MIDIKit" */; diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/xcshareddata/xcschemes/MIDIEventLogger.xcscheme b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/xcshareddata/xcschemes/MIDIEventLogger.xcscheme index b70d29cb19..79ad37ea52 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/xcshareddata/xcschemes/MIDIEventLogger.xcscheme +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/xcshareddata/xcschemes/MIDIEventLogger.xcscheme @@ -1,6 +1,6 @@ + + + + Bool { + true + } +} + +extension AppDelegate { func createAndShowWindow() { // Create the SwiftUI view that provides the window contents. let contentView = ContentView() @@ -38,10 +44,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { styleMask: [.titled, .miniaturizable, .resizable], backing: .buffered, defer: false ) - window.isReleasedWhenClosed = true - window.center() - window.setFrameAutosaveName("Main Window") - window.contentView = NSHostingView(rootView: contentView) - window.makeKeyAndOrderFront(nil) + window?.isReleasedWhenClosed = true + window?.center() + window?.setFrameAutosaveName("Main Window") + window?.contentView = NSHostingView(rootView: contentView) + window?.makeKeyAndOrderFront(nil) } } diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift index c0adc9a9f2..f1f556e268 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift @@ -4,16 +4,16 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import OTCore import SwiftUI final class MIDIHelper: ObservableObject { - private weak var midiManager: MIDIManager? + private weak var midiManager: ObservableMIDIManager? public init() { } - public func setup(midiManager: MIDIManager) { + public func setup(midiManager: ObservableMIDIManager) { self.midiManager = midiManager midiManager.notificationHandler = { notification, manager in @@ -70,44 +70,6 @@ final class MIDIHelper: ObservableObject { } } - public func updateInputConnection(selectedUniqueID: MIDIIdentifier?) { - guard let midiInputConnection else { return } - - guard let selectedUniqueID else { - midiInputConnection.removeAllOutputs() - return - } - - switch selectedUniqueID { - case .invalidMIDIIdentifier: - midiInputConnection.removeAllOutputs() - default: - if !midiInputConnection.outputsCriteria.contains(.uniqueID(selectedUniqueID)) { - midiInputConnection.removeAllOutputs() - midiInputConnection.add(outputs: [.uniqueID(selectedUniqueID)]) - } - } - } - - public func updateOutputConnection(selectedUniqueID: MIDIIdentifier?) { - guard let midiOutputConnection else { return } - - guard let selectedUniqueID else { - midiOutputConnection.removeAllInputs() - return - } - - switch selectedUniqueID { - case .invalidMIDIIdentifier: - midiOutputConnection.removeAllInputs() - default: - if !midiOutputConnection.inputsCriteria.contains(.uniqueID(selectedUniqueID)) { - midiOutputConnection.removeAllInputs() - midiOutputConnection.add(inputs: [.uniqueID(selectedUniqueID)]) - } - } - } - // MARK: - Virtual Endpoints public var midiInput: MIDIInput? { diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ContentView.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ContentView.swift index 5d244eb716..6c16071442 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ContentView.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ContentView.swift @@ -4,12 +4,12 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import OTCore import SwiftUI struct ContentView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @EnvironmentObject var midiHelper: MIDIHelper // MARK: - Constants @@ -25,7 +25,8 @@ struct ContentView: View { @State var midiGroup: UInt4 = 0 /// Currently selected MIDI output endpoint to connect to - @State var midiInputConnectionEndpoint: MIDIOutputEndpoint? = nil + @State var midiInputConnectionID: MIDIIdentifier? = nil + @State var midiInputConnectionDisplayName: String? = nil // MARK: - Body @@ -45,7 +46,8 @@ struct ContentView: View { ReceiveMIDIEventsView( inputName: ConnectionTags.inputName, - midiInputConnectionEndpoint: $midiInputConnectionEndpoint + midiInputConnectionID: $midiInputConnectionID, + midiInputConnectionDisplayName: $midiInputConnectionDisplayName ) Spacer().frame(height: 18) @@ -63,24 +65,15 @@ struct ContentView: View { .onAppear { setInputConnectionToVirtual() } - .onChange(of: midiInputConnectionEndpoint) { _ in - updateInputConnection() - } } // MARK: - Helper Methods /// Auto-select the virtual endpoint as our input connection source. func setInputConnectionToVirtual() { - midiInputConnectionEndpoint = midiHelper.midiOutput?.endpoint - } - - /// Update the MIDI manager's input connection to connect to the selected output endpoint. - func updateInputConnection() { - logger.debug( - "Updating input connection to endpoint: \(midiInputConnectionEndpoint?.displayName.quoted ?? "None")" - ) - midiHelper.updateInputConnection(selectedUniqueID: midiInputConnectionEndpoint?.uniqueID) + guard let midiOutputEndpoint = midiHelper.midiOutput?.endpoint else { return } + midiInputConnectionID = midiOutputEndpoint.uniqueID + midiInputConnectionDisplayName = midiOutputEndpoint.displayName } /// Send a MIDI event using our virtual output endpoint. @@ -91,11 +84,19 @@ struct ContentView: View { } } -struct ContentView_Previews: PreviewProvider { - private static let midiManager = MIDIManager(clientName: "Preview", model: "", manufacturer: "") +struct ContentViewPreviews: PreviewProvider { + private static let midiManager = ObservableMIDIManager( + clientName: "Preview", + model: "TestApp", + manufacturer: "MyCompany" + ) + + private static let midiHelper = MIDIHelper() static var previews: some View { ContentView() .environmentObject(midiManager) + .environmentObject(midiHelper) + .onAppear { midiHelper.setup(midiManager: midiManager) } } } diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/MIDISubsystemStatusView.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/MIDISubsystemStatusView.swift index 1753c9a5a1..f6669c1f45 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/MIDISubsystemStatusView.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/MIDISubsystemStatusView.swift @@ -4,14 +4,14 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import OTCore import SwiftRadix import SwiftUI extension ContentView { struct MIDISubsystemStatusView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager var body: some View { GroupBox(label: Text("MIDI Subsystem")) { diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ReceiveMIDIEventsView.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ReceiveMIDIEventsView.swift index d03983b531..96e7752fce 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ReceiveMIDIEventsView.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ReceiveMIDIEventsView.swift @@ -4,17 +4,19 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO +import MIDIKitUI import OTCore import SwiftRadix import SwiftUI extension ContentView { struct ReceiveMIDIEventsView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager var inputName: String - @Binding var midiInputConnectionEndpoint: MIDIOutputEndpoint? + @Binding var midiInputConnectionID: MIDIIdentifier? + @Binding var midiInputConnectionDisplayName: String? var body: some View { ZStack(alignment: .center) { @@ -32,17 +34,14 @@ extension ContentView { } GroupBox(label: Text("Source: Connection")) { - Picker("", selection: $midiInputConnectionEndpoint) { - Text("None") - .tag(MIDIOutputEndpoint?.none) - - VStack { Divider().padding(.leading) } - - ForEach(midiManager.endpoints.outputs) { - Text("🎹 " + ($0.displayName)) - .tag(MIDIOutputEndpoint?.some($0)) - } - } + MIDIOutputsPicker( + title: "", + selectionID: $midiInputConnectionID, + selectionDisplayName: $midiInputConnectionDisplayName, + showIcons: true, + hideOwned: false + ) + .updatingInputConnection(withTag: ConnectionTags.inputConnectionTag) .padding() .frame(maxWidth: 400) .frame( diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsChannelVoiceView.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsChannelVoiceView.swift index d45fd340e0..9abc7aa576 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsChannelVoiceView.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsChannelVoiceView.swift @@ -4,7 +4,7 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import OTCore import SwiftRadix import SwiftUI diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsMIDI2ChannelVoiceView.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsMIDI2ChannelVoiceView.swift index 3789be2ec5..598790578d 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsMIDI2ChannelVoiceView.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsMIDI2ChannelVoiceView.swift @@ -4,7 +4,7 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import OTCore import SwiftRadix import SwiftUI diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsSystemCommonView.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsSystemCommonView.swift index e459d4b9c0..14d14618f9 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsSystemCommonView.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsSystemCommonView.swift @@ -4,7 +4,7 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import OTCore import SwiftRadix import SwiftUI diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsSystemExclusiveView.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsSystemExclusiveView.swift index 0a352930be..8ce27fc68e 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsSystemExclusiveView.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsSystemExclusiveView.swift @@ -4,7 +4,7 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import OTCore import SwiftRadix import SwiftUI diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsSystemRealTimeView.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsSystemRealTimeView.swift index 4628c4fb5f..1edf1dda32 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsSystemRealTimeView.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsSystemRealTimeView.swift @@ -4,7 +4,7 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import OTCore import SwiftRadix import SwiftUI diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsView.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsView.swift index 9860958410..2c8fb713bc 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsView.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/SendMIDIEventsView.swift @@ -4,14 +4,14 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import OTCore import SwiftRadix import SwiftUI extension ContentView { struct SendMIDIEventsView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @Binding var midiGroup: UInt4 var sendEvent: (MIDIEvent) -> Void diff --git a/Examples/Advanced/MIDIEventLogger/README.md b/Examples/Advanced/MIDIEventLogger/README.md index bec29b6aa6..58262241d8 100644 --- a/Examples/Advanced/MIDIEventLogger/README.md +++ b/Examples/Advanced/MIDIEventLogger/README.md @@ -5,4 +5,4 @@ This is a debugging workhorse that was used while developing MIDIKit. - Buttons to send a test MIDI event for every possible MIDI event type - Logs received events to console -Note: The SwiftUI interface is very laggy and needs to be completely rebuilt. +Note: The SwiftUI interface may be very laggy. diff --git a/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj b/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj index ca4fd4ef9a..03f0c20ab5 100644 --- a/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj +++ b/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ E275EAB32679A493008E396D /* MTCGenContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MTCGenContentView.swift; sourceTree = ""; }; E27E3F9F25A6E5C700F4B78E /* Audio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Audio.swift; sourceTree = ""; }; E2BE847E268BC6680034F62C /* MIDIKitSync Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MIDIKitSync Extensions.swift"; sourceTree = ""; }; + E2CEA4982AFE49E700BE92F7 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; E2F71ED32758ED1E006254C0 /* Log.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; E2FAC4DA257884E000A6DD31 /* MTCExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MTCExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; E2FAC4DD257884E000A6DD31 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -59,6 +60,7 @@ E2FAC4D1257884DF00A6DD31 = { isa = PBXGroup; children = ( + E2CEA4982AFE49E700BE92F7 /* README.md */, E2FAC4DC257884E000A6DD31 /* MTCExample */, E2FAC4DB257884E000A6DD31 /* Products */, ); @@ -126,7 +128,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1230; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1500; TargetAttributes = { E2FAC4D9257884DF00A6DD31 = { CreatedOnToolsVersion = 12.3; @@ -202,6 +204,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -236,6 +239,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -264,6 +268,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -299,6 +304,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = fast; @@ -402,7 +408,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; E29EF02D267BF47F00282F94 /* XCRemoteSwiftPackageReference "SwiftRadix" */ = { @@ -410,7 +416,7 @@ repositoryURL = "https://github.com/orchetect/SwiftRadix.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.0; + minimumVersion = 1.3.1; }; }; E29EF037267BF71B00282F94 /* XCRemoteSwiftPackageReference "DunneAudioKit" */ = { @@ -418,7 +424,7 @@ repositoryURL = "https://github.com/AudioKit/DunneAudioKit.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 5.4.0; + minimumVersion = 5.6.1; }; }; E2E635B725EE4CE10039B8CF /* XCRemoteSwiftPackageReference "OTCore" */ = { @@ -426,7 +432,7 @@ repositoryURL = "https://github.com/orchetect/OTCore.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.4.8; + minimumVersion = 1.4.13; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/Advanced/MTCExample/MTCExample.xcodeproj/xcshareddata/xcschemes/MTCExample.xcscheme b/Examples/Advanced/MTCExample/MTCExample.xcodeproj/xcshareddata/xcschemes/MTCExample.xcscheme index 86980764a3..bf63b8d751 100644 --- a/Examples/Advanced/MTCExample/MTCExample.xcodeproj/xcshareddata/xcschemes/MTCExample.xcscheme +++ b/Examples/Advanced/MTCExample/MTCExample.xcodeproj/xcshareddata/xcschemes/MTCExample.xcscheme @@ -1,6 +1,6 @@ + + + + >> LOCAL SYNC: PLAYBACK START @", timecode) - scheduledLock?.cancel() - scheduledLock = nil - } - - scheduledLock = scheduled - - case .sync: - break - - case .freewheeling: - break - - case .incompatibleFrameRate: - break - } - } - - // create MTC reader MIDI endpoint - do { - let udKey = "\(kMIDIPorts.MTCRec.tag) - Unique ID" - - try midiManager.addInput( - name: kMIDIPorts.MTCRec.name, - tag: kMIDIPorts.MTCRec.tag, - uniqueID: .userDefaultsManaged(key: udKey), - receiver: .object(mtcRec, held: .weakly) - ) - } catch { - logger.error(error) - } - - updateSelfGenListen(state: receiveFromSelfGen) + setup() } .onChange(of: localFrameRate) { _ in @@ -129,41 +56,220 @@ struct MTCRecContentView: View { } } - private var mtcRecView: some View { - VStack(alignment: .center, spacing: 0) { - Toggle(isOn: $receiveFromSelfGen) { - Text("Receive from MTC Generator Window") + private func setup() { + // set up new MTC receiver and configure it + mtcRec = MTCReceiver( + name: "main", + initialLocalFrameRate: .fps24, + syncPolicy: .init( + lockFrames: 16, + dropOutFrames: 10 + ) + ) { timecode, _, _, displayNeedsUpdate in + receiverTC = timecode.stringValue() + receiverFR = mtcRec.mtcFrameRate + + guard displayNeedsUpdate else { return } + + if timecode.seconds != lastSeconds { + playClickB() + lastSeconds = timecode.seconds } - .padding(.top, 10) - Text(receiverTC) - .font(.system(size: 48, weight: .regular, design: .monospaced)) - .frame(maxWidth: .infinity, maxHeight: .infinity) + } stateChanged: { state in + receiverState = state + logger.default("MTC Receiver state:", receiverState) - Group { - if receiverState != .idle { - Text("MTC encoded rate: " + (receiverFR?.stringValue ?? "--") + " fps") - - } else { - Text(" ") + scheduledLock?.cancel() + scheduledLock = nil + + switch state { + case .idle: + break + + case let .preSync(lockTime, timecode): + let scheduled = DispatchQueue.main.schedule( + after: DispatchQueue.SchedulerTimeType(lockTime), + interval: .seconds(1), + tolerance: .zero, + options: .init( + qos: .userInitiated, + flags: [], + group: nil + ) + ) { + logger.default(">>> LOCAL SYNC: PLAYBACK START @", timecode) + scheduledLock?.cancel() + scheduledLock = nil } + + scheduledLock = scheduled + + case .sync: + break + + case .freewheeling: + break + + case .incompatibleFrameRate: + break } - .font(.system(size: 24, weight: .regular, design: .default)) + } + + // create MTC reader MIDI endpoint + do { + let udKey = "\(kMIDIPorts.MTCRec.tag) - Unique ID" + + try midiManager.addInput( + name: kMIDIPorts.MTCRec.name, + tag: kMIDIPorts.MTCRec.tag, + uniqueID: .userDefaultsManaged(key: udKey), + receiver: .object(mtcRec, held: .weakly) + ) + } catch { + logger.error(error) + } + + updateSelfGenListen(state: receiveFromSelfGen) + } + + private var mtcRecView: some View { + VStack(alignment: .center, spacing: 0) { + options + .padding(.top, 10) + + timecodeDisplay + + mtcEncodedRateInfo + + derivedFrameRatesInfo + scalingInfo + + receiverStateDisplay + + localFrameRatePicker + + frameRateInfo + .padding(.bottom, 10) + } + .background(receiverState.stateColor) + } + + private var options: some View { + Toggle(isOn: $receiveFromSelfGen) { + Text("Receive from MTC Generator Window") + } + } + + private var timecodeDisplay: some View { + Text(receiverTC) + .font(.system(size: 48, weight: .regular, design: .monospaced)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + private var mtcEncodedRateInfo: some View { + Group { + if receiverState != .idle { + Text("MTC encoded rate: " + (receiverFR?.stringValue ?? "--") + " fps") + } else { + Text(" ") + } + } + .font(.system(size: 24, weight: .regular, design: .default)) + } + + @ViewBuilder + private var derivedFrameRatesInfo: some View { + if receiverState != .idle, + let receiverFR + { + VStack { + Text("Derived frame rates of \(receiverFR.stringValue):") + + HStack { + ForEach(receiverFR.derivedFrameRates, id: \.self) { + Text($0.stringValue) + .foregroundColor($0 == localFrameRate ? .blue : nil) + .padding(2) + .border( + Color.white, + width: $0 == receiverFR.directEquivalentFrameRate ? 2 : 0 + ) + } + } + } + } else { + VStack { + Text(" ") + Text(" ") + .padding(2) + } + } + } + + private var scalingInfo: some View { + Group { if receiverState != .idle, - let receiverFR + localFrameRate != nil { + if receiverState == .incompatibleFrameRate { + Text("Can't scale frame rate because rates are incompatible.") + } else if receiverFR?.directEquivalentFrameRate == localFrameRate { + Text("Scaling not needed, rates are identical.") + } else { + Text( + "Scaled to local rate: " + (localFrameRate?.stringValue ?? "--") + + " fps" + ) + } + + } else { + Text(" ") + } + } + .font(.system(size: 24, weight: .regular, design: .default)) + } + + private var receiverStateDisplay: some View { + Text(receiverState.description) + .font(.system(size: 48, weight: .regular, design: .monospaced)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + private var localFrameRatePicker: some View { + Picker(selection: $localFrameRate, label: Text("Local Frame Rate")) { + Text("None") + .tag(TimecodeFrameRate?.none) + + Rectangle() + .frame(maxWidth: .infinity) + .frame(height: 3) + + ForEach(TimecodeFrameRate.allCases) { fRate in + Text(fRate.stringValue) + .tag(TimecodeFrameRate?.some(fRate)) + } + } + .frame(width: 250) + } + + private var frameRateInfo: some View { + HStack { + if let unwrappedLocalFrameRate = localFrameRate { VStack { - Text("Derived frame rates of \(receiverFR.stringValue):") + Text( + "Compatible remote frame rates (\(unwrappedLocalFrameRate.compatibleGroup.stringValue)):" + ) HStack { - ForEach(receiverFR.derivedFrameRates, id: \.self) { + ForEach(unwrappedLocalFrameRate.compatibleGroupRates, id: \.self) { Text($0.stringValue) - .foregroundColor($0 == localFrameRate ? .blue : nil) + .foregroundColor($0 == unwrappedLocalFrameRate ? .blue : nil) .padding(2) .border( Color.white, - width: $0 == receiverFR.directEquivalentFrameRate ? 2 : 0 + width: $0 == receiverFR?.directEquivalentFrameRate ? 2 : 0 ) } } @@ -175,79 +281,7 @@ struct MTCRecContentView: View { .padding(2) } } - - Group { - if receiverState != .idle, - localFrameRate != nil - { - if receiverState == .incompatibleFrameRate { - Text("Can't scale frame rate because rates are incompatible.") - } else if receiverFR?.directEquivalentFrameRate == localFrameRate { - Text("Scaling not needed, rates are identical.") - } else { - Text( - "Scaled to local rate: " + (localFrameRate?.stringValue ?? "--") + - " fps" - ) - } - - } else { - Text(" ") - } - } - .font(.system(size: 24, weight: .regular, design: .default)) - - Text(receiverState.description) - .font(.system(size: 48, weight: .regular, design: .monospaced)) - .frame(maxWidth: .infinity, maxHeight: .infinity) - - Picker(selection: $localFrameRate, label: Text("Local Frame Rate")) { - Text("None") - .tag(TimecodeFrameRate?.none) - - Rectangle() - .frame(maxWidth: .infinity) - .frame(height: 3) - - ForEach(TimecodeFrameRate.allCases) { fRate in - Text(fRate.stringValue) - .tag(TimecodeFrameRate?.some(fRate)) - } - } - .frame(width: 250) - - HStack { - if let unwrappedLocalFrameRate = localFrameRate { - VStack { - Text( - "Compatible remote frame rates (\(unwrappedLocalFrameRate.compatibleGroup.stringValue)):" - ) - - HStack { - ForEach(unwrappedLocalFrameRate.compatibleGroupRates, id: \.self) { - Text($0.stringValue) - .foregroundColor($0 == unwrappedLocalFrameRate ? .blue : nil) - .padding(2) - .border( - Color.white, - width: $0 == receiverFR?.directEquivalentFrameRate - ? 2 - : 0 - ) - } - } - } - } else { - VStack { - Text(" ") - Text(" ") - .padding(2) - } - } - } - .padding(.bottom, 10) } - .background(receiverState.stateColor) } private func updateSelfGenListen(state: Bool) { @@ -277,8 +311,12 @@ extension MTCReceiver.State { } } -struct MTCRecContentView_Previews: PreviewProvider { - private static let midiManager = MIDIManager(clientName: "Preview", model: "", manufacturer: "") +struct MTCRecContentViewPreviews: PreviewProvider { + private static let midiManager = ObservableMIDIManager( + clientName: "Preview", + model: "TestApp", + manufacturer: "MyCompany" + ) static var previews: some View { MTCRecContentView() diff --git a/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj b/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj index 208201d024..90fc0e6875 100644 --- a/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj +++ b/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj @@ -3,15 +3,15 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ E24BA23E284F1A3300AA6767 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24BA23D284F1A3300AA6767 /* AppDelegate.swift */; }; E24BA245284F1A3400AA6767 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E24BA243284F1A3400AA6767 /* Main.storyboard */; }; - E24BA251284F1C5B00AA6767 /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = E24BA250284F1C5B00AA6767 /* MIDIKit */; }; - E29AC924285BF271009D1C2C /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = E29AC923285BF271009D1C2C /* MIDIKit */; }; E29FF29F2880BC99005E2BC2 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E29FF29E2880BC99005E2BC2 /* Images.xcassets */; }; + E2CEA4922AFE2DD800BE92F7 /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = E2CEA4912AFE2DD800BE92F7 /* MIDIKit */; }; + E2CEA4942AFE2E6400BE92F7 /* MIDIEndpointsMenusHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CEA4932AFE2E6400BE92F7 /* MIDIEndpointsMenusHelper.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -21,6 +21,7 @@ E24BA246284F1A3400AA6767 /* EndpointMenus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EndpointMenus.entitlements; sourceTree = ""; }; E24CB06D285172CF00649B50 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; E29FF29E2880BC99005E2BC2 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + E2CEA4932AFE2E6400BE92F7 /* MIDIEndpointsMenusHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MIDIEndpointsMenusHelper.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -28,8 +29,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E29AC924285BF271009D1C2C /* MIDIKit in Frameworks */, - E24BA251284F1C5B00AA6767 /* MIDIKit in Frameworks */, + E2CEA4922AFE2DD800BE92F7 /* MIDIKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -57,6 +57,7 @@ isa = PBXGroup; children = ( E24BA23D284F1A3300AA6767 /* AppDelegate.swift */, + E2CEA4932AFE2E6400BE92F7 /* MIDIEndpointsMenusHelper.swift */, E24BA243284F1A3400AA6767 /* Main.storyboard */, E24BA246284F1A3400AA6767 /* EndpointMenus.entitlements */, E29FF29E2880BC99005E2BC2 /* Images.xcassets */, @@ -81,8 +82,7 @@ ); name = EndpointMenus; packageProductDependencies = ( - E24BA250284F1C5B00AA6767 /* MIDIKit */, - E29AC923285BF271009D1C2C /* MIDIKit */, + E2CEA4912AFE2DD800BE92F7 /* MIDIKit */, ); productName = EndpointMenus; productReference = E24BA23A284F1A3300AA6767 /* EndpointMenus.app */; @@ -96,7 +96,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E24BA239284F1A3300AA6767 = { CreatedOnToolsVersion = 14.0; @@ -113,7 +113,7 @@ ); mainGroup = E24BA231284F1A3300AA6767; packageReferences = ( - E29AC922285BF271009D1C2C /* XCRemoteSwiftPackageReference "MIDIKit" */, + E2CEA48D2AFE2DB700BE92F7 /* XCRemoteSwiftPackageReference "MIDIKit" */, ); productRefGroup = E24BA23B284F1A3300AA6767 /* Products */; projectDirPath = ""; @@ -141,6 +141,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E2CEA4942AFE2E6400BE92F7 /* MIDIEndpointsMenusHelper.swift in Sources */, E24BA23E284F1A3300AA6767 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -163,6 +164,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -196,6 +198,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -210,7 +213,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -224,6 +227,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -257,6 +261,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -265,7 +270,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; @@ -358,23 +363,20 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - E29AC922285BF271009D1C2C /* XCRemoteSwiftPackageReference "MIDIKit" */ = { + E2CEA48D2AFE2DB700BE92F7 /* XCRemoteSwiftPackageReference "MIDIKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - minimumVersion = 0.9.3; + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - E24BA250284F1C5B00AA6767 /* MIDIKit */ = { + E2CEA4912AFE2DD800BE92F7 /* MIDIKit */ = { isa = XCSwiftPackageProductDependency; - productName = MIDIKit; - }; - E29AC923285BF271009D1C2C /* MIDIKit */ = { - isa = XCSwiftPackageProductDependency; - package = E29AC922285BF271009D1C2C /* XCRemoteSwiftPackageReference "MIDIKit" */; + package = E2CEA48D2AFE2DB700BE92F7 /* XCRemoteSwiftPackageReference "MIDIKit" */; productName = MIDIKit; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/xcshareddata/xcschemes/EndpointMenus.xcscheme b/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/xcshareddata/xcschemes/EndpointMenus.xcscheme new file mode 100644 index 0000000000..c3959b80e7 --- /dev/null +++ b/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/xcshareddata/xcschemes/EndpointMenus.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/AppKit/EndpointMenus/EndpointMenus/AppDelegate.swift b/Examples/AppKit/EndpointMenus/EndpointMenus/AppDelegate.swift index c25bbf60bc..6a17022ba8 100644 --- a/Examples/AppKit/EndpointMenus/EndpointMenus/AppDelegate.swift +++ b/Examples/AppKit/EndpointMenus/EndpointMenus/AppDelegate.swift @@ -5,7 +5,7 @@ // import Cocoa -import MIDIKit +import MIDIKitIO @main class AppDelegate: NSObject, NSApplicationDelegate { @@ -18,319 +18,41 @@ class AppDelegate: NSObject, NSApplicationDelegate { manufacturer: "MyCompany" ) - public private(set) var midiOutMenuSelectedID: MIDIIdentifier = .invalidMIDIIdentifier - public private(set) var midiOutMenuSelectedDisplayName: String = "" - - public private(set) var midiInMenuSelectedID: MIDIIdentifier = .invalidMIDIIdentifier - public private(set) var midiInMenuSelectedDisplayName: String = "" + var midiEndpointsMenusHelper: MIDIEndpointsMenusHelper? func applicationDidFinishLaunching(_ aNotification: Notification) { - do { - print("Starting MIDI services.") - try midiManager.start() - - // set up MIDI subsystem notification handler - midiManager.notificationHandler = { [weak self] notification, manager in - self?.didReceiveMIDIIONotification(notification) - } - - // set up input connection - try midiManager.addInputConnection( - to: .none, - tag: ConnectionTags.midiIn, - receiver: .eventsLogging() - ) - - // set up output connection - try midiManager.addOutputConnection( - to: .none, - tag: ConnectionTags.midiOut - ) - } catch { - print("Error starting MIDI services:", error.localizedDescription) - } - + midiEndpointsMenusHelper = MIDIEndpointsMenusHelper( + midiManager: midiManager, + midiInMenu: midiInMenu, + midiOutMenu: midiOutMenu + ) + midiEndpointsMenusHelper?.setup() + // restore endpoint selection saved to persistent storage - midiRestorePersistentState() + midiEndpointsMenusHelper?.restorePersistentState() } func applicationWillTerminate(_ notification: Notification) { // save endpoint selection to persistent storage - midiSavePersistentState() - } -} - -// MARK: - String Constants - -extension AppDelegate { - private enum ConnectionTags { - static let midiIn = "SelectedInputConnection" - static let midiOut = "SelectedOutputConnection" + midiEndpointsMenusHelper?.savePersistentState() } - private enum UserDefaultsKeys { - static let midiInID = "SelectedMIDIInID" - static let midiInDisplayName = "SelectedMIDIInDisplayName" - - static let midiOutID = "SelectedMIDIOutID" - static let midiOutDisplayName = "SelectedMIDIOutDisplayName" + func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + true } } -// MARK: - Helpers +// MARK: - Menu Action First Responder Receivers extension AppDelegate { - /// Call this only once on app launch. - private func midiRestorePersistentState() { - // restore endpoint selection saved to UserDefaults - midiInMenuSetSelected( - id: .init( - exactly: UserDefaults.standard.integer(forKey: UserDefaultsKeys.midiInID) - ) ?? .invalidMIDIIdentifier, - displayName: UserDefaults.standard.string( - forKey: UserDefaultsKeys.midiInDisplayName - ) ?? "" - ) - midiOutMenuSetSelected( - id: .init( - exactly: UserDefaults.standard.integer(forKey: UserDefaultsKeys.midiOutID) - ) ?? .invalidMIDIIdentifier, - displayName: UserDefaults.standard.string( - forKey: UserDefaultsKeys.midiOutDisplayName - ) ?? "" - ) - } - - /// Call this only once on app quit. - private func midiSavePersistentState() { - // save endpoint selection to UserDefaults - - UserDefaults.standard.set( - midiInMenuSelectedID, - forKey: UserDefaultsKeys.midiInID - ) - UserDefaults.standard.set( - midiInMenuSelectedDisplayName, - forKey: UserDefaultsKeys.midiInDisplayName - ) - - UserDefaults.standard.set( - midiOutMenuSelectedID, - forKey: UserDefaultsKeys.midiOutID - ) - UserDefaults.standard.set( - midiOutMenuSelectedDisplayName, - forKey: UserDefaultsKeys.midiOutDisplayName - ) - } - - private func didReceiveMIDIIONotification(_ notification: MIDIIONotification) { - switch notification { - case .added, .removed, .propertyChanged: - midiOutMenuRefresh() - midiInMenuRefresh() - default: - break - } - } -} - -// MARK: - MIDI In Menu - -extension AppDelegate { - var midiInputConnection: MIDIInputConnection? { - midiManager.managedInputConnections[ConnectionTags.midiIn] - } - - /// Set the selected MIDI output manually. - public func midiInMenuSetSelected( - id: MIDIIdentifier, - displayName: String - ) { - midiInMenuSelectedID = id - midiInMenuSelectedDisplayName = displayName - midiInMenuRefresh() - midiInMenuUpdateConnection() - } - - private func midiInMenuRefresh() { - midiInMenu.items.removeAll() - - let sortedEndpoints = midiManager.endpoints.outputs.sortedByDisplayName() - - // None menu item - do { - let newMenuItem = NSMenuItem( - title: "None", - action: #selector(midiInMenuItemSelected), - keyEquivalent: "" - ) - newMenuItem.tag = Int(MIDIIdentifier.invalidMIDIIdentifier) - newMenuItem.state = midiInMenuSelectedID == .invalidMIDIIdentifier ? .on : .off - midiInMenu.addItem(newMenuItem) - } - - // --------------- - midiInMenu.addItem(.separator()) - - // If selected endpoint doesn't exist in the system, - // show it in the menu as missing but still selected. - // The MIDIManager will auto-reconnect to it if it reappears - // in the system in this condition. - if midiInMenuSelectedID != .invalidMIDIIdentifier, - !sortedEndpoints.contains(whereUniqueID: midiInMenuSelectedID) - { - let newMenuItem = NSMenuItem( - title: "⚠️ " + midiInMenuSelectedDisplayName, - action: #selector(midiInMenuItemSelected), - keyEquivalent: "" - ) - newMenuItem.tag = Int(midiInMenuSelectedID) - newMenuItem.state = .on - midiInMenu.addItem(newMenuItem) - } - - // Add endpoints to the menu - for endpoint in sortedEndpoints { - let newMenuItem = NSMenuItem( - title: endpoint.displayName, - action: #selector(midiInMenuItemSelected), - keyEquivalent: "" - ) - newMenuItem.tag = Int(endpoint.uniqueID) - if endpoint.uniqueID == midiInMenuSelectedID { - newMenuItem.state = .on - } - - midiInMenu.addItem(newMenuItem) - } - } - @objc - private func midiInMenuItemSelected(_ sender: NSMenuItem?) { - midiInMenuSelectedID = MIDIIdentifier( - exactly: sender?.tag ?? 0 - ) ?? .invalidMIDIIdentifier - - if let foundOutput = midiManager.endpoints.outputs.first(where: { - $0.uniqueID == midiInMenuSelectedID - }) { - midiInMenuSelectedDisplayName = foundOutput.displayName - } - - midiInMenuRefresh() - midiInMenuUpdateConnection() - } - - private func midiInMenuUpdateConnection() { - guard let midiInputConnection else { return } - - if midiInMenuSelectedID == .invalidMIDIIdentifier { - midiInputConnection.removeAllOutputs() - } else { - if midiInputConnection.outputsCriteria != [.uniqueID(midiInMenuSelectedID)] { - midiInputConnection.removeAllOutputs() - midiInputConnection.add(outputs: [.uniqueID(midiInMenuSelectedID)]) - } - } - } -} - -// MARK: - MIDI Out Menu - -extension AppDelegate { - var midiOutputConnection: MIDIOutputConnection? { - midiManager.managedOutputConnections[ConnectionTags.midiOut] - } - - public func midiOutMenuSetSelected( - id: MIDIIdentifier, - displayName: String - ) { - midiOutMenuSelectedID = id - midiOutMenuSelectedDisplayName = displayName - midiOutMenuRefresh() - midiOutMenuUpdateConnection() - } - - private func midiOutMenuRefresh() { - midiOutMenu.items.removeAll() - - let sortedEndpoints = midiManager.endpoints.inputs.sortedByDisplayName() - - // None menu item - do { - let newMenuItem = NSMenuItem( - title: "None", - action: #selector(midiOutMenuItemSelected), - keyEquivalent: "" - ) - newMenuItem.tag = Int(MIDIIdentifier.invalidMIDIIdentifier) - newMenuItem.state = midiOutMenuSelectedID == .invalidMIDIIdentifier ? .on : .off - midiOutMenu.addItem(newMenuItem) - } - - // --------------- - midiOutMenu.addItem(.separator()) - - // If selected endpoint doesn't exist in the system, - // show it in the menu as missing but still selected. - // The MIDIManager will auto-reconnect to it if it reappears - // in the system in this condition. - if midiOutMenuSelectedID != .invalidMIDIIdentifier, - !sortedEndpoints.contains(whereUniqueID: midiOutMenuSelectedID) - { - let newMenuItem = NSMenuItem( - title: "⚠️ " + midiOutMenuSelectedDisplayName, - action: #selector(midiOutMenuItemSelected), - keyEquivalent: "" - ) - newMenuItem.tag = Int(midiOutMenuSelectedID) - newMenuItem.state = .on - midiOutMenu.addItem(newMenuItem) - } - - // Add endpoints to the menu - for endpoint in sortedEndpoints { - let newMenuItem = NSMenuItem( - title: endpoint.displayName, - action: #selector(midiOutMenuItemSelected), - keyEquivalent: "" - ) - newMenuItem.tag = Int(endpoint.uniqueID) - if endpoint.uniqueID == midiOutMenuSelectedID { newMenuItem.state = .on } - - midiOutMenu.addItem(newMenuItem) - } + func midiInMenuItemSelected(_ sender: NSMenuItem?) { + midiEndpointsMenusHelper?.midiInMenuItemSelected(sender) } @objc - private func midiOutMenuItemSelected(_ sender: NSMenuItem?) { - midiOutMenuSelectedID = MIDIIdentifier( - exactly: sender?.tag ?? 0 - ) ?? .invalidMIDIIdentifier - - if let foundInput = midiManager.endpoints.inputs.first(where: { - $0.uniqueID == midiOutMenuSelectedID - }) { - midiOutMenuSelectedDisplayName = foundInput.displayName - } - - midiOutMenuRefresh() - midiOutMenuUpdateConnection() - } - - private func midiOutMenuUpdateConnection() { - guard let midiOutputConnection else { return } - - if midiOutMenuSelectedID == .invalidMIDIIdentifier { - midiOutputConnection.removeAllInputs() - } else { - if midiOutputConnection.inputsCriteria != [.uniqueID(midiOutMenuSelectedID)] { - midiOutputConnection.removeAllInputs() - midiOutputConnection.add(inputs: [.uniqueID(midiOutMenuSelectedID)]) - } - } + func midiOutMenuItemSelected(_ sender: NSMenuItem?) { + midiEndpointsMenusHelper?.midiOutMenuItemSelected(sender) } } @@ -339,7 +61,7 @@ extension AppDelegate { extension AppDelegate { @IBAction func sendNoteOn(_ sender: Any) { - try? midiOutputConnection?.send( + try? midiEndpointsMenusHelper?.midiOutputConnection?.send( event: .noteOn( 60, velocity: .midi1(127), @@ -350,7 +72,7 @@ extension AppDelegate { @IBAction func sendNoteOff(_ sender: Any) { - try? midiOutputConnection?.send( + try? midiEndpointsMenusHelper?.midiOutputConnection?.send( event: .noteOff( 60, velocity: .midi1(0), @@ -361,7 +83,7 @@ extension AppDelegate { @IBAction func sendCC1(_ sender: Any) { - try? midiOutputConnection?.send( + try? midiEndpointsMenusHelper?.midiOutputConnection?.send( event: .cc( 1, value: .midi1(64), diff --git a/Examples/AppKit/EndpointMenus/EndpointMenus/Base.lproj/Main.storyboard b/Examples/AppKit/EndpointMenus/EndpointMenus/Base.lproj/Main.storyboard index 09ea17e40c..26eaa50dbb 100644 --- a/Examples/AppKit/EndpointMenus/EndpointMenus/Base.lproj/Main.storyboard +++ b/Examples/AppKit/EndpointMenus/EndpointMenus/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -717,15 +717,15 @@ - - + + - + @@ -736,7 +736,7 @@ Refer to this example's README.md file for important information. - + @@ -747,7 +747,7 @@ Refer to this example's README.md file for important information. - + diff --git a/Examples/AppKit/EndpointMenus/EndpointMenus/EndpointMenus.entitlements b/Examples/AppKit/EndpointMenus/EndpointMenus/EndpointMenus.entitlements index f2ef3ae026..852fa1a472 100644 --- a/Examples/AppKit/EndpointMenus/EndpointMenus/EndpointMenus.entitlements +++ b/Examples/AppKit/EndpointMenus/EndpointMenus/EndpointMenus.entitlements @@ -2,9 +2,7 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + diff --git a/Examples/AppKit/EndpointMenus/EndpointMenus/MIDIEndpointsMenusHelper.swift b/Examples/AppKit/EndpointMenus/EndpointMenus/MIDIEndpointsMenusHelper.swift new file mode 100644 index 0000000000..6e72565992 --- /dev/null +++ b/Examples/AppKit/EndpointMenus/EndpointMenus/MIDIEndpointsMenusHelper.swift @@ -0,0 +1,381 @@ +// +// MIDIEndpointsMenusHelper.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +import AppKit +import Foundation +import MIDIKitIO + +final class MIDIEndpointsMenusHelper { + weak var midiManager: MIDIManager? + weak var midiInMenu: NSMenu? + weak var midiOutMenu: NSMenu? + + public private(set) var midiOutMenuSelectedID: MIDIIdentifier = .invalidMIDIIdentifier + public private(set) var midiOutMenuSelectedDisplayName: String = "" + + public private(set) var midiInMenuSelectedID: MIDIIdentifier = .invalidMIDIIdentifier + public private(set) var midiInMenuSelectedDisplayName: String = "" + + public init( + midiManager: MIDIManager, + midiInMenu: NSMenu, + midiOutMenu: NSMenu + ) { + self.midiManager = midiManager + self.midiInMenu = midiInMenu + self.midiOutMenu = midiOutMenu + } +} + +// MARK: - Setup + +extension MIDIEndpointsMenusHelper { + public func setup() { + if let midiManager = midiManager { self.midiManager = midiManager } + + guard let midiManager = midiManager else { return } + + do { + print("Starting MIDI services.") + try midiManager.start() + + // set up MIDI subsystem notification handler + midiManager.notificationHandler = { [weak self] notification, manager in + self?.didReceiveMIDIIONotification(notification) + } + + // set up input connection + try midiManager.addInputConnection( + to: .none, + tag: ConnectionTags.midiIn, + receiver: .eventsLogging() + ) + + // set up output connection + try midiManager.addOutputConnection( + to: .none, + tag: ConnectionTags.midiOut + ) + } catch { + print("Error starting MIDI services:", error.localizedDescription) + } + } +} + +// MARK: - String Constants + +extension MIDIEndpointsMenusHelper { + private enum ConnectionTags { + static let midiIn = "SelectedInputConnection" + static let midiOut = "SelectedOutputConnection" + } + + private enum UserDefaultsKeys { + static let midiInID = "SelectedMIDIInID" + static let midiInDisplayName = "SelectedMIDIInDisplayName" + + static let midiOutID = "SelectedMIDIOutID" + static let midiOutDisplayName = "SelectedMIDIOutDisplayName" + } +} + +// MARK: - Persistent State + +extension MIDIEndpointsMenusHelper { + /// Call this only once on app launch. + public func restorePersistentState() { + // restore endpoint selection saved to UserDefaults + midiInMenuSetSelected( + id: .init( + exactly: UserDefaults.standard.integer(forKey: UserDefaultsKeys.midiInID) + ) ?? .invalidMIDIIdentifier, + displayName: UserDefaults.standard.string( + forKey: UserDefaultsKeys.midiInDisplayName + ) ?? "" + ) + midiOutMenuSetSelected( + id: .init( + exactly: UserDefaults.standard.integer(forKey: UserDefaultsKeys.midiOutID) + ) ?? .invalidMIDIIdentifier, + displayName: UserDefaults.standard.string( + forKey: UserDefaultsKeys.midiOutDisplayName + ) ?? "" + ) + } + + /// Call this only once on app quit. + public func savePersistentState() { + // save endpoint selection to UserDefaults + + UserDefaults.standard.set( + midiInMenuSelectedID, + forKey: UserDefaultsKeys.midiInID + ) + UserDefaults.standard.set( + midiInMenuSelectedDisplayName, + forKey: UserDefaultsKeys.midiInDisplayName + ) + + UserDefaults.standard.set( + midiOutMenuSelectedID, + forKey: UserDefaultsKeys.midiOutID + ) + UserDefaults.standard.set( + midiOutMenuSelectedDisplayName, + forKey: UserDefaultsKeys.midiOutDisplayName + ) + } +} + +// MARK: - Helpers + +extension MIDIEndpointsMenusHelper { + private func didReceiveMIDIIONotification(_ notification: MIDIIONotification) { + switch notification { + case .added, .removed, .propertyChanged: + midiInUpdateMetadata() + midiInMenuRefresh() + + midiOutUpdateMetadata() + midiOutMenuRefresh() + default: + break + } + } +} + +// MARK: - MIDI In Menu + +extension MIDIEndpointsMenusHelper { + public var midiInputConnection: MIDIInputConnection? { + midiManager?.managedInputConnections[ConnectionTags.midiIn] + } + + /// Set the selected MIDI output manually. + public func midiInMenuSetSelected( + id: MIDIIdentifier, + displayName: String + ) { + midiInMenuSelectedID = id + midiInMenuSelectedDisplayName = displayName + midiInMenuRefresh() + midiInMenuUpdateConnection() + } + + private func midiInMenuRefresh() { + guard let midiManager = midiManager, + let midiInMenu = midiInMenu + else { return } + + midiInMenu.items.removeAll() + + let sortedEndpoints = midiManager.endpoints.outputs.sortedByDisplayName() + + // None menu item + do { + let newMenuItem = NSMenuItem( + title: "None", + action: #selector(midiInMenuItemSelected), + keyEquivalent: "" + ) + newMenuItem.tag = Int(MIDIIdentifier.invalidMIDIIdentifier) + newMenuItem.state = midiInMenuSelectedID == .invalidMIDIIdentifier ? .on : .off + midiInMenu.addItem(newMenuItem) + } + + // --------------- + midiInMenu.addItem(.separator()) + + // If selected endpoint doesn't exist in the system, + // show it in the menu as missing but still selected. + // The MIDIManager will auto-reconnect to it if it reappears + // in the system in this condition. + if midiInMenuSelectedID != .invalidMIDIIdentifier, + !sortedEndpoints.contains( + whereUniqueID: midiInMenuSelectedID, + fallbackDisplayName: midiInMenuSelectedDisplayName + ) + { + let newMenuItem = NSMenuItem( + title: "⚠️ " + midiInMenuSelectedDisplayName, + action: #selector(midiInMenuItemSelected), + keyEquivalent: "" + ) + newMenuItem.tag = Int(midiInMenuSelectedID) + newMenuItem.state = .on + midiInMenu.addItem(newMenuItem) + } + + // Add endpoints to the menu + for endpoint in sortedEndpoints { + let newMenuItem = NSMenuItem( + title: endpoint.displayName, + action: #selector(midiInMenuItemSelected), + keyEquivalent: "" + ) + newMenuItem.tag = Int(endpoint.uniqueID) + if endpoint.uniqueID == midiInMenuSelectedID { + newMenuItem.state = .on + } + + midiInMenu.addItem(newMenuItem) + } + } + + @objc + public func midiInMenuItemSelected(_ sender: NSMenuItem?) { + midiInMenuSelectedID = MIDIIdentifier( + exactly: sender?.tag ?? 0 + ) ?? .invalidMIDIIdentifier + + midiInUpdateMetadata() + + midiInMenuRefresh() + midiInMenuUpdateConnection() + } + + private func midiInUpdateMetadata() { + if let foundOutput = midiManager?.endpoints.outputs.first( + whereUniqueID: midiInMenuSelectedID, + fallbackDisplayName: midiInMenuSelectedDisplayName + ) { + midiInMenuSelectedID = foundOutput.uniqueID + midiInMenuSelectedDisplayName = foundOutput.displayName + } + } + + private func midiInMenuUpdateConnection() { + guard let midiInputConnection else { return } + + if midiInMenuSelectedID == .invalidMIDIIdentifier { + midiInputConnection.removeAllOutputs() + } else { + let criterium: MIDIEndpointIdentity = .uniqueIDWithFallback( + id: midiInMenuSelectedID, + fallbackDisplayName: midiInMenuSelectedDisplayName + ) + if midiInputConnection.outputsCriteria != [criterium] { + midiInputConnection.removeAllOutputs() + midiInputConnection.add(outputs: [criterium]) + } + } + } +} + +// MARK: - MIDI Out Menu + +extension MIDIEndpointsMenusHelper { + public var midiOutputConnection: MIDIOutputConnection? { + midiManager?.managedOutputConnections[ConnectionTags.midiOut] + } + + public func midiOutMenuSetSelected( + id: MIDIIdentifier, + displayName: String + ) { + midiOutMenuSelectedID = id + midiOutMenuSelectedDisplayName = displayName + midiOutMenuRefresh() + midiOutMenuUpdateConnection() + } + + private func midiOutMenuRefresh() { + guard let midiManager = midiManager, + let midiOutMenu = midiOutMenu + else { return } + + midiOutMenu.items.removeAll() + + let sortedEndpoints = midiManager.endpoints.inputs.sortedByDisplayName() + + // None menu item + do { + let newMenuItem = NSMenuItem( + title: "None", + action: #selector(midiOutMenuItemSelected), + keyEquivalent: "" + ) + newMenuItem.tag = Int(MIDIIdentifier.invalidMIDIIdentifier) + newMenuItem.state = midiOutMenuSelectedID == .invalidMIDIIdentifier ? .on : .off + midiOutMenu.addItem(newMenuItem) + } + + // --------------- + midiOutMenu.addItem(.separator()) + + // If selected endpoint doesn't exist in the system, + // show it in the menu as missing but still selected. + // The MIDIManager will auto-reconnect to it if it reappears + // in the system in this condition. + if midiOutMenuSelectedID != .invalidMIDIIdentifier, + !sortedEndpoints.contains( + whereUniqueID: midiOutMenuSelectedID, + fallbackDisplayName: midiOutMenuSelectedDisplayName + ) + { + let newMenuItem = NSMenuItem( + title: "⚠️ " + midiOutMenuSelectedDisplayName, + action: #selector(midiOutMenuItemSelected), + keyEquivalent: "" + ) + newMenuItem.tag = Int(midiOutMenuSelectedID) + newMenuItem.state = .on + midiOutMenu.addItem(newMenuItem) + } + + // Add endpoints to the menu + for endpoint in sortedEndpoints { + let newMenuItem = NSMenuItem( + title: endpoint.displayName, + action: #selector(midiOutMenuItemSelected), + keyEquivalent: "" + ) + newMenuItem.tag = Int(endpoint.uniqueID) + if endpoint.uniqueID == midiOutMenuSelectedID { newMenuItem.state = .on } + + midiOutMenu.addItem(newMenuItem) + } + } + + @objc + public func midiOutMenuItemSelected(_ sender: NSMenuItem?) { + midiOutMenuSelectedID = MIDIIdentifier( + exactly: sender?.tag ?? 0 + ) ?? .invalidMIDIIdentifier + + midiOutUpdateMetadata() + + midiOutMenuRefresh() + midiOutMenuUpdateConnection() + } + + private func midiOutUpdateMetadata() { + if let foundInput = midiManager?.endpoints.inputs.first( + whereUniqueID: midiOutMenuSelectedID, + fallbackDisplayName: midiOutMenuSelectedDisplayName + ) { + midiOutMenuSelectedID = foundInput.uniqueID + midiOutMenuSelectedDisplayName = foundInput.displayName + } + } + + private func midiOutMenuUpdateConnection() { + guard let midiOutputConnection else { return } + + if midiOutMenuSelectedID == .invalidMIDIIdentifier { + midiOutputConnection.removeAllInputs() + } else { + let criterium: MIDIEndpointIdentity = .uniqueIDWithFallback( + id: midiOutMenuSelectedID, + fallbackDisplayName: midiOutMenuSelectedDisplayName + ) + if midiOutputConnection.inputsCriteria != [criterium] { + midiOutputConnection.removeAllInputs() + midiOutputConnection.add(inputs: [criterium]) + } + } + } +} diff --git a/Examples/AppKit/EndpointMenus/README.md b/Examples/AppKit/EndpointMenus/README.md index 468b8ec820..928bf5e73a 100644 --- a/Examples/AppKit/EndpointMenus/README.md +++ b/Examples/AppKit/EndpointMenus/README.md @@ -5,22 +5,26 @@ This example demonstrates best practises when creating MIDI input and output sel ## Key Features - The menus are updated in real-time if endpoints change in the system. - > In AppKit, this is accomplished imperatively by refreshing the menus as a result of the MIDIManager receiving a Core MIDI notification that endpoints have changed in the system. -- The menus allow for a single endpoint to be selected, or None may be selected to disable the connection. - > This is a common use case. + > In AppKit, this is accomplished imperatively by refreshing the menus as a result of the `MIDIManager` receiving a Core MIDI notification that endpoints have changed in the system. + +- The menus allow for a single endpoint to be selected, or `None` may be selected to disable the connection. + + > This is one common use case. - The menu selections are stored in UserDefaults and restored on app relaunch so the user's selections are remembered. - > This is optional but included to demonstrate that using endpoint UniqueID numbers are the proper method to persistently reference endpoints (since endpoint names can change and multiple endpoints in the system may share the same name). UserDefaults is simply a convenient location to store the setting. - > - > In order to display a missing port's name to the user, we also persistently store the port's Display Name string since it's impossible to query Core MIDI for that if the port doesn't exist in the system. + > This is included to demonstrate that using endpoint Unique ID numbers are considered the primary method to persistently reference endpoints (since endpoint names can change and multiple endpoints in the system may share the same name). `UserDefaults` is a convenient location to store the setting. + > + > A secondary reason for persistently storing the endpoints's Display Name string is to allow us to display it to the user in the UI when the endpoint is missing in the system, since it's impossible to query Core MIDI for an endpoint property if the endpoint doesn't exist in the system. + - Maintaining a user's desired selection is reserved even if it disappears from the system. + > Often a user will select a desired MIDI endpoint and want that to always remain selected, but they may disconnect the device from time to time or it may not be present in the system at the point when your app is launching or your are restoring MIDI endpoint connections in your app. (Such as a USB keyboard by powering it off or physically disconnecting it from the system). > - > In this example, if a selected endpoint no longer exists in the system, it will still remain selected in the menu but will gain a caution symbol showing that it's missing simply to communicate to the user that it is currently missing. Since we set up the managed connections with that desired endpoint's unique ID, if the endpoint reappears in the system, the `MIDIManager` will automatically reconnect it and resume data flow. The menu item will once again appear as a normal selection without the caution symbol. The user does not need to do anything and this is all handled seamlessly and automatically. - -- Received events are logged the console, and test events can be sent to the MIDI Out selection using the buttons in the window. + > In this example, if a selected endpoint no longer exists in the system, it will still remain selected in the menu but will gain a caution symbol showing that it's missing simply to communicate to the user that it is currently missing. Since we set up the managed connections with that desired endpoint's unique ID, if the endpoint reappears in the system, the MIDI manager will automatically reconnect it and resume data flow. The menu item will once again appear as a normal selection without the caution symbol. The user does not need to do anything and this is all handled seamlessly and automatically. + +- Received events are logged to the console, and test events can be sent to the MIDI Out selection using the buttons provided in the window. ## Troubleshooting diff --git a/Examples/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj b/Examples/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj index 46a603cee9..bec29e55f4 100644 --- a/Examples/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj +++ b/Examples/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -99,7 +99,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E24BA239284F1A3300AA6767 = { CreatedOnToolsVersion = 14.0; @@ -167,6 +167,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -200,6 +201,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -214,7 +216,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -228,6 +230,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -261,6 +264,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -269,7 +273,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; @@ -367,7 +371,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; E2D7FF2029754A93003212AF /* XCRemoteSwiftPackageReference "SwiftRadix" */ = { diff --git a/Examples/AppKit/EventParsing/EventParsing.xcodeproj/xcshareddata/xcschemes/EventParsing.xcscheme b/Examples/AppKit/EventParsing/EventParsing.xcodeproj/xcshareddata/xcschemes/EventParsing.xcscheme new file mode 100644 index 0000000000..153353e8f4 --- /dev/null +++ b/Examples/AppKit/EventParsing/EventParsing.xcodeproj/xcshareddata/xcschemes/EventParsing.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/AppKit/EventParsing/EventParsing/AppDelegate.swift b/Examples/AppKit/EventParsing/EventParsing/AppDelegate.swift index f2bbcc168f..6692ff4c57 100644 --- a/Examples/AppKit/EventParsing/EventParsing/AppDelegate.swift +++ b/Examples/AppKit/EventParsing/EventParsing/AppDelegate.swift @@ -5,7 +5,7 @@ // import Cocoa -import MIDIKit +import MIDIKitIO import SwiftRadix @main @@ -25,7 +25,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } catch { print("Error starting MIDI services:", error.localizedDescription) } - + do { print("Creating virtual MIDI input.") try midiManager.addInput( @@ -33,7 +33,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { tag: virtualInputName, uniqueID: .userDefaultsManaged(key: virtualInputName), receiver: .events { [weak self] events in - events.forEach { self?.handleMIDI(event: $0) } + DispatchQueue.main.async { + events.forEach { self?.handleMIDI(event: $0) } + } } ) } catch { @@ -41,6 +43,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + true + } +} + +extension AppDelegate { private func handleMIDI(event: MIDIEvent) { switch event { case let .noteOn(payload): diff --git a/Examples/AppKit/EventParsing/EventParsing/EventParsing.entitlements b/Examples/AppKit/EventParsing/EventParsing/EventParsing.entitlements index f2ef3ae026..852fa1a472 100644 --- a/Examples/AppKit/EventParsing/EventParsing/EventParsing.entitlements +++ b/Examples/AppKit/EventParsing/EventParsing/EventParsing.entitlements @@ -2,9 +2,7 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + diff --git a/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj b/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj index 71cb088e1e..beebd5237c 100644 --- a/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj +++ b/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -96,7 +96,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E24BA239284F1A3300AA6767 = { CreatedOnToolsVersion = 14.0; @@ -163,6 +163,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -196,6 +197,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -210,7 +212,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -224,6 +226,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -257,6 +260,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -265,7 +269,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; @@ -363,7 +367,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/xcshareddata/xcschemes/VirtualInput.xcscheme b/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/xcshareddata/xcschemes/VirtualInput.xcscheme new file mode 100644 index 0000000000..e0fd8b1e59 --- /dev/null +++ b/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/xcshareddata/xcschemes/VirtualInput.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/AppKit/VirtualInput/VirtualInput/AppDelegate.swift b/Examples/AppKit/VirtualInput/VirtualInput/AppDelegate.swift index 03318a8fc0..f010d051b7 100644 --- a/Examples/AppKit/VirtualInput/VirtualInput/AppDelegate.swift +++ b/Examples/AppKit/VirtualInput/VirtualInput/AppDelegate.swift @@ -5,7 +5,7 @@ // import Cocoa -import MIDIKit +import MIDIKitIO @main class AppDelegate: NSObject, NSApplicationDelegate { diff --git a/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj b/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj index d8407fc833..6054dc4538 100644 --- a/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj +++ b/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -96,7 +96,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E24BA239284F1A3300AA6767 = { CreatedOnToolsVersion = 14.0; @@ -163,6 +163,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -192,9 +193,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -209,7 +212,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -223,6 +226,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -252,9 +256,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -263,7 +269,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; @@ -281,6 +287,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -310,6 +317,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -359,7 +367,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/xcshareddata/xcschemes/VirtualOutput.xcscheme b/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/xcshareddata/xcschemes/VirtualOutput.xcscheme new file mode 100644 index 0000000000..9918511e9a --- /dev/null +++ b/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/xcshareddata/xcschemes/VirtualOutput.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/AppKit/VirtualOutput/VirtualOutput/AppDelegate.swift b/Examples/AppKit/VirtualOutput/VirtualOutput/AppDelegate.swift index a96260c4b8..01a9bd6fbe 100644 --- a/Examples/AppKit/VirtualOutput/VirtualOutput/AppDelegate.swift +++ b/Examples/AppKit/VirtualOutput/VirtualOutput/AppDelegate.swift @@ -5,7 +5,7 @@ // import Cocoa -import MIDIKit +import MIDIKitIO @main class AppDelegate: NSObject, NSApplicationDelegate { @@ -24,10 +24,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { } catch { print("Error starting MIDI services:", error.localizedDescription) } - + setupVirtualOutput() } + func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + true + } +} + +extension AppDelegate { private func setupVirtualOutput() { do { print("Creating virtual output port.") diff --git a/Examples/AppKit/VirtualOutput/VirtualOutput/VirtualOutput.entitlements b/Examples/AppKit/VirtualOutput/VirtualOutput/VirtualOutput.entitlements index 18aff0ce43..852fa1a472 100644 --- a/Examples/AppKit/VirtualOutput/VirtualOutput/VirtualOutput.entitlements +++ b/Examples/AppKit/VirtualOutput/VirtualOutput/VirtualOutput.entitlements @@ -4,7 +4,5 @@ com.apple.security.app-sandbox - com.apple.security.files.user-selected.read-only - diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj index 2e2a551620..c6955b52c9 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj @@ -3,34 +3,28 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ E21299092851D8D600957FE8 /* MIDIHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21299082851D8D600957FE8 /* MIDIHelper.swift */; }; - E2496A912989087F003FD165 /* MIDIEndpointSelectionView-iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2496A8A2989087F003FD165 /* MIDIEndpointSelectionView-iOS.swift */; }; E2496A922989087F003FD165 /* ContentView-iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2496A8B2989087F003FD165 /* ContentView-iOS.swift */; }; E27C5DC42A034B3100189B15 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27C5DC32A034B3100189B15 /* Utilities.swift */; }; E27D0E63284F3FB600F43247 /* EndpointPickersApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27D0E62284F3FB600F43247 /* EndpointPickersApp.swift */; }; - E27D0E75284F409600F43247 /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = E27D0E74284F409600F43247 /* MIDIKit */; }; E2841ABB2989CB08006907BD /* ContentView-macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2496A902989087F003FD165 /* ContentView-macOS.swift */; }; - E2841ABC2989CB0B006907BD /* MIDIEndpointSelectionView-macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2496A8E2989087F003FD165 /* MIDIEndpointSelectionView-macOS.swift */; }; E29AC912285BF048009D1C2C /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = E29AC911285BF048009D1C2C /* MIDIKit */; }; E29FF28D2880BB54005E2BC2 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E29FF28C2880BB54005E2BC2 /* Images.xcassets */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ E21299082851D8D600957FE8 /* MIDIHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MIDIHelper.swift; sourceTree = ""; }; - E2496A8A2989087F003FD165 /* MIDIEndpointSelectionView-iOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MIDIEndpointSelectionView-iOS.swift"; sourceTree = ""; }; E2496A8B2989087F003FD165 /* ContentView-iOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ContentView-iOS.swift"; sourceTree = ""; }; E2496A8C2989087F003FD165 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - E2496A8E2989087F003FD165 /* MIDIEndpointSelectionView-macOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MIDIEndpointSelectionView-macOS.swift"; sourceTree = ""; }; E2496A8F2989087F003FD165 /* EndpointPickers.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = EndpointPickers.entitlements; sourceTree = ""; }; E2496A902989087F003FD165 /* ContentView-macOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ContentView-macOS.swift"; sourceTree = ""; }; E27C5DC32A034B3100189B15 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; E27D0E5F284F3FB600F43247 /* EndpointPickers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EndpointPickers.app; sourceTree = BUILT_PRODUCTS_DIR; }; E27D0E62284F3FB600F43247 /* EndpointPickersApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndpointPickersApp.swift; sourceTree = ""; }; - E2841ABA2989CAF5006907BD /* EndpointPickers.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EndpointPickers.entitlements; sourceTree = ""; }; E29FF28C2880BB54005E2BC2 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; E2FDAE542851AA8C00F98425 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; /* End PBXFileReference section */ @@ -41,7 +35,6 @@ buildActionMask = 2147483647; files = ( E29AC912285BF048009D1C2C /* MIDIKit in Frameworks */, - E27D0E75284F409600F43247 /* MIDIKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -52,7 +45,6 @@ isa = PBXGroup; children = ( E2496A8B2989087F003FD165 /* ContentView-iOS.swift */, - E2496A8A2989087F003FD165 /* MIDIEndpointSelectionView-iOS.swift */, E2496A8C2989087F003FD165 /* Info.plist */, ); path = iOS; @@ -62,7 +54,6 @@ isa = PBXGroup; children = ( E2496A902989087F003FD165 /* ContentView-macOS.swift */, - E2496A8E2989087F003FD165 /* MIDIEndpointSelectionView-macOS.swift */, E2496A8F2989087F003FD165 /* EndpointPickers.entitlements */, ); path = macOS; @@ -88,7 +79,6 @@ E27D0E61284F3FB600F43247 /* EndpointPickers */ = { isa = PBXGroup; children = ( - E2841ABA2989CAF5006907BD /* EndpointPickers.entitlements */, E2496A892989087F003FD165 /* iOS */, E2496A8D2989087F003FD165 /* macOS */, E27D0E62284F3FB600F43247 /* EndpointPickersApp.swift */, @@ -116,7 +106,6 @@ ); name = EndpointPickers; packageProductDependencies = ( - E27D0E74284F409600F43247 /* MIDIKit */, E29AC911285BF048009D1C2C /* MIDIKit */, ); productName = EndpointPickers; @@ -131,7 +120,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E27D0E5E284F3FB600F43247 = { CreatedOnToolsVersion = 14.0; @@ -180,8 +169,6 @@ E27C5DC42A034B3100189B15 /* Utilities.swift in Sources */, E21299092851D8D600957FE8 /* MIDIHelper.swift in Sources */, E2496A922989087F003FD165 /* ContentView-iOS.swift in Sources */, - E2496A912989087F003FD165 /* MIDIEndpointSelectionView-iOS.swift in Sources */, - E2841ABC2989CB0B006907BD /* MIDIEndpointSelectionView-macOS.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -192,6 +179,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -224,6 +212,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -238,8 +227,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - MACOSX_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -253,6 +242,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -285,6 +275,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -293,8 +284,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - MACOSX_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -309,7 +300,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = EndpointPickers/EndpointPickers.entitlements; + "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = EndpointPickers/macOS/EndpointPickers.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; @@ -317,14 +308,14 @@ DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = EndpointPickers/iOS/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = EndpointPickersSwiftUI; + "INFOPLIST_FILE[sdk=iphoneos*]" = EndpointPickers/iOS/Info.plist; + "INFOPLIST_FILE[sdk=iphonesimulator*]" = EndpointPickers/iOS/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Endpoint Pickers"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -347,7 +338,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = EndpointPickers/EndpointPickers.entitlements; + "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = EndpointPickers/macOS/EndpointPickers.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; @@ -355,14 +346,14 @@ DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = EndpointPickers/iOS/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = EndpointPickersSwiftUI; + "INFOPLIST_FILE[sdk=iphoneos*]" = EndpointPickers/iOS/Info.plist; + "INFOPLIST_FILE[sdk=iphonesimulator*]" = EndpointPickers/iOS/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Endpoint Pickers"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -409,16 +400,12 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - E27D0E74284F409600F43247 /* MIDIKit */ = { - isa = XCSwiftPackageProductDependency; - productName = MIDIKit; - }; E29AC911285BF048009D1C2C /* MIDIKit */ = { isa = XCSwiftPackageProductDependency; package = E29AC910285BF048009D1C2C /* XCRemoteSwiftPackageReference "MIDIKit" */; diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/xcshareddata/xcschemes/EndpointPickers.xcscheme b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/xcshareddata/xcschemes/EndpointPickers.xcscheme index 8d2e19e8d4..ea414f11a8 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/xcshareddata/xcschemes/EndpointPickers.xcscheme +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/xcshareddata/xcschemes/EndpointPickers.xcscheme @@ -1,6 +1,6 @@ + + + + Self { + UInt7(UInt.random(in: 0 ... 127)) + } + + public static func random(in range: ClosedRange) -> Self { + let lb = UInt(range.lowerBound) + let ub = UInt(range.upperBound) + return UInt7(UInt.random(in: lb ... ub)) + } +} diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift index 02727e7696..c538109c21 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift @@ -7,107 +7,153 @@ #if os(iOS) import MIDIKitIO +import MIDIKitUI import SwiftUI struct ContentView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @EnvironmentObject var midiHelper: MIDIHelper - @Binding var midiInSelectedID: MIDIIdentifier - @Binding var midiInSelectedDisplayName: String + @Binding var midiInSelectedID: MIDIIdentifier? + @Binding var midiInSelectedDisplayName: String? - @Binding var midiOutSelectedID: MIDIIdentifier - @Binding var midiOutSelectedDisplayName: String + @Binding var midiOutSelectedID: MIDIIdentifier? + @Binding var midiOutSelectedDisplayName: String? var body: some View { NavigationView { Form { - Section() { - NavigationLink("Info") { - InfoView() - } - } - - Section() { - MIDIEndpointSelectionView( - midiInSelectedID: $midiInSelectedID, - midiInSelectedDisplayName: $midiInSelectedDisplayName, - midiOutSelectedID: $midiOutSelectedID, - midiOutSelectedDisplayName: $midiOutSelectedDisplayName - ) + infoSection + + endpointSelectionSection + + virtualEndpointsSection + + eventLogSection + } + .navigationBarTitle("Endpoint Pickers") + + infoView + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .padding() + } - Group { - Button("Send Note On C3") { - sendToConnection(.noteOn(60, velocity: .midi1(127), channel: 0)) - } + private var infoSection: some View { + Section() { + NavigationLink("Info") { + infoView + } + } + } - Button("Send Note Off C3") { - sendToConnection(.noteOff(60, velocity: .midi1(0), channel: 0)) - } + private var infoView: some View { + Text( + """ + This example demonstrates maintaining menus with MIDI endpoints in the system, allowing a single selection for each menu. + + Refer to this example's README.md file for important information. + + For testing purposes, try creating virtual endpoints, selecting them as MIDI In and MIDI Out, then destroying them. They appear as missing but their selection is retained. Then create them again, and they will appear normally once again and connection will resume. They are remembered even if you quit the app. + """ + ) + .multilineTextAlignment(.center) + .navigationTitle("Info") + .navigationBarTitleDisplayMode(.inline) + .frame(maxWidth: 600) + } - Button("Send CC1") { - sendToConnection(.cc(1, value: .midi1(64), channel: 0)) - } - } - .disabled( - midiOutSelectedID == .invalidMIDIIdentifier || - !midiManager.endpoints.inputs.contains(whereUniqueID: midiOutSelectedID) - ) + private var endpointSelectionSection: some View { + Section() { + MIDIOutputsPicker( + title: "MIDI In", + selectionID: $midiInSelectedID, + selectionDisplayName: $midiInSelectedDisplayName, + showIcons: true, + hideOwned: false + ) + + MIDIInputsPicker( + title: "MIDI Out", + selectionID: $midiOutSelectedID, + selectionDisplayName: $midiOutSelectedDisplayName, + showIcons: true, + hideOwned: false + ) + + Group { + Button("Send Note On C3") { + sendToConnection(.noteOn(60, velocity: .midi1(UInt7.random(in: 20 ... 127)), channel: 0)) } - - Section() { - Button("Create Test Virtual Endpoints") { - midiHelper.createVirtualEndpoints() - } - .disabled(midiHelper.virtualsExist) - - Button("Destroy Test Virtual Endpoints") { - midiHelper.destroyVirtualInputs() - } - .disabled(!midiHelper.virtualsExist) - - Group { - Button("Send Note On C3") { - sendToVirtuals(.noteOn(60, velocity: .midi1(127), channel: 0)) - } - - Button("Send Note Off C3") { - sendToVirtuals(.noteOff(60, velocity: .midi1(0), channel: 0)) - } - - Button("Send CC1") { - sendToVirtuals(.cc(1, value: .midi1(64), channel: 0)) - } - } - .disabled(!midiHelper.virtualsExist) + + Button("Send Note Off C3") { + sendToConnection(.noteOff(60, velocity: .midi1(0), channel: 0)) + } + + Button("Send CC1") { + sendToConnection(.cc(1, value: .midi1(UInt7.random()), channel: 0)) } + } + .disabled(isMIDIOutDisabled) + } + } - Section(header: Text("Received Events")) { - Toggle( - "Filter Active Sensing and Clock", - isOn: $midiHelper.filterActiveSensingAndClock - ) - - let events = midiHelper.receivedEvents.reversed() - - // Since MIDIEvent doesn't conform to Identifiable (and won't ever), in a List - // or ForEach we need to either use an array index or a wrap MIDIEvent in a - // custom type that does conform to Identifiable. It's really up to your use - // case. - // Usually application interaction is driven by MIDI events and we aren't - // literally logging events, but this is for diagnostic purposes here. - List(events.indices, id: \.self) { index in - Text(events[index].description) - .foregroundColor(color(for: events[index])) - } + private var virtualEndpointsSection: some View { + Section() { + Button("Create Test Virtual Endpoints") { + midiHelper.createVirtualEndpoints() + } + .disabled(midiHelper.virtualsExist) + + Button("Destroy Test Virtual Endpoints") { + midiHelper.destroyVirtualInputs() + } + .disabled(!midiHelper.virtualsExist) + + Group { + Button("Send Note On C3") { + sendToVirtuals(.noteOn(60, velocity: .midi1(UInt7.random(in: 20 ... 127)), channel: 0)) + } + + Button("Send Note Off C3") { + sendToVirtuals(.noteOff(60, velocity: .midi1(0), channel: 0)) + } + + Button("Send CC1") { + sendToVirtuals(.cc(1, value: .midi1(UInt7.random()), channel: 0)) } } - .navigationBarTitle("Endpoint Pickers") + .disabled(!midiHelper.virtualsExist) + } + } + + private var eventLogSection: some View { + Section(header: Text("Received Events")) { + Toggle( + "Filter Active Sensing and Clock", + isOn: $midiHelper.filterActiveSensingAndClock + ) + + let events = midiHelper.receivedEvents.reversed() - InfoView() + // Since MIDIEvent doesn't conform to Identifiable (and won't ever), in a List + // or ForEach we need to either use an array index or a wrap MIDIEvent in a + // custom type that does conform to Identifiable. It's really up to your use + // case. + // Usually application interaction is driven by MIDI events and we aren't + // literally logging events, but this is for diagnostic purposes here. + List(events.indices, id: \.self) { index in + Text(events[index].description) + .foregroundColor(color(for: events[index])) + } } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) - .padding() + } +} + +extension ContentView { + private var isMIDIOutDisabled: Bool { + midiOutSelectedID == .invalidMIDIIdentifier || + midiOutSelectedID == nil } func sendToConnection(_ event: MIDIEvent) { @@ -129,22 +175,4 @@ struct ContentView: View { } } -struct InfoView: View { - var body: some View { - Text( - """ - This example demonstrates maintaining menus with MIDI endpoints in the system, allowing a single selection for each menu. - - Refer to this example's README.md file for important information. - - For testing purposes, try creating virtual endpoints, selecting them as MIDI In and MIDI Out, then destroying them. They appear as missing but their selection is retained. Then create them again, and they will appear normally once again and connection will resume. They are remembered even if you quit the app. - """ - ) - .multilineTextAlignment(.center) - .navigationTitle("Info") - .navigationBarTitleDisplayMode(.inline) - .frame(maxWidth: 600) - } -} - #endif diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/MIDIEndpointSelectionView-iOS.swift b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/MIDIEndpointSelectionView-iOS.swift index a2b1cbae57..34cbd8a417 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/MIDIEndpointSelectionView-iOS.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/MIDIEndpointSelectionView-iOS.swift @@ -10,7 +10,7 @@ import MIDIKit import SwiftUI struct MIDIEndpointSelectionView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @EnvironmentObject var midiHelper: MIDIHelper @Binding var midiInSelectedID: MIDIIdentifier @@ -24,15 +24,13 @@ struct MIDIEndpointSelectionView: View { Text("None") .tag(MIDIIdentifier.invalidMIDIIdentifier) - if midiInSelectedID != .invalidMIDIIdentifier, - !midiManager.endpoints.outputs.contains(whereUniqueID: midiInSelectedID) - { + if isSelectedInputMissing { Text("⚠️ " + midiInSelectedDisplayName) .tag(midiInSelectedID) .foregroundColor(.secondary) } - ForEach(midiManager.endpoints.outputs) { + ForEach(midiManager.observableEndpoints.outputs) { Text($0.displayName) .tag($0.uniqueID) } @@ -42,20 +40,34 @@ struct MIDIEndpointSelectionView: View { Text("None") .tag(MIDIIdentifier.invalidMIDIIdentifier) - if midiOutSelectedID != .invalidMIDIIdentifier, - !midiManager.endpoints.inputs.contains(whereUniqueID: midiOutSelectedID) - { + if isSelectedOutputMissing { Text("⚠️ " + midiOutSelectedDisplayName) .tag(midiOutSelectedID) .foregroundColor(.secondary) } - ForEach(midiManager.endpoints.inputs) { + ForEach(midiManager.observableEndpoints.inputs) { Text($0.displayName) .tag($0.uniqueID) } } } + + private var isSelectedInputMissing: Bool { + midiInSelectedID != .invalidMIDIIdentifier && + !midiManager.observableEndpoints.outputs.contains( + whereUniqueID: midiInSelectedID, + fallbackDisplayName: midiInSelectedDisplayName + ) + } + + private var isSelectedOutputMissing: Bool { + midiOutSelectedID != .invalidMIDIIdentifier && + !midiManager.observableEndpoints.inputs.contains( + whereUniqueID: midiOutSelectedID, + fallbackDisplayName: midiOutSelectedDisplayName + ) + } } #endif diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift index 1ce95e1ff9..66d21e3c69 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift @@ -7,126 +7,156 @@ #if os(macOS) import MIDIKitIO +import MIDIKitUI import SwiftUI struct ContentView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @EnvironmentObject var midiHelper: MIDIHelper - @Binding var midiInSelectedID: MIDIIdentifier - @Binding var midiInSelectedDisplayName: String + @Binding var midiInSelectedID: MIDIIdentifier? + @Binding var midiInSelectedDisplayName: String? - @Binding var midiOutSelectedID: MIDIIdentifier - @Binding var midiOutSelectedDisplayName: String + @Binding var midiOutSelectedID: MIDIIdentifier? + @Binding var midiOutSelectedDisplayName: String? var body: some View { VStack { - Text( - """ - This example demonstrates maintaining menus with MIDI endpoints in the system, allowing a single selection for each menu. - - Refer to this example's README.md file for important information. - """ - ) - .font(.system(size: 14)) - .padding(5) - - GroupBox(label: Text("MIDI In Connection")) { - MIDIInSelectionView( - midiInSelectedID: $midiInSelectedID, - midiInSelectedDisplayName: $midiInSelectedDisplayName - ) - .padding([.leading, .trailing], 60) - - Toggle( - "Filter Active Sensing and Clock", - isOn: $midiHelper.filterActiveSensingAndClock - ) - } - .padding(5) - - GroupBox(label: Text("MIDI Out Connection")) { - MIDIOutSelectionView( - midiOutSelectedID: $midiOutSelectedID, - midiOutSelectedDisplayName: $midiOutSelectedDisplayName - ) - .padding([.leading, .trailing], 60) + infoView + .padding(5) + + midiInConnectionView + .padding(5) + + midiOutConnectionView + .padding(5) + + virtualEndpointsView + .padding(5) + + eventLogView + } + .multilineTextAlignment(.center) + .lineLimit(nil) + .padding() + .frame(minWidth: 700, minHeight: 660) + } - HStack { - Button("Send Note On C3") { - sendToConnection(.noteOn(60, velocity: .midi1(127), channel: 0)) - } + private var infoView: some View { + Text( + """ + This example demonstrates maintaining menus with MIDI endpoints in the system, allowing a single selection for each menu. + + Refer to this example's README.md file for important information. + """ + ) + .font(.system(size: 14)) + } - Button("Send Note Off C3") { - sendToConnection(.noteOff(60, velocity: .midi1(0), channel: 0)) - } + private var midiInConnectionView: some View { + GroupBox(label: Text("MIDI In Connection")) { + MIDIOutputsPicker( + title: "MIDI In", + selectionID: $midiInSelectedID, + selectionDisplayName: $midiInSelectedDisplayName, + showIcons: true, + hideOwned: false + ) + .updatingInputConnection(withTag: MIDIHelper.Tags.midiIn) + .padding([.leading, .trailing], 60) + + Toggle( + "Filter Active Sensing and Clock", + isOn: $midiHelper.filterActiveSensingAndClock + ) + } + } - Button("Send CC1") { - sendToConnection(.cc(1, value: .midi1(64), channel: 0)) - } + private var midiOutConnectionView: some View { + GroupBox(label: Text("MIDI Out Connection")) { + MIDIInputsPicker( + title: "MIDI Out", + selectionID: $midiOutSelectedID, + selectionDisplayName: $midiOutSelectedDisplayName, + showIcons: true, + hideOwned: false + ) + .updatingOutputConnection(withTag: MIDIHelper.Tags.midiOut) + .padding([.leading, .trailing], 60) + + HStack { + Button("Send Note On C3") { + sendToConnection(.noteOn(60, velocity: .midi1(UInt7.random(in: 20 ... 127)), channel: 0)) + } + + Button("Send Note Off C3") { + sendToConnection(.noteOff(60, velocity: .midi1(0), channel: 0)) + } + Button("Send CC1") { + sendToConnection(.cc(1, value: .midi1(UInt7.random()), channel: 0)) } - .disabled( - midiOutSelectedID == .invalidMIDIIdentifier || - !midiManager.endpoints.inputs.contains(whereUniqueID: midiOutSelectedID) - ) } - .padding(5) - - GroupBox(label: Text("Virtual Endpoints")) { - HStack { - Button("Create Test Virtual Endpoints") { - midiHelper.createVirtualEndpoints() - } - .disabled(midiHelper.virtualsExist) + .disabled(isMIDIOutDisabled) + } + } - Button("Destroy Test Virtual Endpoints") { - midiHelper.destroyVirtualInputs() - } - .disabled(!midiHelper.virtualsExist) + private var virtualEndpointsView: some View { + GroupBox(label: Text("Virtual Endpoints")) { + HStack { + Button("Create Test Virtual Endpoints") { + midiHelper.createVirtualEndpoints() } - .frame(maxWidth: .infinity) - - HStack { - Button("Send Note On C3") { - sendToVirtuals(.noteOn(60, velocity: .midi1(127), channel: 0)) - } - - Button("Send Note Off C3") { - sendToVirtuals(.noteOff(60, velocity: .midi1(0), channel: 0)) - } - - Button("Send CC1") { - sendToVirtuals(.cc(1, value: .midi1(64), channel: 0)) - } + .disabled(midiHelper.virtualsExist) + + Button("Destroy Test Virtual Endpoints") { + midiHelper.destroyVirtualInputs() } - .frame(maxWidth: .infinity) .disabled(!midiHelper.virtualsExist) } - .padding(5) - - GroupBox(label: Text("Received Events")) { - let events = midiHelper.receivedEvents.reversed() + .frame(maxWidth: .infinity) + + HStack { + Button("Send Note On C3") { + sendToVirtuals(.noteOn(60, velocity: .midi1(UInt7.random(in: 20 ... 127)), channel: 0)) + } + + Button("Send Note Off C3") { + sendToVirtuals(.noteOff(60, velocity: .midi1(0), channel: 0)) + } - // Since MIDIEvent doesn't conform to Identifiable (and won't ever), in a List or - // ForEach we need to either use an array index or a wrap MIDIEvent in a custom - // type that does conform to Identifiable. It's really up to your use case. - // Usually application interaction is driven by MIDI events and we aren't literally - // logging events, but this is for diagnostic purposes here. - List(events.indices, id: \.self) { index in - Text(events[index].description) - .foregroundColor(color(for: events[index])) + Button("Send CC1") { + sendToVirtuals(.cc(1, value: .midi1(UInt7.random()), channel: 0)) } - .frame(minHeight: 100) } + .frame(maxWidth: .infinity) + .disabled(!midiHelper.virtualsExist) + } + } + + private var eventLogView: some View { + GroupBox(label: Text("Received Events")) { + let events = midiHelper.receivedEvents.reversed() + + // Since MIDIEvent doesn't conform to Identifiable (and won't ever), in a List or + // ForEach we need to either use an array index or a wrap MIDIEvent in a custom + // type that does conform to Identifiable. It's really up to your use case. + // Usually application interaction is driven by MIDI events and we aren't literally + // logging events, but this is for diagnostic purposes here. + List(events.indices, id: \.self) { index in + Text(events[index].description) + .foregroundColor(color(for: events[index])) + } + .frame(minHeight: 100) } - .multilineTextAlignment(.center) - .lineLimit(nil) - .padding() - .frame(minWidth: 700, minHeight: 660) } } extension ContentView { + private var isMIDIOutDisabled: Bool { + midiOutSelectedID == .invalidMIDIIdentifier || + midiOutSelectedID == nil + } + private func sendToConnection(_ event: MIDIEvent) { try? midiHelper.midiOutputConnection?.send(event: event) } diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/EndpointPickers.entitlements b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/EndpointPickers.entitlements index f2ef3ae026..852fa1a472 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/EndpointPickers.entitlements +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/EndpointPickers.entitlements @@ -2,9 +2,7 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/MIDIEndpointSelectionView-macOS.swift b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/MIDIEndpointSelectionView-macOS.swift index 36374c5e72..d3d26530bc 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/MIDIEndpointSelectionView-macOS.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/MIDIEndpointSelectionView-macOS.swift @@ -10,7 +10,7 @@ import MIDIKitIO import SwiftUI struct MIDIInSelectionView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @EnvironmentObject var midiHelper: MIDIHelper @Binding var midiInSelectedID: MIDIIdentifier @@ -21,24 +21,30 @@ struct MIDIInSelectionView: View { Text("None") .tag(MIDIIdentifier.invalidMIDIIdentifier) - if midiInSelectedID != .invalidMIDIIdentifier, - !midiManager.endpoints.outputs.contains(whereUniqueID: midiInSelectedID) - { + if isSelectedInputMissing { Text("⚠️ " + midiInSelectedDisplayName) .tag(midiInSelectedID) .foregroundColor(.secondary) } - ForEach(midiManager.endpoints.outputs) { + ForEach(midiManager.observableEndpoints.outputs) { Text($0.displayName) .tag($0.uniqueID) } } } + + private var isSelectedInputMissing: Bool { + midiInSelectedID != .invalidMIDIIdentifier && + !midiManager.observableEndpoints.outputs.contains( + whereUniqueID: midiInSelectedID, + fallbackDisplayName: midiInSelectedDisplayName + ) + } } struct MIDIOutSelectionView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @EnvironmentObject var midiHelper: MIDIHelper @Binding var midiOutSelectedID: MIDIIdentifier @@ -49,20 +55,26 @@ struct MIDIOutSelectionView: View { Text("None") .tag(MIDIIdentifier.invalidMIDIIdentifier) - if midiOutSelectedID != .invalidMIDIIdentifier, - !midiManager.endpoints.inputs.contains(whereUniqueID: midiOutSelectedID) - { + if isSelectedOutputMissing { Text("⚠️ " + midiOutSelectedDisplayName) .tag(midiOutSelectedID) .foregroundColor(.secondary) } - ForEach(midiManager.endpoints.inputs) { + ForEach(midiManager.observableEndpoints.inputs) { Text($0.displayName) .tag($0.uniqueID) } } } + + private var isSelectedOutputMissing: Bool { + midiOutSelectedID != .invalidMIDIIdentifier && + !midiManager.observableEndpoints.inputs.contains( + whereUniqueID: midiOutSelectedID, + fallbackDisplayName: midiOutSelectedDisplayName + ) + } } #endif diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/README.md b/Examples/SwiftUI Multiplatform/EndpointPickers/README.md index 39efd18b84..f189e7ca7d 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/README.md +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/README.md @@ -4,35 +4,42 @@ This example demonstrates best practises when creating MIDI input and output sel ## Key Features -- The menus are updated in real-time if endpoints change in the system. - > In SwiftUI, this happens automatically when using certain data-source properties of the `MIDIManager`. +- The pickers are updated in real-time if endpoints change in the system. + + > In SwiftUI, this happens automatically when using certain data-source properties of the `ObservableMIDIManager` class. > - > - `midiManager.endpoints.inputs` or `midiManager.endpoints.outputs` + > - `midiManager.observableEndpoints.inputs` or `midiManager.observableEndpoints.outputs` + > > Changes in MIDI endpoints in the system will trigger these arrays to refresh your view. - > - `midiManager.devices.devices` + > + > - `midiManager.observableDevices.devices` + > > Changes in MIDI devices in the system will trigger this array to refresh your view. -- The menus allow for a single endpoint to be selected, or None may be selected to disable the connection. +- The menus allow for a single endpoint to be selected, or `None` may be selected to disable the connection. + > This is one common use case. -- The menu selections are stored in UserDefaults and restored on app relaunch so the user's selections are remembered. - > This is optional but included to demonstrate that using endpoint UniqueID numbers are the proper method to persistently reference endpoints (since endpoint names can change and multiple endpoints in the system may share the same name). UserDefaults is simply a convenient location to store the setting. - > - > In order to display a missing port's name to the user, we also persistently store the port's Display Name string since it's impossible to query Core MIDI for that if the port doesn't exist in the system. +- The menu selections are stored in `UserDefaults` and restored on app relaunch so the user's selections are remembered. + > This is included to demonstrate that using endpoint Unique ID numbers are considered the primary method to persistently reference endpoints (since endpoint names can change and multiple endpoints in the system may share the same name). The endpoint's Display Name is also stored as a fallback method to identify the endpoint (this is only used if a 3rd-party manufacturer or developer fails to reassign their Unique ID each time their endpoint is registered in the system. While rare, it does happen occasionally.). `UserDefaults` is a convenient location to store the setting. + > + > A secondary reason for persistently storing the endpoints's Display Name string is to allow us to display it to the user in the UI when the endpoint is missing in the system, since it's impossible to query Core MIDI for an endpoint property if the endpoint doesn't exist in the system. + - Maintaining a user's desired selection is reserved even if it disappears from the system. + > Often a user will select a desired MIDI endpoint and want that to always remain selected, but they may disconnect the device from time to time or it may not be present in the system at the point when your app is launching or your are restoring MIDI endpoint connections in your app. (Such as a USB keyboard by powering it off or physically disconnecting it from the system). > - > In this example, if a selected endpoint no longer exists in the system, it will still remain selected in the menu but will gain a caution symbol showing that it's missing simply to communicate to the user that it is currently missing. Since we set up the managed connections with that desired endpoint's unique ID, if the endpoint reappears in the system, the `MIDIManager` will automatically reconnect it and resume data flow. The menu item will once again appear as a normal selection without the caution symbol. The user does not need to do anything and this is all handled seamlessly and automatically. - + > In this example, if a selected endpoint no longer exists in the system, it will still remain selected in the menu but will gain a caution symbol showing that it's missing simply to communicate to the user that it is currently missing. Since we set up the managed connections with that desired endpoint's unique ID, if the endpoint reappears in the system, the MIDI manager will automatically reconnect it and resume data flow. The menu item will once again appear as a normal selection without the caution symbol. The user does not need to do anything and this is all handled seamlessly and automatically. + - Received events are logged on-screen in this example, and test events can be sent to the MIDI Out selection using the buttons provided in the app's user interface. ## Special Notes -- Due to SwiftUI limitations, it is necessary (and beneficial) to abstract MIDI setup and maintenance functions inside a custom helper class instance (called `MIDIHelper` in this example) while also keeping the MIDI `MIDIManager` separate. - - This ensures the use of Combine features of the MIDI `MIDIManager` remain available, which would be lost if the `MIDIManager` was bundled inside the custom helper class. +- Due to SwiftUI limitations, it is necessary (and beneficial) to abstract MIDI setup and maintenance functions inside a custom helper class instance (called `MIDIHelper` in this example) while also keeping the MIDI manager separate. + - This ensures the use of Combine features of the `ObservableMIDIManager` remain available, which would be lost if the manager was bundled inside the custom helper class due to lack of propagation of nested `ObservableObject`s. - Since MIDI event receiver handlers are escaping closures, it's impossible to mutate SwiftUI `App` or `View` state from within them. By creating these handlers inside the helper class, we can update a `@Published` variable inside the helper class which can be observed by any view in the SwiftUI hierarchy if desired. - - This way the `MIDIManager` and helper become central services with an app-scoped lifecycle that can be passed into subviews using `.environmentObject()` + - This way the manager and helper become central services with an app-scoped lifecycle that can be passed into subviews using `.environmentObject()` - For testing purposes, try clicking the **Create Test Virtual Endpoints** button, selecting them as MIDI In and MIDI Out, then destroying them. They appear as missing but their selection is retained. Then create them again, and they will appear normally once again and connection will resume. They are remembered even if you quit the app. ## Troubleshooting diff --git a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj index 0cee04a2c4..ab2d65b6a5 100644 --- a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -104,7 +104,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E27D0E5E284F3FB600F43247 = { CreatedOnToolsVersion = 14.0; @@ -162,6 +162,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -194,6 +195,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -223,6 +225,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -255,6 +258,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -292,8 +296,7 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -328,8 +331,7 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -375,7 +377,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; E2D7FF1A29754911003212AF /* XCRemoteSwiftPackageReference "SwiftRadix" */ = { @@ -383,7 +385,7 @@ repositoryURL = "https://github.com/orchetect/SwiftRadix.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.0; + minimumVersion = 1.3.1; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/xcshareddata/xcschemes/EventParsing.xcscheme b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/xcshareddata/xcschemes/EventParsing.xcscheme new file mode 100644 index 0000000000..32ee2e351b --- /dev/null +++ b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/xcshareddata/xcschemes/EventParsing.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/ContentView.swift b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/ContentView.swift index ccd3df244c..5568113932 100644 --- a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/ContentView.swift +++ b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/ContentView.swift @@ -5,8 +5,10 @@ // import SwiftUI +import MIDIKit struct ContentView: View { + @EnvironmentObject var midiManager: ObservableMIDIManager @EnvironmentObject var midiHelper: MIDIHelper var body: some View { diff --git a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/EventParsingApp.swift b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/EventParsingApp.swift index 7c93a4b711..f7a76724a0 100644 --- a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/EventParsingApp.swift +++ b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/EventParsingApp.swift @@ -9,13 +9,13 @@ import SwiftUI @main struct EventParsingApp: App { - let midiManager = MIDIManager( + @ObservedObject var midiManager = ObservableMIDIManager( clientName: "TestAppMIDIManager", model: "TestApp", manufacturer: "MyCompany" ) - let midiHelper = MIDIHelper() + @ObservedObject var midiHelper = MIDIHelper() init() { midiHelper.setup(midiManager: midiManager) diff --git a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/MIDIHelper.swift b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/MIDIHelper.swift index c112da7254..1784d0ecb6 100644 --- a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/MIDIHelper.swift +++ b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/MIDIHelper.swift @@ -4,7 +4,7 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftRadix import SwiftUI @@ -12,13 +12,13 @@ import SwiftUI /// SwiftUI view state directly. Therefore, we need a helper class that conforms to /// `ObservableObject` which contains `@Published` properties that SwiftUI can use to update views. final class MIDIHelper: ObservableObject { - private weak var midiManager: MIDIManager? + private weak var midiManager: ObservableMIDIManager? let virtualInputName = "TestApp Input" public init() { } - public func setup(midiManager: MIDIManager) { + public func setup(midiManager: ObservableMIDIManager) { self.midiManager = midiManager do { diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj index fbcaefe5c3..f7d83daf5a 100644 --- a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj @@ -7,14 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + E23AD72D2AFE21A400A69A01 /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = E23AD72C2AFE21A400A69A01 /* MIDIKit */; }; E2B8BD9E29B308D600B92BDF /* ListsExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B8BD9D29B308D600B92BDF /* ListsExampleView.swift */; }; E2B8BDA429B30B3200B92BDF /* PickersExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B8BDA329B30B3200B92BDF /* PickersExampleView.swift */; }; - E2B8BDAD29B356CE00B92BDF /* MIDIKitUI in Frameworks */ = {isa = PBXBuildFile; productRef = E2B8BDAC29B356CE00B92BDF /* MIDIKitUI */; }; E2F1FE7429B2DF5800054467 /* MIDIKitUIExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F1FE7329B2DF5800054467 /* MIDIKitUIExampleApp.swift */; }; E2F1FE7629B2DF5800054467 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F1FE7529B2DF5800054467 /* ContentView.swift */; }; E2F1FE7829B2DF5A00054467 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2F1FE7729B2DF5A00054467 /* Assets.xcassets */; }; E2F1FE7C29B2DF5A00054467 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2F1FE7B29B2DF5A00054467 /* Preview Assets.xcassets */; }; - E2F1FE8729B2E04A00054467 /* MIDIKitIO in Frameworks */ = {isa = PBXBuildFile; productRef = E2F1FE8629B2E04A00054467 /* MIDIKitIO */; }; E2F1FE8929B2E3F600054467 /* MIDIHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F1FE8829B2E3F600054467 /* MIDIHelper.swift */; }; E2F1FE8B29B2F2ED00054467 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F1FE8A29B2F2ED00054467 /* Utilities.swift */; }; /* End PBXBuildFile section */ @@ -23,13 +22,13 @@ E2B8BD9D29B308D600B92BDF /* ListsExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListsExampleView.swift; sourceTree = ""; }; E2B8BDA329B30B3200B92BDF /* PickersExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickersExampleView.swift; sourceTree = ""; }; E2B8BDAA29B3301300B92BDF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + E2CEA4952AFE3C3200BE92F7 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; E2F1FE7029B2DF5800054467 /* MIDIKitUIExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MIDIKitUIExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; E2F1FE7329B2DF5800054467 /* MIDIKitUIExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MIDIKitUIExampleApp.swift; sourceTree = ""; }; E2F1FE7529B2DF5800054467 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; E2F1FE7729B2DF5A00054467 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; E2F1FE7929B2DF5A00054467 /* MIDIKitUIExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MIDIKitUIExample.entitlements; sourceTree = ""; }; E2F1FE7B29B2DF5A00054467 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - E2F1FE8429B2DFEF00054467 /* MIDIKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MIDIKit; path = ../../..; sourceTree = ""; }; E2F1FE8829B2E3F600054467 /* MIDIHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MIDIHelper.swift; sourceTree = ""; }; E2F1FE8A29B2F2ED00054467 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -39,8 +38,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E2B8BDAD29B356CE00B92BDF /* MIDIKitUI in Frameworks */, - E2F1FE8729B2E04A00054467 /* MIDIKitIO in Frameworks */, + E23AD72D2AFE21A400A69A01 /* MIDIKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -50,7 +48,7 @@ E2F1FE6729B2DF5800054467 = { isa = PBXGroup; children = ( - E2F1FE8429B2DFEF00054467 /* MIDIKit */, + E2CEA4952AFE3C3200BE92F7 /* README.md */, E2F1FE7229B2DF5800054467 /* MIDIKitUIExample */, E2F1FE7129B2DF5800054467 /* Products */, ); @@ -67,7 +65,6 @@ E2F1FE7229B2DF5800054467 /* MIDIKitUIExample */ = { isa = PBXGroup; children = ( - E2B8BDAA29B3301300B92BDF /* Info.plist */, E2F1FE7329B2DF5800054467 /* MIDIKitUIExampleApp.swift */, E2F1FE7529B2DF5800054467 /* ContentView.swift */, E2B8BD9D29B308D600B92BDF /* ListsExampleView.swift */, @@ -75,6 +72,7 @@ E2F1FE8829B2E3F600054467 /* MIDIHelper.swift */, E2F1FE8A29B2F2ED00054467 /* Utilities.swift */, E2F1FE7729B2DF5A00054467 /* Assets.xcassets */, + E2B8BDAA29B3301300B92BDF /* Info.plist */, E2F1FE7929B2DF5A00054467 /* MIDIKitUIExample.entitlements */, E2F1FE7A29B2DF5A00054467 /* Preview Content */, ); @@ -106,8 +104,7 @@ ); name = MIDIKitUIExample; packageProductDependencies = ( - E2F1FE8629B2E04A00054467 /* MIDIKitIO */, - E2B8BDAC29B356CE00B92BDF /* MIDIKitUI */, + E23AD72C2AFE21A400A69A01 /* MIDIKit */, ); productName = MIDIKitUI; productReference = E2F1FE7029B2DF5800054467 /* MIDIKitUIExample.app */; @@ -121,7 +118,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1500; TargetAttributes = { E2F1FE6F29B2DF5800054467 = { CreatedOnToolsVersion = 14.2; @@ -137,6 +134,9 @@ Base, ); mainGroup = E2F1FE6729B2DF5800054467; + packageReferences = ( + E23AD72A2AFE218A00A69A01 /* XCRemoteSwiftPackageReference "MIDIKit" */, + ); productRefGroup = E2F1FE7129B2DF5800054467 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -179,6 +179,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -208,9 +209,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -239,6 +242,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -268,9 +272,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -297,6 +303,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"MIDIKitUIExample/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; @@ -338,6 +345,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"MIDIKitUIExample/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; @@ -393,14 +401,22 @@ }; /* End XCConfigurationList section */ -/* Begin XCSwiftPackageProductDependency section */ - E2B8BDAC29B356CE00B92BDF /* MIDIKitUI */ = { - isa = XCSwiftPackageProductDependency; - productName = MIDIKitUI; +/* Begin XCRemoteSwiftPackageReference section */ + E23AD72A2AFE218A00A69A01 /* XCRemoteSwiftPackageReference "MIDIKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/orchetect/MIDIKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; + }; }; - E2F1FE8629B2E04A00054467 /* MIDIKitIO */ = { +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + E23AD72C2AFE21A400A69A01 /* MIDIKit */ = { isa = XCSwiftPackageProductDependency; - productName = MIDIKitIO; + package = E23AD72A2AFE218A00A69A01 /* XCRemoteSwiftPackageReference "MIDIKit" */; + productName = MIDIKit; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/xcshareddata/xcschemes/MIDIKitUIExample.xcscheme b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/xcshareddata/xcschemes/MIDIKitUIExample.xcscheme index d74d8ac5e4..0c93fbf4b3 100644 --- a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/xcshareddata/xcschemes/MIDIKitUIExample.xcscheme +++ b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/xcshareddata/xcschemes/MIDIKitUIExample.xcscheme @@ -1,6 +1,6 @@ + + + + **Warning**: This project is still a work-in-progress and some features may be broken until a future update. - -## To Do - -- [ ] Add multiple-selection list controls -- [ ] Demonstrate updating connections in the `MIDIManager` on endpoint selection change -- [ ] Add `unownedOnly: Bool` parameter to controls so they can filter out manager-owned virtual endpoints (♻️) \ No newline at end of file diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj index f3a77bd261..5032a8f1f5 100644 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj @@ -3,13 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ E20CF4BD299C7F6200F0E003 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20CF4BC299C7F6200F0E003 /* SceneDelegate.swift */; }; E20CF4C7299C7F6300F0E003 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E20CF4C5299C7F6300F0E003 /* LaunchScreen.storyboard */; }; - E20CF4CC299C7F8100F0E003 /* DetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B849B25EDA4F400080052 /* DetailsView.swift */; }; + E20CF4CC299C7F8100F0E003 /* TableDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B849B25EDA4F400080052 /* TableDetailsView.swift */; }; E20CF4CD299C7F8400F0E003 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E230C4A425AE7B31005DCB55 /* ContentView.swift */; }; E20CF4CE299C7F8C00F0E003 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20CF4B2299C7E2A00F0E003 /* AppDelegate.swift */; }; E20CF4CF299C7F9700F0E003 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E24696C22880B69E00485518 /* Images.xcassets */; }; @@ -17,9 +17,27 @@ E230C4A325AE7B31005DCB55 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E230C4A225AE7B31005DCB55 /* AppDelegate.swift */; }; E230C4A525AE7B31005DCB55 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E230C4A425AE7B31005DCB55 /* ContentView.swift */; }; E230C4AD25AE7B33005DCB55 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E230C4AB25AE7B33005DCB55 /* Main.storyboard */; }; + E23AD70E2AFE1C5400A69A01 /* DeviceTreeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD70D2AFE1C5400A69A01 /* DeviceTreeView.swift */; }; + E23AD70F2AFE1C5400A69A01 /* DeviceTreeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD70D2AFE1C5400A69A01 /* DeviceTreeView.swift */; }; + E23AD7112AFE1C8700A69A01 /* OtherInputsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD7102AFE1C8700A69A01 /* OtherInputsView.swift */; }; + E23AD7122AFE1C8700A69A01 /* OtherInputsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD7102AFE1C8700A69A01 /* OtherInputsView.swift */; }; + E23AD7142AFE1CAC00A69A01 /* OtherOutputsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD7132AFE1CAC00A69A01 /* OtherOutputsView.swift */; }; + E23AD7152AFE1CAC00A69A01 /* OtherOutputsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD7132AFE1CAC00A69A01 /* OtherOutputsView.swift */; }; + E23AD7172AFE1CF000A69A01 /* ItemIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD7162AFE1CF000A69A01 /* ItemIcon.swift */; }; + E23AD7182AFE1CF000A69A01 /* ItemIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD7162AFE1CF000A69A01 /* ItemIcon.swift */; }; + E23AD71C2AFE1E3800A69A01 /* EmptyDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD71B2AFE1E3800A69A01 /* EmptyDetailsView.swift */; }; + E23AD71D2AFE1E3800A69A01 /* EmptyDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD71B2AFE1E3800A69A01 /* EmptyDetailsView.swift */; }; + E23AD71F2AFE1E5600A69A01 /* DetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD71E2AFE1E5600A69A01 /* DetailsView.swift */; }; + E23AD7202AFE1E5600A69A01 /* DetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD71E2AFE1E5600A69A01 /* DetailsView.swift */; }; + E23AD7222AFE1E8700A69A01 /* Details Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD7212AFE1E8700A69A01 /* Details Content.swift */; }; + E23AD7232AFE1E8700A69A01 /* Details Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD7212AFE1E8700A69A01 /* Details Content.swift */; }; + E23AD7252AFE1EC000A69A01 /* Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD7242AFE1EC000A69A01 /* Property.swift */; }; + E23AD7262AFE1EC000A69A01 /* Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD7242AFE1EC000A69A01 /* Property.swift */; }; + E23AD7282AFE1EE700A69A01 /* LegacyDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD7272AFE1EE700A69A01 /* LegacyDetailsView.swift */; }; + E23AD7292AFE1EE700A69A01 /* LegacyDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23AD7272AFE1EE700A69A01 /* LegacyDetailsView.swift */; }; E24696C32880B69E00485518 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E24696C22880B69E00485518 /* Images.xcassets */; }; E24D200B25BE75D90095BDE5 /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = E24D200A25BE75D90095BDE5 /* MIDIKit */; }; - E26B849C25EDA4F400080052 /* DetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B849B25EDA4F400080052 /* DetailsView.swift */; }; + E26B849C25EDA4F400080052 /* TableDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B849B25EDA4F400080052 /* TableDetailsView.swift */; }; E29AC90F285BEFF4009D1C2C /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = E29AC90E285BEFF4009D1C2C /* MIDIKit */; }; /* End PBXBuildFile section */ @@ -35,8 +53,18 @@ E230C4AC25AE7B33005DCB55 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; E230C4AE25AE7B33005DCB55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E230C4AF25AE7B33005DCB55 /* MIDISystemInfo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MIDISystemInfo.entitlements; sourceTree = ""; }; + E23AD70D2AFE1C5400A69A01 /* DeviceTreeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTreeView.swift; sourceTree = ""; }; + E23AD7102AFE1C8700A69A01 /* OtherInputsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherInputsView.swift; sourceTree = ""; }; + E23AD7132AFE1CAC00A69A01 /* OtherOutputsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OtherOutputsView.swift; sourceTree = ""; }; + E23AD7162AFE1CF000A69A01 /* ItemIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemIcon.swift; sourceTree = ""; }; + E23AD71B2AFE1E3800A69A01 /* EmptyDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyDetailsView.swift; sourceTree = ""; }; + E23AD71E2AFE1E5600A69A01 /* DetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailsView.swift; sourceTree = ""; }; + E23AD7212AFE1E8700A69A01 /* Details Content.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Details Content.swift"; sourceTree = ""; }; + E23AD7242AFE1EC000A69A01 /* Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Property.swift; sourceTree = ""; }; + E23AD7272AFE1EE700A69A01 /* LegacyDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyDetailsView.swift; sourceTree = ""; }; E24696C22880B69E00485518 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; - E26B849B25EDA4F400080052 /* DetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsView.swift; sourceTree = ""; }; + E26B849B25EDA4F400080052 /* TableDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableDetailsView.swift; sourceTree = ""; }; + E2CEA4962AFE3C6100BE92F7 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +113,7 @@ E230C49625AE7B31005DCB55 = { isa = PBXGroup; children = ( + E2CEA4962AFE3C6100BE92F7 /* README.md */, E230C4A125AE7B31005DCB55 /* MIDISystemInfo */, E230C4A025AE7B31005DCB55 /* Products */, ); @@ -104,13 +133,38 @@ children = ( E20CF4B1299C7E1B00F0E003 /* iOS */, E20CF4B0299C7DE300F0E003 /* macOS */, - E230C4A425AE7B31005DCB55 /* ContentView.swift */, - E26B849B25EDA4F400080052 /* DetailsView.swift */, + E23AD7192AFE1DF200A69A01 /* Navigation */, + E23AD71A2AFE1E1800A69A01 /* Details */, E24696C22880B69E00485518 /* Images.xcassets */, ); path = MIDISystemInfo; sourceTree = ""; }; + E23AD7192AFE1DF200A69A01 /* Navigation */ = { + isa = PBXGroup; + children = ( + E230C4A425AE7B31005DCB55 /* ContentView.swift */, + E23AD70D2AFE1C5400A69A01 /* DeviceTreeView.swift */, + E23AD7102AFE1C8700A69A01 /* OtherInputsView.swift */, + E23AD7132AFE1CAC00A69A01 /* OtherOutputsView.swift */, + E23AD7162AFE1CF000A69A01 /* ItemIcon.swift */, + ); + path = Navigation; + sourceTree = ""; + }; + E23AD71A2AFE1E1800A69A01 /* Details */ = { + isa = PBXGroup; + children = ( + E23AD7212AFE1E8700A69A01 /* Details Content.swift */, + E23AD7242AFE1EC000A69A01 /* Property.swift */, + E23AD71E2AFE1E5600A69A01 /* DetailsView.swift */, + E23AD71B2AFE1E3800A69A01 /* EmptyDetailsView.swift */, + E23AD7272AFE1EE700A69A01 /* LegacyDetailsView.swift */, + E26B849B25EDA4F400080052 /* TableDetailsView.swift */, + ); + path = Details; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -161,8 +215,9 @@ E230C49725AE7B31005DCB55 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E20CF4B7299C7F6200F0E003 = { CreatedOnToolsVersion = 14.2; @@ -221,9 +276,18 @@ buildActionMask = 2147483647; files = ( E20CF4CE299C7F8C00F0E003 /* AppDelegate.swift in Sources */, + E23AD7182AFE1CF000A69A01 /* ItemIcon.swift in Sources */, + E23AD7122AFE1C8700A69A01 /* OtherInputsView.swift in Sources */, + E23AD7292AFE1EE700A69A01 /* LegacyDetailsView.swift in Sources */, E20CF4CD299C7F8400F0E003 /* ContentView.swift in Sources */, - E20CF4CC299C7F8100F0E003 /* DetailsView.swift in Sources */, + E23AD7202AFE1E5600A69A01 /* DetailsView.swift in Sources */, + E23AD7152AFE1CAC00A69A01 /* OtherOutputsView.swift in Sources */, + E23AD7262AFE1EC000A69A01 /* Property.swift in Sources */, + E23AD70F2AFE1C5400A69A01 /* DeviceTreeView.swift in Sources */, + E20CF4CC299C7F8100F0E003 /* TableDetailsView.swift in Sources */, + E23AD71D2AFE1E3800A69A01 /* EmptyDetailsView.swift in Sources */, E20CF4BD299C7F6200F0E003 /* SceneDelegate.swift in Sources */, + E23AD7232AFE1E8700A69A01 /* Details Content.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -231,9 +295,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E26B849C25EDA4F400080052 /* DetailsView.swift in Sources */, + E23AD7112AFE1C8700A69A01 /* OtherInputsView.swift in Sources */, + E23AD7222AFE1E8700A69A01 /* Details Content.swift in Sources */, + E23AD7252AFE1EC000A69A01 /* Property.swift in Sources */, + E23AD71C2AFE1E3800A69A01 /* EmptyDetailsView.swift in Sources */, + E23AD7282AFE1EE700A69A01 /* LegacyDetailsView.swift in Sources */, + E26B849C25EDA4F400080052 /* TableDetailsView.swift in Sources */, E230C4A525AE7B31005DCB55 /* ContentView.swift in Sources */, + E23AD70E2AFE1C5400A69A01 /* DeviceTreeView.swift in Sources */, + E23AD7142AFE1CAC00A69A01 /* OtherOutputsView.swift in Sources */, E230C4A325AE7B31005DCB55 /* AppDelegate.swift in Sources */, + E23AD7172AFE1CF000A69A01 /* ItemIcon.swift in Sources */, + E23AD71F2AFE1E5600A69A01 /* DetailsView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -330,6 +403,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -364,6 +438,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -393,6 +468,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -427,6 +503,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -543,7 +620,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/xcshareddata/xcschemes/MIDISystemInfo iOS.xcscheme b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/xcshareddata/xcschemes/MIDISystemInfo iOS.xcscheme index 9c318afc72..1c3ce76543 100644 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/xcshareddata/xcschemes/MIDISystemInfo iOS.xcscheme +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/xcshareddata/xcschemes/MIDISystemInfo iOS.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + : View { - @EnvironmentObject var midiManager: MIDIManager - - let detailsContent: ( - _ object: AnyMIDIIOObject?, - _ showAllBinding: Binding - ) -> DetailsContent - - var body: some View { - NavigationView { - sidebar - .frame(width: 300) - - EmptyDetailsView() - } - .navigationViewStyle(DoubleColumnNavigationViewStyle()) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .environmentObject(midiManager) - } - - private var sidebar: some View { - List { - DeviceTreeView(detailsContent: detailsContent) - OtherInputsView(detailsContent: detailsContent) - OtherOutputsView(detailsContent: detailsContent) - } - #if os(macOS) - .listStyle(.sidebar) - #endif - } -} - -struct DeviceTreeView: View { - @EnvironmentObject private var midiManager: MIDIManager - - let detailsContent: ( - _ object: AnyMIDIIOObject?, - _ showAllBinding: Binding - ) -> DetailsContent - - var body: some View { - Section(header: Text("Device Tree")) { - ForEach(deviceTreeItems) { item in - navLink(item: item) - } - } - } - - private func navLink(item: AnyMIDIIOObject) -> some View { - NavigationLink(destination: detailsView(item: item)) { - switch item.objectType { - case .device: - // SwiftUI doesn't allow 'break' in a switch case - // so just put a 0x0 pixel spacer here - Spacer() - .frame(width: 0, height: 0, alignment: .center) - - case .entity: - Spacer() - .frame(width: 24, height: 18, alignment: .center) - - case .inputEndpoint, .outputEndpoint: - Spacer() - .frame(width: 48, height: 18, alignment: .center) - } - - ItemIcon(item: item, default: Text("🎹")) - - Text("\(item.name)") - if item.objectType == .inputEndpoint { - Text("(In)") - } else if item.objectType == .outputEndpoint { - Text("(Out)") - } - } - } - - private func detailsView(item: AnyMIDIIOObject) -> some View { - DetailsView( - object: item.asAnyMIDIIOObject(), - detailsContent: detailsContent - ) - } - - private var deviceTreeItems: [AnyMIDIIOObject] { - midiManager.devices.devices - .sortedByName() - .flatMap { - [$0.asAnyMIDIIOObject()] - + $0.entities - .flatMap { - [$0.asAnyMIDIIOObject()] - + $0.inputs.asAnyMIDIIOObjects() - + $0.outputs.asAnyMIDIIOObjects() - } - } - } -} - -struct OtherInputsView: View { - @EnvironmentObject private var midiManager: MIDIManager - - let detailsContent: ( - _ object: AnyMIDIIOObject?, - _ showAllBinding: Binding - ) -> DetailsContent - - var body: some View { - Section(header: Text("Other Inputs")) { - ForEach(otherInputs) { item in - NavigationLink(destination: detailsView(item: item)) { - ItemIcon(item: item.asAnyMIDIIOObject(), default: Text("🎵")) - Text("\(item.name)") - } - } - } - } - - private func detailsView(item: MIDIInputEndpoint) -> some View { - DetailsView( - object: item.asAnyMIDIIOObject(), - detailsContent: detailsContent - ) - } - - private var otherInputs: [MIDIInputEndpoint] { - // filter out endpoints that have an entity because - // they are already being displayed in the Devices tree - midiManager.endpoints.inputs.sortedByName() - .filter { $0.entity == nil } - } -} - -struct OtherOutputsView: View { - @EnvironmentObject private var midiManager: MIDIManager - - let detailsContent: ( - _ object: AnyMIDIIOObject?, - _ showAllBinding: Binding - ) -> DetailsContent - - var body: some View { - Section(header: Text("Other Outputs")) { - ForEach(otherOutputs) { item in - NavigationLink(destination: detailsView(item: item)) { - ItemIcon(item: item.asAnyMIDIIOObject(), default: Text("🎵")) - Text("\(item.name)") - } - } - } - } - - private func detailsView(item: MIDIOutputEndpoint) -> some View { - DetailsView( - object: item.asAnyMIDIIOObject(), - detailsContent: detailsContent - ) - } - - private var otherOutputs: [MIDIOutputEndpoint] { - // filter out endpoints that have an entity because - // they are already being displayed in the Devices tree - midiManager.endpoints.outputs.sortedByName() - .filter { $0.entity == nil } - } -} - -struct ItemIcon: View { - let item: AnyMIDIIOObject - let `default`: Content - - var body: some View { - Group { - if let img = image { - img - } else { - Text("🎹") - } - } - .frame(width: 18, height: 18, alignment: .center) - } - - #if os(macOS) - private var image: Image? { - guard let img = item.imageAsNSImage else { return nil } - return Image(nsImage: img).resizable() - } - - #elseif os(iOS) - private var image: Image? { - guard let img = item.imageAsUIImage else { return nil } - return Image(uiImage: img).resizable() - } - #endif -} - -struct ContentViewCatalina_Previews: PreviewProvider { - static let midiManager = MIDIManager( - clientName: "Preview", - model: "", - manufacturer: "" - ) - - static var previews: some View { - ContentViewForCurrentPlatform() - } -} diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/Details Content.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/Details Content.swift new file mode 100644 index 0000000000..cfc86ce570 --- /dev/null +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/Details Content.swift @@ -0,0 +1,51 @@ +// +// DetailsContent.swift +// MIDISystemInfo +// +// Created by Steffan Andrews on 2023-11-10. +// + +import MIDIKitIO +import SwiftUI + +protocol DetailsContent where Self: View { + var object: AnyMIDIIOObject? { get set } + var showAll: Bool { get set } + + var properties: [Property] { get nonmutating set } + var selection: Set { get set } +} + +extension DetailsContent { + func refreshProperties() { + guard let unwrappedObject = object else { return } + + properties = unwrappedObject.propertyStringValues(relevantOnly: !showAll) + .map { Property(key: $0.key, value: $0.value) } + } + + func selectedItemsProviders() -> [NSItemProvider] { + let str: String + + switch selection.count { + case 0: + return [] + + case 1: // single + // just return value + str = properties + .first { $0.id == selection.first! }? + .value ?? "" + + default: // multiple + // return key/value pairs, one per line + str = properties + .filter { selection.contains($0.key) } + .map { "\($0.key): \($0.value)" } + .joined(separator: "\n") + } + + let provider: NSItemProvider = .init(object: str as NSString) + return [provider] + } +} diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/DetailsView.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/DetailsView.swift new file mode 100644 index 0000000000..a43c5c37f1 --- /dev/null +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/DetailsView.swift @@ -0,0 +1,41 @@ +// +// DetailsView.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +import MIDIKit +import SwiftUI + +struct DetailsView: View { + let object: AnyMIDIIOObject? + + let detailsContent: ( + _ object: AnyMIDIIOObject?, + _ showAllBinding: Binding + ) -> Content + + @State private var showAll: Bool = false + + var body: some View { + if let unwrappedObject = object { + detailsContent(unwrappedObject, $showAll) + + Group { + if showAll { + Button("Show Relevant Properties") { + showAll.toggle() + } + } else { + Button("Show All") { + showAll.toggle() + } + } + } + .padding(.all, 10) + + } else { + EmptyDetailsView() + } + } +} diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/EmptyDetailsView.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/EmptyDetailsView.swift new file mode 100644 index 0000000000..2d41f372a0 --- /dev/null +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/EmptyDetailsView.swift @@ -0,0 +1,26 @@ +// +// EmptyDetailsView.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +import MIDIKit +import SwiftUI + +struct EmptyDetailsView: View { + var body: some View { + VStack { + if #available(macOS 11.0, iOS 14.0, *) { + Image(systemName: "pianokeys") + .resizable() + .foregroundColor(.secondary) + .frame(width: 200, height: 200) + Spacer() + .frame(height: 50) + } + Text("Make a selection from the sidebar.") + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/LegacyDetailsView.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/LegacyDetailsView.swift new file mode 100644 index 0000000000..4faa673b99 --- /dev/null +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/LegacyDetailsView.swift @@ -0,0 +1,63 @@ +// +// LegacyDetailsView.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +import Combine +import MIDIKit +import SwiftUI + +/// Legacy details view for systems prior to macOS 12 / iOS 16. +struct LegacyDetailsView: View, DetailsContent { + public var object: AnyMIDIIOObject? + @Binding public var showAll: Bool + + @State var properties: [Property] = [] + @State var selection: Set = [] + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + List(selection: $selection) { + Section { + ForEach(properties) { + Row(property: $0).tag($0) + } + } header: { + Row(property: Property(key: "Property", value: "Value")) + .font(.headline) + } footer: { + // empty + } + } + #if os(macOS) + .onCopyCommand { + selectedItemsProviders() + } + #endif + } + .onAppear { + refreshProperties() + } + .onReceive(Just(showAll)) { _ in // workaround since we can't use onChange {} + refreshProperties() + } + } +} + +extension LegacyDetailsView { + private struct Row: View, Identifiable { + let property: Property + + var id: Property.ID { property.id } + + var body: some View { + HStack(alignment: .top) { + Text(property.key) + .frame(width: 220, alignment: .leading) + Text(property.value) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } +} diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/Property.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/Property.swift new file mode 100644 index 0000000000..d59c1b5d1e --- /dev/null +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/Property.swift @@ -0,0 +1,16 @@ +// +// Property.swift +// MIDISystemInfo +// +// Created by Steffan Andrews on 2023-11-10. +// + +import MIDIKitIO +import SwiftUI + +struct Property: Identifiable, Hashable { + let key: String + let value: String + + var id: String { key } +} diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/TableDetailsView.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/TableDetailsView.swift new file mode 100644 index 0000000000..f12014526f --- /dev/null +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/TableDetailsView.swift @@ -0,0 +1,67 @@ +// +// TableDetailsView.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +import MIDIKitIO +import SwiftUI + +/// Modern details view. +@available(macOS 12.0, iOS 16.0, *) +struct TableDetailsView: View, DetailsContent { + #if os(iOS) + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + private var isCompact: Bool { horizontalSizeClass == .compact } + #else + private let isCompact = false + #endif + + public var object: AnyMIDIIOObject? + @Binding public var showAll: Bool + + @State var properties: [Property] = [] + @State var selection: Set = [] + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + table + .onAppear { + refreshProperties() + } + .onChange(of: showAll) { _ in + refreshProperties() + } + #if os(macOS) + .tableStyle(.inset(alternatesRowBackgrounds: true)) + .onCopyCommand { + selectedItemsProviders() + } + #elseif os(iOS) + .tableStyle(InsetTableStyle()) + #endif + } + } + + @ViewBuilder + private var table: some View { + if isCompact { + Table(properties, selection: $selection) { + TableColumn("Property") { property in + HStack { + Text(property.key) + Spacer() + Text(property.value) + .foregroundColor(.secondary) + } + } + } + } else { + Table(properties, selection: $selection) { + TableColumn("Property", value: \.key) + .width(min: 50, ideal: 120, max: 250) + TableColumn("Value", value: \.value) + } + } + } +} diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/DetailsView.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/DetailsView.swift deleted file mode 100644 index 0368a0b7a5..0000000000 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/DetailsView.swift +++ /dev/null @@ -1,198 +0,0 @@ -// -// DetailsView.swift -// MIDIKit • https://github.com/orchetect/MIDIKit -// © 2021-2023 Steffan Andrews • Licensed under MIT License -// - -import Combine -import MIDIKit -import SwiftUI - -// MARK: - Empty Details Views - -struct EmptyDetailsView: View { - var body: some View { - VStack { - if #available(macOS 11.0, iOS 14.0, *) { - Image(systemName: "pianokeys") - .resizable() - .foregroundColor(.secondary) - .frame(width: 200, height: 200) - Spacer() - .frame(height: 50) - } - Text("Make a selection from the sidebar.") - } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} - -// MARK: - Surrogate Details View - -struct DetailsView: View { - let object: AnyMIDIIOObject? - let detailsContent: ( - _ object: AnyMIDIIOObject?, - _ showAllBinding: Binding - ) -> Content - - @State private var showAll: Bool = false - - var body: some View { - if let unwrappedObject = object { - detailsContent(unwrappedObject, $showAll) - - Group { - if showAll { - Button("Show Relevant Properties") { - showAll.toggle() - } - } else { - Button("Show All") { - showAll.toggle() - } - } - } - .padding(.all, 10) - - } else { - EmptyDetailsView() - } - } -} - -// MARK: - Per-Platform Details Views - -protocol DetailsContent where Self: View { - var object: AnyMIDIIOObject? { get set } - var showAll: Bool { get set } - - var properties: [Property] { get nonmutating set } - var selection: Set { get set } -} - -struct Property: Identifiable, Hashable { - let key: String - let value: String - - var id: String { key } -} - -extension DetailsContent { - func refreshProperties() { - guard let unwrappedObject = object else { return } - properties = unwrappedObject.propertyStringValues(relevantOnly: !showAll) - .map { Property(key: $0.key, value: $0.value) } - } - - func selectedItemsProviders() -> [NSItemProvider] { - let str: String - - switch selection.count { - case 0: - return [] - - case 1: // single - // just return value - str = properties - .first { $0.id == selection.first! }? - .value ?? "" - - default: // multiple - // return key/value pairs, one per line - str = properties - .filter { selection.contains($0.key) } - .map { "\($0.key): \($0.value)" } - .joined(separator: "\n") - } - - let provider: NSItemProvider = .init(object: str as NSString) - return [provider] - } -} - -/// Legacy details view for systems prior to macOS 12 / iOS 16. -struct LegacyDetailsView: View, DetailsContent { - public var object: AnyMIDIIOObject? - @Binding public var showAll: Bool - - @State var properties: [Property] = [] - @State var selection: Set = [] - - var body: some View { - VStack(alignment: .leading, spacing: 20) { - List(selection: $selection) { - Section { - ForEach(properties) { - Row(property: $0).tag($0) - } - } header: { - Row(property: Property(key: "Property", value: "Value")) - .font(.headline) - } footer: { - // empty - } - } - #if os(macOS) - .onCopyCommand { - selectedItemsProviders() - } - #endif - } - .onAppear { - refreshProperties() - } - .onReceive(Just(showAll)) { _ in // workaround since we can't use onChange {} - refreshProperties() - } - } - - struct Row: View, Identifiable { - let property: Property - - var id: Property.ID { property.id } - - var body: some View { - HStack(alignment: .top) { - Text(property.key) - .frame(width: 220, alignment: .leading) - Text(property.value) - .frame(maxWidth: .infinity, alignment: .leading) - } - } - } -} - -/// Modern details view. -@available(macOS 12.0, iOS 16.0, *) -struct TableDetailsView: View, DetailsContent { - public var object: AnyMIDIIOObject? - @Binding public var showAll: Bool - - @State var properties: [Property] = [] - @State var selection: Set = [] - - var body: some View { - VStack(alignment: .leading, spacing: 20) { - Table(properties, selection: $selection) { - TableColumn("Property", value: \.key).width(min: 50, ideal: 120, max: 250) - TableColumn("Value", value: \.value) - } - .onAppear { - refreshProperties() - } - .onChange(of: showAll) { _ in - refreshProperties() - } - #if os(macOS) - .tableStyle(.inset(alternatesRowBackgrounds: true)) - .onCopyCommand { - selectedItemsProviders() - } - #elseif os(iOS) - .tableStyle(InsetTableStyle()) - #endif - } - } -} diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/ContentView.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/ContentView.swift new file mode 100644 index 0000000000..af9cd1a489 --- /dev/null +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/ContentView.swift @@ -0,0 +1,68 @@ +// +// ContentView.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +import MIDIKitIO +import SwiftUI + +/// Dynamically uses modern UI elements when the platform supports it. +struct ContentViewForCurrentPlatform: View { + var body: some View { + if #available(macOS 12, iOS 16, *) { + return ContentView { object, showAll in + TableDetailsView(object: object, showAll: showAll) + } + } else { + return ContentView { object, showAll in + LegacyDetailsView(object: object, showAll: showAll) + } + } + } +} + +struct ContentView: View { + @EnvironmentObject private var midiManager: ObservableMIDIManager + + let detailsContent: ( + _ object: AnyMIDIIOObject?, + _ showAllBinding: Binding + ) -> DetailsContent + + var body: some View { + NavigationView { + sidebar + .frame(width: 300) + + EmptyDetailsView() + } + .navigationViewStyle(DoubleColumnNavigationViewStyle()) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .environmentObject(midiManager) + } + + private var sidebar: some View { + List { + DeviceTreeView(detailsContent: detailsContent) + OtherInputsView(detailsContent: detailsContent) + OtherOutputsView(detailsContent: detailsContent) + } + #if os(macOS) + .listStyle(.sidebar) + #endif + } +} + +struct ContentViewPreviews: PreviewProvider { + static let midiManager = ObservableMIDIManager( + clientName: "Preview", + model: "Preview", + manufacturer: "MyCompany" + ) + + static var previews: some View { + ContentViewForCurrentPlatform() + .environmentObject(midiManager) + } +} diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/DeviceTreeView.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/DeviceTreeView.swift new file mode 100644 index 0000000000..d3deaf5335 --- /dev/null +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/DeviceTreeView.swift @@ -0,0 +1,75 @@ +// +// DeviceTreeView.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +import MIDIKitIO +import SwiftUI + +struct DeviceTreeView: View { + @EnvironmentObject private var midiManager: ObservableMIDIManager + + let detailsContent: ( + _ object: AnyMIDIIOObject?, + _ showAllBinding: Binding + ) -> DetailsContent + + var body: some View { + Section(header: Text("Device Tree")) { + ForEach(deviceTreeItems) { item in + navLink(item: item) + } + } + } + + private func navLink(item: AnyMIDIIOObject) -> some View { + NavigationLink(destination: detailsView(item: item)) { + switch item.objectType { + case .device: + // SwiftUI doesn't allow 'break' in a switch case + // so just put a 0x0 pixel spacer here + Spacer() + .frame(width: 0, height: 0, alignment: .center) + + case .entity: + Spacer() + .frame(width: 24, height: 18, alignment: .center) + + case .inputEndpoint, .outputEndpoint: + Spacer() + .frame(width: 48, height: 18, alignment: .center) + } + + ItemIcon(item: item, default: Text("🎹")) + + Text("\(item.name)") + if item.objectType == .inputEndpoint { + Text("(In)") + } else if item.objectType == .outputEndpoint { + Text("(Out)") + } + } + } + + private func detailsView(item: AnyMIDIIOObject) -> some View { + DetailsView( + object: item.asAnyMIDIIOObject(), + detailsContent: detailsContent + ) + } + + private var deviceTreeItems: [AnyMIDIIOObject] { + midiManager.devices.devices + .sortedByName() + .flatMap { + [$0.asAnyMIDIIOObject()] + + $0.entities + .flatMap { + [$0.asAnyMIDIIOObject()] + + $0.inputs.asAnyMIDIIOObjects() + + $0.outputs.asAnyMIDIIOObjects() + } + } + } +} diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/ItemIcon.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/ItemIcon.swift new file mode 100644 index 0000000000..fb983c96a6 --- /dev/null +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/ItemIcon.swift @@ -0,0 +1,36 @@ +// +// ItemIcon.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +import MIDIKit +import SwiftUI + +struct ItemIcon: View { + let item: AnyMIDIIOObject + let `default`: Content + + var body: some View { + Group { + if let img = image { + img + } else { + Text("🎹") + } + } + .frame(width: 18, height: 18, alignment: .center) + } + + #if os(macOS) + private var image: Image? { + guard let img = item.imageAsNSImage else { return nil } + return Image(nsImage: img).resizable() + } + #elseif os(iOS) + private var image: Image? { + guard let img = item.imageAsUIImage else { return nil } + return Image(uiImage: img).resizable() + } + #endif +} diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherInputsView.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherInputsView.swift new file mode 100644 index 0000000000..1506494329 --- /dev/null +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherInputsView.swift @@ -0,0 +1,42 @@ +// +// OtherInputsView.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +import MIDIKitIO +import SwiftUI + +struct OtherInputsView: View { + @EnvironmentObject private var midiManager: ObservableMIDIManager + + let detailsContent: ( + _ object: AnyMIDIIOObject?, + _ showAllBinding: Binding + ) -> DetailsContent + + var body: some View { + Section(header: Text("Other Inputs")) { + ForEach(otherInputs) { item in + NavigationLink(destination: detailsView(item: item)) { + ItemIcon(item: item.asAnyMIDIIOObject(), default: Text("🎵")) + Text("\(item.name)") + } + } + } + } + + private func detailsView(item: MIDIInputEndpoint) -> some View { + DetailsView( + object: item.asAnyMIDIIOObject(), + detailsContent: detailsContent + ) + } + + private var otherInputs: [MIDIInputEndpoint] { + // filter out endpoints that have an entity because + // they are already being displayed in the Devices tree + midiManager.observableEndpoints.inputs.sortedByName() + .filter { $0.entity == nil } + } +} diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherOutputsView.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherOutputsView.swift new file mode 100644 index 0000000000..24d427d257 --- /dev/null +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherOutputsView.swift @@ -0,0 +1,42 @@ +// +// OtherOutputsView.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +import MIDIKitIO +import SwiftUI + +struct OtherOutputsView: View { + @EnvironmentObject private var midiManager: ObservableMIDIManager + + let detailsContent: ( + _ object: AnyMIDIIOObject?, + _ showAllBinding: Binding + ) -> DetailsContent + + var body: some View { + Section(header: Text("Other Outputs")) { + ForEach(otherOutputs) { item in + NavigationLink(destination: detailsView(item: item)) { + ItemIcon(item: item.asAnyMIDIIOObject(), default: Text("🎵")) + Text("\(item.name)") + } + } + } + } + + private func detailsView(item: MIDIOutputEndpoint) -> some View { + DetailsView( + object: item.asAnyMIDIIOObject(), + detailsContent: detailsContent + ) + } + + private var otherOutputs: [MIDIOutputEndpoint] { + // filter out endpoints that have an entity because + // they are already being displayed in the Devices tree + midiManager.observableEndpoints.outputs.sortedByName() + .filter { $0.entity == nil } + } +} diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/iOS/AppDelegate.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/iOS/AppDelegate.swift index fc99d7fdd1..77e74faf52 100644 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/iOS/AppDelegate.swift +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/iOS/AppDelegate.swift @@ -9,7 +9,7 @@ import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - static let midiManager = MIDIManager( + static let midiManager = ObservableMIDIManager( clientName: "MIDISystemInfo", model: "TestApp", manufacturer: "MyCompany" diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/iOS/SceneDelegate.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/iOS/SceneDelegate.swift index 066ffd0d1f..2ad2fc096e 100644 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/iOS/SceneDelegate.swift +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/iOS/SceneDelegate.swift @@ -27,24 +27,4 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window.makeKeyAndVisible() } } - - func sceneDidDisconnect(_ scene: UIScene) { - // empty - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // empty - } - - func sceneWillResignActive(_ scene: UIScene) { - // empty - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // empty - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // empty - } } diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/macOS/AppDelegate.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/macOS/AppDelegate.swift index 41f1facf61..9d886bf4cb 100644 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/macOS/AppDelegate.swift +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/macOS/AppDelegate.swift @@ -11,7 +11,7 @@ import SwiftUI class AppDelegate: NSObject, NSApplicationDelegate { var window: NSWindow! - private let midiManager = MIDIManager( + private let midiManager = ObservableMIDIManager( clientName: "MIDISystemInfo", model: "TestApp", manufacturer: "MyCompany" diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/README.md b/Examples/SwiftUI Multiplatform/MIDISystemInfo/README.md index c814a06884..74ff1cc05d 100644 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/README.md +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/README.md @@ -2,6 +2,8 @@ This example demonstrates reading MIDI device/port information from the system. It is also a useful diagnostic workbench. +The example app's structure is using legacy `NSApplicationDelegate`/`UIApplicationDelegate` in order to maintain backwards compatibility with macOS 10.10 and iOS 13. + ## Key Features - Lists devices, their entities, and their endpoints in a navigation tree diff --git a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj index 3d424d079a..d999493e80 100644 --- a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -101,7 +101,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E27D0E5E284F3FB600F43247 = { CreatedOnToolsVersion = 14.0; @@ -158,6 +158,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -190,6 +191,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -219,6 +221,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -251,6 +254,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -373,7 +377,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/xcshareddata/xcschemes/SystemNotifications.xcscheme b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/xcshareddata/xcschemes/SystemNotifications.xcscheme new file mode 100644 index 0000000000..909d5afeb4 --- /dev/null +++ b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/xcshareddata/xcschemes/SystemNotifications.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/ContentView.swift b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/ContentView.swift index bd55e9fc8f..357f3a367c 100644 --- a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/ContentView.swift +++ b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/ContentView.swift @@ -4,11 +4,11 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI struct ContentView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @EnvironmentObject var midiHelper: MIDIHelper var body: some View { diff --git a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/MIDIHelper.swift b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/MIDIHelper.swift index 5b525282e6..de7d05d85e 100644 --- a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/MIDIHelper.swift +++ b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/MIDIHelper.swift @@ -4,18 +4,18 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI /// Receiving MIDI happens as an asynchronous background callback. That means it cannot update /// SwiftUI view state directly. Therefore, we need a helper class that conforms to /// `ObservableObject` which contains `@Published` properties that SwiftUI can use to update views. final class MIDIHelper: ObservableObject { - private weak var midiManager: MIDIManager? + private weak var midiManager: ObservableMIDIManager? public init() { } - public func setup(midiManager: MIDIManager) { + public func setup(midiManager: ObservableMIDIManager) { self.midiManager = midiManager midiManager.notificationHandler = { notification, manager in diff --git a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/SystemNotificationsApp.swift b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/SystemNotificationsApp.swift index eed0bf1f19..c06b5b656c 100644 --- a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/SystemNotificationsApp.swift +++ b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/SystemNotificationsApp.swift @@ -4,18 +4,18 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI @main struct SystemNotificationsApp: App { - let midiManager = MIDIManager( + @ObservedObject var midiManager = ObservableMIDIManager( clientName: "TestAppMIDIManager", model: "TestApp", manufacturer: "MyCompany" ) - let midiHelper = MIDIHelper() + @ObservedObject var midiHelper = MIDIHelper() init() { midiHelper.setup(midiManager: midiManager) diff --git a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj index c9df1607c2..c8a729eba7 100644 --- a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -373,7 +373,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/xcshareddata/xcschemes/VirtualInput.xcscheme b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/xcshareddata/xcschemes/VirtualInput.xcscheme new file mode 100644 index 0000000000..4a044b79d1 --- /dev/null +++ b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/xcshareddata/xcschemes/VirtualInput.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput/MIDIHelper.swift b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput/MIDIHelper.swift index 2d48498f43..0eefae0005 100644 --- a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput/MIDIHelper.swift +++ b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput/MIDIHelper.swift @@ -4,18 +4,18 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI /// Receiving MIDI happens as an asynchronous background callback. That means it cannot update /// SwiftUI view state directly. Therefore, we need a helper class that conforms to /// `ObservableObject` which contains `@Published` properties that SwiftUI can use to update views. final class MIDIHelper: ObservableObject { - private weak var midiManager: MIDIManager? + private weak var midiManager: ObservableMIDIManager? public init() { } - public func setup(midiManager: MIDIManager) { + public func setup(midiManager: ObservableMIDIManager) { self.midiManager = midiManager do { diff --git a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput/VirtualInputApp.swift b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput/VirtualInputApp.swift index 33514f534e..02312d81ec 100644 --- a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput/VirtualInputApp.swift +++ b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput/VirtualInputApp.swift @@ -4,18 +4,18 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI @main struct VirtualInputApp: App { - let midiManager = MIDIManager( + @ObservedObject var midiManager = ObservableMIDIManager( clientName: "TestAppMIDIManager", model: "TestApp", manufacturer: "MyCompany" ) - let midiHelper = MIDIHelper() + @ObservedObject var midiHelper = MIDIHelper() init() { midiHelper.setup(midiManager: midiManager) diff --git a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj index f8aa010e5a..84f49f370b 100644 --- a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -101,7 +101,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E27D0E5E284F3FB600F43247 = { CreatedOnToolsVersion = 14.0; @@ -158,6 +158,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -190,6 +191,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -219,6 +221,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -251,6 +254,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -371,7 +375,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/xcshareddata/xcschemes/VirtualOutput.xcscheme b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/xcshareddata/xcschemes/VirtualOutput.xcscheme new file mode 100644 index 0000000000..ad4dc2d299 --- /dev/null +++ b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/xcshareddata/xcschemes/VirtualOutput.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput/ContentView.swift b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput/ContentView.swift index 2687e451ef..340caefff4 100644 --- a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput/ContentView.swift +++ b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput/ContentView.swift @@ -4,11 +4,11 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI struct ContentView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @EnvironmentObject var midiHelper: MIDIHelper var body: some View { diff --git a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput/MIDIHelper.swift b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput/MIDIHelper.swift index c61511a6e0..cfb94679bf 100644 --- a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput/MIDIHelper.swift +++ b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput/MIDIHelper.swift @@ -4,18 +4,18 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI /// Receiving MIDI happens as an asynchronous background callback. That means it cannot update /// SwiftUI view state directly. Therefore, we need a helper class that conforms to /// `ObservableObject` which contains `@Published` properties that SwiftUI can use to update views. final class MIDIHelper: ObservableObject { - private weak var midiManager: MIDIManager? + private weak var midiManager: ObservableMIDIManager? public init() { } - public func setup(midiManager: MIDIManager) { + public func setup(midiManager: ObservableMIDIManager) { self.midiManager = midiManager do { diff --git a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput/VirtualOutputApp.swift b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput/VirtualOutputApp.swift index c467b654dc..33497e3639 100644 --- a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput/VirtualOutputApp.swift +++ b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput/VirtualOutputApp.swift @@ -4,18 +4,18 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI @main struct VirtualOutputApp: App { - let midiManager = MIDIManager( + @ObservedObject var midiManager = ObservableMIDIManager( clientName: "TestAppMIDIManager", model: "TestApp", manufacturer: "MyCompany" ) - let midiHelper = MIDIHelper() + @ObservedObject var midiHelper = MIDIHelper() init() { midiHelper.setup(midiManager: midiManager) diff --git a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj index b01d8891ab..4fe3f8fdfe 100644 --- a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -61,10 +61,10 @@ isa = PBXGroup; children = ( E27D0E62284F3FB600F43247 /* BluetoothMIDIApp.swift */, + E2908A682A039E970072F300 /* MIDIHelper.swift */, E27D0E64284F3FB600F43247 /* ContentView.swift */, E2B5EE5A284F478700E120DC /* BluetoothMIDIView.swift */, E2E1FCC628F6636A00B4351E /* BluetoothMIDIPeripheralView.swift */, - E2908A682A039E970072F300 /* MIDIHelper.swift */, E29FF28A2880B730005E2BC2 /* Images.xcassets */, E27D0E70284F402C00F43247 /* Info.plist */, ); @@ -102,7 +102,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E27D0E5E284F3FB600F43247 = { CreatedOnToolsVersion = 14.0; @@ -161,6 +161,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -193,6 +194,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -221,6 +223,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -253,6 +256,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -364,7 +368,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/xcshareddata/xcschemes/BluetoothMIDI.xcscheme b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/xcshareddata/xcschemes/BluetoothMIDI.xcscheme new file mode 100644 index 0000000000..2e494b1836 --- /dev/null +++ b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/xcshareddata/xcschemes/BluetoothMIDI.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/BluetoothMIDIApp.swift b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/BluetoothMIDIApp.swift index 48c06d1274..008096b345 100644 --- a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/BluetoothMIDIApp.swift +++ b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/BluetoothMIDIApp.swift @@ -4,18 +4,18 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI @main struct BluetoothMIDIApp: App { - let midiManager = MIDIManager( + @ObservedObject var midiManager = ObservableMIDIManager( clientName: "TestAppMIDIManager", model: "TestApp", manufacturer: "MyCompany" ) - let midiHelper = MIDIHelper() + @ObservedObject var midiHelper = MIDIHelper() init() { midiHelper.setup(midiManager: midiManager) diff --git a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/ContentView.swift b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/ContentView.swift index 16fd89e206..35dae9faed 100644 --- a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/ContentView.swift +++ b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/ContentView.swift @@ -4,11 +4,11 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI struct ContentView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @EnvironmentObject var midiHelper: MIDIHelper var body: some View { diff --git a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/MIDIHelper.swift b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/MIDIHelper.swift index 2e5095977b..8a08c2b726 100644 --- a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/MIDIHelper.swift +++ b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/MIDIHelper.swift @@ -4,18 +4,18 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI /// Receiving MIDI happens as an asynchronous background callback. That means it cannot update /// SwiftUI view state directly. Therefore, we need a helper class that conforms to /// `ObservableObject` which contains `@Published` properties that SwiftUI can use to update views. final class MIDIHelper: ObservableObject { - private weak var midiManager: MIDIManager? + private weak var midiManager: ObservableMIDIManager? public init() { } - public func setup(midiManager: MIDIManager) { + public func setup(midiManager: ObservableMIDIManager) { self.midiManager = midiManager do { diff --git a/Examples/SwiftUI iOS/BluetoothMIDI/README.md b/Examples/SwiftUI iOS/BluetoothMIDI/README.md index 0d1ca74df5..f9633b5352 100644 --- a/Examples/SwiftUI iOS/BluetoothMIDI/README.md +++ b/Examples/SwiftUI iOS/BluetoothMIDI/README.md @@ -19,8 +19,8 @@ Events received from all MIDI output endpoints are automatically logged to the c Once Bluetooth connectivity is implemented (see examples above), Bluetooth MIDI devices' ports simply show up as MIDI input or output endpoints in the system. Access them by getting these properties on your `MIDIManager` instance: -- `midiManager.endpoints.inputs` -- `midiManager.endpoints.outputs` +- `midiManager.observableEndpoints.inputs` +- `midiManager.observableEndpoints.outputs` ## Troubleshooting diff --git a/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac.xcodeproj/project.pbxproj b/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac.xcodeproj/project.pbxproj index 6b8779760c..51b954d456 100644 --- a/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -99,7 +99,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E27D0E5E284F3FB600F43247 = { CreatedOnToolsVersion = 14.0; @@ -156,6 +156,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -188,6 +189,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -216,6 +218,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -248,6 +251,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -357,7 +361,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac.xcodeproj/xcshareddata/xcschemes/USB iOS to Mac.xcscheme b/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac.xcodeproj/xcshareddata/xcschemes/USB iOS to Mac.xcscheme index ee692327b7..82b7da13ca 100644 --- a/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac.xcodeproj/xcshareddata/xcschemes/USB iOS to Mac.xcscheme +++ b/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac.xcodeproj/xcshareddata/xcschemes/USB iOS to Mac.xcscheme @@ -1,6 +1,6 @@ + + + + - - UIBackgroundModes - - + diff --git a/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac/MIDIHelper.swift b/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac/MIDIHelper.swift index f87d36678c..1b07085b24 100644 --- a/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac/MIDIHelper.swift +++ b/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac/MIDIHelper.swift @@ -4,18 +4,18 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI /// Receiving MIDI happens as an asynchronous background callback. That means it cannot update /// SwiftUI view state directly. Therefore, we need a helper class that conforms to /// `ObservableObject` which contains `@Published` properties that SwiftUI can use to update views. final class MIDIHelper: ObservableObject { - private weak var midiManager: MIDIManager? + private weak var midiManager: ObservableMIDIManager? public init() { } - public func setup(midiManager: MIDIManager) { + public func setup(midiManager: ObservableMIDIManager) { self.midiManager = midiManager do { diff --git a/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac/USBiOStoMacApp.swift b/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac/USBiOStoMacApp.swift index daaf1dacb1..a857cb4c49 100644 --- a/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac/USBiOStoMacApp.swift +++ b/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac/USBiOStoMacApp.swift @@ -4,18 +4,18 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI @main struct USBiOStoMacApp: App { - let midiManager = MIDIManager( + @ObservedObject var midiManager = ObservableMIDIManager( clientName: "TestAppMIDIManager", model: "TestApp", manufacturer: "MyCompany" ) - let midiHelper = MIDIHelper() + @ObservedObject var midiHelper = MIDIHelper() init() { midiHelper.setup(midiManager: midiManager) diff --git a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj index 76192483d8..0e405e28a2 100644 --- a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj +++ b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj @@ -3,17 +3,17 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ + E225F84C2AFE27C900E3858A /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = E225F84B2AFE27C900E3858A /* MIDIKit */; }; E27D0E1E284F2A8900F43247 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27D0E1D284F2A8900F43247 /* AppDelegate.swift */; }; E27D0E22284F2A8900F43247 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27D0E21284F2A8900F43247 /* ViewController.swift */; }; E27D0E25284F2A8900F43247 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E27D0E23284F2A8900F43247 /* Main.storyboard */; }; E27D0E2A284F2A8900F43247 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E27D0E28284F2A8900F43247 /* LaunchScreen.storyboard */; }; E29FF2952880BBDB005E2BC2 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E29FF2942880BBDB005E2BC2 /* Images.xcassets */; }; E2B5EE5D284F4C6F00E120DC /* BTMIDICentralViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5EE5C284F4C6F00E120DC /* BTMIDICentralViewController.swift */; }; - E2C137BE289DF0D60043AF3D /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = E2C137BD289DF0D60043AF3D /* MIDIKit */; }; E2E1FCC928F67B6400B4351E /* BTMIDIPeripheralViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E1FCC828F67B6400B4351E /* BTMIDIPeripheralViewController.swift */; }; /* End PBXBuildFile section */ @@ -35,7 +35,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E2C137BE289DF0D60043AF3D /* MIDIKit in Frameworks */, + E225F84C2AFE27C900E3858A /* MIDIKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -91,7 +91,7 @@ ); name = BluetoothMIDI; packageProductDependencies = ( - E2C137BD289DF0D60043AF3D /* MIDIKit */, + E225F84B2AFE27C900E3858A /* MIDIKit */, ); productName = BluetoothMIDI; productReference = E27D0E1A284F2A8900F43247 /* BluetoothMIDI.app */; @@ -105,7 +105,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E27D0E19284F2A8900F43247 = { CreatedOnToolsVersion = 14.0; @@ -184,6 +184,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -216,6 +217,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -230,7 +232,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -244,6 +246,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -276,6 +279,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -284,7 +288,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -310,8 +314,7 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen.storyboard; INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -344,8 +347,7 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen.storyboard; INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -391,13 +393,13 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - E2C137BD289DF0D60043AF3D /* MIDIKit */ = { + E225F84B2AFE27C900E3858A /* MIDIKit */ = { isa = XCSwiftPackageProductDependency; package = E2C137BC289DF0D60043AF3D /* XCRemoteSwiftPackageReference "MIDIKit" */; productName = MIDIKit; diff --git a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/xcshareddata/xcschemes/BluetoothMIDI.xcscheme b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/xcshareddata/xcschemes/BluetoothMIDI.xcscheme new file mode 100644 index 0000000000..1a6c2cfec5 --- /dev/null +++ b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/xcshareddata/xcschemes/BluetoothMIDI.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/AppDelegate.swift b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/AppDelegate.swift index dd835defe3..83e9bfef27 100644 --- a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/AppDelegate.swift +++ b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/AppDelegate.swift @@ -4,7 +4,7 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import UIKit @main diff --git a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/Info.plist b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/Info.plist index a114855431..97ff79dd99 100644 --- a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/Info.plist +++ b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/Info.plist @@ -7,9 +7,5 @@ UIApplicationSupportsMultipleScenes - UIBackgroundModes - - audio - diff --git a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/ViewController.swift b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/ViewController.swift index 5f4b8fd120..549c447761 100644 --- a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/ViewController.swift +++ b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/ViewController.swift @@ -4,7 +4,7 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import UIKit class ViewController: UIViewController { diff --git a/Examples/UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj b/Examples/UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj index 6e0561ee20..c95da5773e 100644 --- a/Examples/UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj +++ b/Examples/UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -102,7 +102,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E27D0E19284F2A8900F43247 = { CreatedOnToolsVersion = 14.0; @@ -179,6 +179,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -211,6 +212,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -225,7 +227,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -239,6 +241,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -271,6 +274,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -279,7 +283,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -376,7 +380,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; E2D7FF1D29754A1C003212AF /* XCRemoteSwiftPackageReference "SwiftRadix" */ = { diff --git a/Examples/UIKit/EventParsing/EventParsing.xcodeproj/xcshareddata/xcschemes/EventParsing.xcscheme b/Examples/UIKit/EventParsing/EventParsing.xcodeproj/xcshareddata/xcschemes/EventParsing.xcscheme new file mode 100644 index 0000000000..1fc8bebb9e --- /dev/null +++ b/Examples/UIKit/EventParsing/EventParsing.xcodeproj/xcshareddata/xcschemes/EventParsing.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/UIKit/EventParsing/EventParsing/AppDelegate.swift b/Examples/UIKit/EventParsing/EventParsing/AppDelegate.swift index 26ceffb365..4bfcb1614e 100644 --- a/Examples/UIKit/EventParsing/EventParsing/AppDelegate.swift +++ b/Examples/UIKit/EventParsing/EventParsing/AppDelegate.swift @@ -4,14 +4,12 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftRadix import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - let midiManager = MIDIManager( clientName: "TestAppMIDIManager", model: "TestApp", @@ -20,6 +18,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let virtualInputName = "TestApp Input" + var window: UIWindow? + func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -30,7 +30,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } catch { print("Error starting MIDI services:", error.localizedDescription) } - + do { print("Creating virtual MIDI input.") try midiManager.addInput( @@ -44,10 +44,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } catch { print("Error creating virtual MIDI input:", error.localizedDescription) } - + return true } - +} + +extension AppDelegate { private func handleMIDI(event: MIDIEvent) { switch event { case let .noteOn(payload): diff --git a/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj b/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj index 2f4afd9a6b..6c94aad4fa 100644 --- a/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj +++ b/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -99,7 +99,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E27D0E19284F2A8900F43247 = { CreatedOnToolsVersion = 14.0; @@ -175,6 +175,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -207,6 +208,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -221,7 +223,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -235,6 +237,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -267,6 +270,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -275,7 +279,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -300,8 +304,7 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen.storyboard; INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -330,8 +333,7 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen.storyboard; INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -374,7 +376,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/xcshareddata/xcschemes/VirtualInput.xcscheme b/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/xcshareddata/xcschemes/VirtualInput.xcscheme new file mode 100644 index 0000000000..5049c97ef5 --- /dev/null +++ b/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/xcshareddata/xcschemes/VirtualInput.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/UIKit/VirtualInput/VirtualInput/AppDelegate.swift b/Examples/UIKit/VirtualInput/VirtualInput/AppDelegate.swift index 12fc70726b..8eadc00f9f 100644 --- a/Examples/UIKit/VirtualInput/VirtualInput/AppDelegate.swift +++ b/Examples/UIKit/VirtualInput/VirtualInput/AppDelegate.swift @@ -4,13 +4,11 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - let midiManager = MIDIManager( clientName: "TestAppMIDIManager", model: "TestApp", @@ -19,6 +17,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let virtualInputName = "TestApp Input" + var window: UIWindow? + func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? diff --git a/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj b/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj index 459d2d1473..3d1ae9bdd2 100644 --- a/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj +++ b/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -99,7 +99,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E27D0E19284F2A8900F43247 = { CreatedOnToolsVersion = 14.0; @@ -175,6 +175,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -207,6 +208,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -221,7 +223,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -235,6 +237,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -267,6 +270,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -275,7 +279,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -372,7 +376,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/xcshareddata/xcschemes/VirtualOutput.xcscheme b/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/xcshareddata/xcschemes/VirtualOutput.xcscheme new file mode 100644 index 0000000000..1f07444fc6 --- /dev/null +++ b/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/xcshareddata/xcschemes/VirtualOutput.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/UIKit/VirtualOutput/VirtualOutput/AppDelegate.swift b/Examples/UIKit/VirtualOutput/VirtualOutput/AppDelegate.swift index 24f5f260f6..725be1b182 100644 --- a/Examples/UIKit/VirtualOutput/VirtualOutput/AppDelegate.swift +++ b/Examples/UIKit/VirtualOutput/VirtualOutput/AppDelegate.swift @@ -4,13 +4,11 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - let midiManager = MIDIManager( clientName: "TestAppMIDIManager", model: "TestApp", @@ -19,6 +17,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let virtualOutputName = "TestApp Output" + var window: UIWindow? + func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -29,7 +29,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } catch { print("Error starting MIDI services:", error.localizedDescription) } - + do { print("Creating virtual MIDI output.") try midiManager.addOutput( @@ -40,10 +40,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } catch { print("Error creating virtual MIDI output:", error.localizedDescription) } - + return true } - +} + +extension AppDelegate { /// Convenience accessor for created virtual MIDI Output. var virtualOutput: MIDIOutput? { midiManager.managedOutputs[virtualOutputName] diff --git a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Combine-and-SwiftUI-Features.md b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Combine-and-SwiftUI-Features.md index f52d003cb1..00de61401d 100644 --- a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Combine-and-SwiftUI-Features.md +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Combine-and-SwiftUI-Features.md @@ -2,10 +2,9 @@ Certain objects and properties are observable. -``MIDIManager`` contains several instance properties that are observable in a Combine or SwiftUI context. This can be useful in updating user interface displaying a list of MIDI endpoints in real-time as endpoints are added or removed from the system, for example. +``ObservableMIDIManager`` is a ``MIDIManager`` subclass that contains several instance properties that are observable in a Combine or SwiftUI context. This can be useful in updating user interface displaying a list of MIDI endpoints in real-time as endpoints are added or removed from the system, for example. -- ``MIDIManager/devices``.``MIDIDevices/devices`` -- ``MIDIManager/endpoints``.``MIDIEndpoints/inputs`` -- ``MIDIManager/endpoints``.``MIDIEndpoints/outputs`` +- ``ObservableMIDIManager/observableDevices`` +- ``ObservableMIDIManager/observableEndpoints`` Where use of Combine is not possible, notifications of changes can be received by storing a handler closure in ``MIDIManager/notificationHandler`` where you might then update user interface to reflect the new collection of endpoints. diff --git a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Devices.md b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Devices.md index a0329e8f92..42b97887d9 100644 --- a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Devices.md +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Devices.md @@ -25,6 +25,7 @@ In most use cases, it is not necessary work with devices and entities. A single ### Devices in the System - ``MIDIManager/devices`` +- ``ObservableMIDIManager/observableDevices`` ### Device and Entity Objects diff --git a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Endpoints.md b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Endpoints.md index fbabd3c956..e22f66a96f 100644 --- a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Endpoints.md +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Endpoints.md @@ -7,6 +7,7 @@ Endpoints represent both virtual and physical MIDI inputs (destinations) and out ### Endpoints in the System - ``MIDIManager/endpoints`` +- ``ObservableMIDIManager/observableEndpoints`` ### Endpoint Identification @@ -26,3 +27,5 @@ Endpoints represent both virtual and physical MIDI inputs (destinations) and out ### Endpoint Filtering - ``MIDIEndpointFilter`` +- ``MIDIEndpointFilterMask`` +- ``MIDIEndpointMaskedFilter`` diff --git a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Receiving-MIDI-Events.md b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Receiving-MIDI-Events.md index 422986246b..f2df145575 100644 --- a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Receiving-MIDI-Events.md +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Receiving-MIDI-Events.md @@ -12,6 +12,7 @@ In order to begin receiving MIDI events, there are two primary mechanisms: ### Receive Handlers - ``MIDIReceiver`` +- ``MIDIReceiverOptions`` ### Protocols diff --git a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO.md b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO.md index 7f49c6cd93..38636c026f 100644 --- a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO.md +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO.md @@ -19,6 +19,7 @@ To add additional functionality, import extension modules or import the MIDIKit - - - +- ``ObservableMIDIManager`` - ### Devices & Entities diff --git a/Sources/MIDIKit/MIDIKit.docc/MIDIKitUI/MIDIKitUI-Internals.md b/Sources/MIDIKit/MIDIKit.docc/MIDIKitUI/MIDIKitUI-Internals.md new file mode 100644 index 0000000000..8331bfcdad --- /dev/null +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitUI/MIDIKitUI-Internals.md @@ -0,0 +1,9 @@ +# Internals + +## Topics + +### Protocols + +- ``MIDIEndpointsSelectable`` +- ``MIDIInputsSelectable`` +- ``MIDIOutputsSelectable`` diff --git a/Sources/MIDIKit/MIDIKit.docc/MIDIKitUI/MIDIKitUI.md b/Sources/MIDIKit/MIDIKit.docc/MIDIKitUI/MIDIKitUI.md index f9cbed1704..3b9d98abe2 100644 --- a/Sources/MIDIKit/MIDIKit.docc/MIDIKitUI/MIDIKitUI.md +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitUI/MIDIKitUI.md @@ -16,3 +16,5 @@ MIDIKitUI adds convenient reusable user interface controls to simplify building - ``MIDIOutputsPicker`` ### Internals + +- \ No newline at end of file diff --git a/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.4.swift b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.4.swift new file mode 100644 index 0000000000..dc5cff49e2 --- /dev/null +++ b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.4.swift @@ -0,0 +1,30 @@ +// +// MIDIKit-0.9.4.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +#if !os(tvOS) && !os(watchOS) + +extension Collection where Element: MIDIEndpoint { + @available( + *, + deprecated, + renamed: "filter(_:_:in:)", + message: "This method has been refactored." + ) + public func filter( + using endpointFilter: MIDIEndpointFilter, + in manager: MIDIManager, + isIncluded: Bool = true + ) -> [Element] { + switch isIncluded { + case true: + return filter(endpointFilter, in: manager) + case false: + return filter(dropping: endpointFilter, in: manager) + } + } +} + +#endif diff --git a/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift b/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift index a3f32c0c7c..885b4755ed 100644 --- a/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift +++ b/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift @@ -8,10 +8,11 @@ import Foundation -// TODO: this protocol may not be necessary -// it was experimental so that the `MIDIManager.devices` property could be swapped out with -// a different devices class with Combine support -public protocol MIDIDevicesProtocol { +#if canImport(Combine) +import Combine +#endif + +public protocol MIDIDevicesProtocol where Self: Equatable, Self: Hashable { /// List of MIDI devices in the system. /// /// A device can contain zero or more entities, and an entity can contain zero or more inputs @@ -21,7 +22,19 @@ public protocol MIDIDevicesProtocol { /// Manually update the locally cached contents from the system. /// This method does not need to be manually invoked, as it is handled internally when MIDI /// system endpoints change. - func updateCachedProperties() + mutating func updateCachedProperties() +} + +extension MIDIDevicesProtocol /* : Equatable */ { + public static func == (lhs: any MIDIDevicesProtocol, rhs: any MIDIDevicesProtocol) -> Bool { + lhs.devices == rhs.devices + } +} + +extension MIDIDevicesProtocol /* : Hashable */ { + public func hash(into hasher: inout Hasher) { + hasher.combine(devices) + } } extension MIDIDevicesProtocol { @@ -52,18 +65,14 @@ extension MIDIDevicesProtocol { /// /// Do not instance this class directly. Instead, access the ``MIDIManager/devices`` property of /// your central ``MIDIManager`` instance. -public final class MIDIDevices: NSObject, MIDIDevicesProtocol { - public internal(set) dynamic var devices: [MIDIDevice] = [] - - override init() { - super.init() - } +public struct MIDIDevices: MIDIDevicesProtocol { + public internal(set) var devices: [MIDIDevice] = [] /// Manually update the locally cached contents from the system. /// /// It is not necessary to call this method as the ``MIDIManager`` will automate updating device /// cache. - public func updateCachedProperties() { + public mutating func updateCachedProperties() { devices = getSystemDevices() } } diff --git a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpointFilter.swift b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpointFilter.swift index 3f17e88a02..d287b868e2 100644 --- a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpointFilter.swift +++ b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpointFilter.swift @@ -60,6 +60,35 @@ public struct MIDIEndpointFilter: Equatable, Hashable { } } +extension MIDIEndpointFilter { + /// Process a collection of MIDI endpoints events using this filter. + /// + /// Alternatively, a `Collection` category method is available: + /// + /// ```swift + /// let endpoints: [MIDIInputEndpoint] = [ ... ] + /// let filter = MIDIEndpointFilter( ... ) + /// + /// // filter only matches + /// let filtered = endpoints.filter(filter, in: midiManager) + /// + /// // filter by dropping matches + /// let filtered = endpoints.filter(dropping: filter, in: midiManager) + /// ``` + /// + /// - Parameters: + /// - endpoints: Collection of endpoints to filter. + /// - mask: Filter behavior. + /// - manager: Reference to the MIDI manager. + public func apply( + to endpoints: some Collection, + mask: MIDIEndpointFilterMask, + in manager: MIDIManager + ) -> [Element] { + endpoints.filter(mask, self, in: manager) + } +} + extension MIDIEndpointFilter: Sendable { } extension MIDIEndpointFilter { @@ -77,25 +106,169 @@ extension MIDIEndpointFilter { } } +// MARK: - Filter Mask + +public enum MIDIEndpointFilterMask: Equatable, Hashable { + /// Filter by keeping only endpoints that match the filter. + case only + + /// Filter by dropping endpoints that match the filter and retaining all others. + case drop +} + +extension MIDIEndpointFilterMask: Sendable { } + +public enum MIDIEndpointMaskedFilter: Equatable, Hashable { + /// Filter by keeping only endpoints that match the filter. + case only(MIDIEndpointFilter) + + /// Filter by dropping endpoints that match the filter and retaining all others. + case drop(MIDIEndpointFilter) +} + +extension MIDIEndpointMaskedFilter: Sendable { } + // MARK: - Collection Methods extension Collection where Element: MIDIEndpoint { + // MARK: - Filter Mask Methods + + /// Return a new endpoint collection filtered by the given criteria. + /// + /// - Parameters: + /// - mask: Filter behavior. + /// - endpointFilter: Filter to use. + /// - manager: Reference to the MIDI manager. + public func filter( + _ mask: MIDIEndpointFilterMask, + _ endpointFilter: MIDIEndpointFilter, + in manager: MIDIManager + ) -> [Element] { + switch mask { + case .only: + return filter(endpointFilter, in: manager) + case .drop: + return filter(dropping: endpointFilter, in: manager) + } + } + + /// Return a new endpoint collection filtered by the given criteria. + /// + /// - Parameters: + /// - maskedFilter: Masked filter to use. + /// - manager: Reference to the MIDI manager. + public func filter( + _ maskedFilter: MIDIEndpointMaskedFilter, + in manager: MIDIManager + ) -> [Element] { + switch maskedFilter { + case let .only(endpointFilter): + return filter(endpointFilter, in: manager) + case let .drop(endpointFilter): + return filter(dropping: endpointFilter, in: manager) + } + } + + // MARK: - Filter Methods + + /// Return a new endpoint collection filtered by keeping only endpoints matching the given + /// criteria. public func filter( - using endpointFilter: MIDIEndpointFilter, - in manager: MIDIManager, - isIncluded: Bool = true + _ endpointFilter: MIDIEndpointFilter, + in manager: MIDIManager + ) -> [Element] { + filter( + endpointFilter, + ownedInputs: Array(manager.managedInputs.values), + ownedOutputs: Array(manager.managedOutputs.values) + ) + } + + /// Return a new endpoint collection filtered by keeping only endpoints matching the given + /// criteria. + func filter( + _ endpointFilter: MIDIEndpointFilter, + ownedInputs: [MIDIInput], + ownedOutputs: [MIDIOutput] + ) -> [Element] { + filter( + endpointFilter, + ownedInputEndpoints: ownedInputs.map(\.endpoint), + ownedOutputEndpoints: ownedOutputs.map(\.endpoint) + ) + } + + /// Return a new endpoint collection filtered by keeping only endpoints matching the given + /// criteria. + func filter( + _ endpointFilter: MIDIEndpointFilter, + ownedInputEndpoints: [MIDIInputEndpoint], + ownedOutputEndpoints: [MIDIOutputEndpoint] ) -> [Element] { filter { endpoint in if !endpointFilter.criteria.isEmpty { guard endpointFilter.criteria - .allSatisfy({ $0.matches(endpoint: endpoint) }) != isIncluded + .contains(where: { $0.matches(endpoint: endpoint) }) + else { return false } + } + + if endpointFilter.owned { + let inputs = ownedInputEndpoints.asAnyEndpoints() + let outputs = ownedOutputEndpoints.asAnyEndpoints() + guard (inputs + outputs) + .contains(endpoint.asAnyEndpoint()) + else { return false } + } + return true + } + } + + /// Return a new endpoint collection filtered by excluding endpoints matching the given + /// criteria. + public func filter( + dropping endpointFilter: MIDIEndpointFilter, + in manager: MIDIManager + ) -> [Element] { + filter( + dropping: endpointFilter, + ownedInputs: Array(manager.managedInputs.values), + ownedOutputs: Array(manager.managedOutputs.values) + ) + } + + /// Return a new endpoint collection filtered by excluding endpoints matching the given + /// criteria. + func filter( + dropping endpointFilter: MIDIEndpointFilter, + ownedInputs: [MIDIInput], + ownedOutputs: [MIDIOutput] + ) -> [Element] { + filter( + dropping: endpointFilter, + ownedInputEndpoints: ownedInputs.map(\.endpoint), + ownedOutputEndpoints: ownedOutputs.map(\.endpoint) + ) + } + + /// Return a new endpoint collection filtered by excluding endpoints matching the given + /// criteria. + func filter( + dropping endpointFilter: MIDIEndpointFilter, + ownedInputEndpoints: [MIDIInputEndpoint], + ownedOutputEndpoints: [MIDIOutputEndpoint] + ) -> [Element] { + filter { endpoint in + if !endpointFilter.criteria.isEmpty { + guard !endpointFilter.criteria + .contains(where: { $0.matches(endpoint: endpoint) }) else { return false } } if endpointFilter.owned { - let inputs = manager.managedInputs.map(\.value.endpoint).asAnyEndpoints() - let outputs = manager.managedOutputs.map(\.value.endpoint).asAnyEndpoints() - guard (inputs + outputs).contains(endpoint.asAnyEndpoint()) != isIncluded + let inputs = ownedInputEndpoints.asAnyEndpoints() + let outputs = ownedOutputEndpoints.asAnyEndpoints() + guard !(inputs + outputs) + .contains(endpoint.asAnyEndpoint()) else { return false } } return true diff --git a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift index f7e718d935..461576d843 100644 --- a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift +++ b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift @@ -8,9 +8,11 @@ import Foundation -// this protocol may not be necessary, it was experimental so that the `MIDIManager.endpoints` -// property could be swapped out with a different Endpoints class with Combine support -public protocol MIDIEndpointsProtocol { +#if canImport(Combine) +import Combine +#endif + +public protocol MIDIEndpointsProtocol where Self: Equatable, Self: Hashable { /// List of MIDI input endpoints in the system. var inputs: [MIDIInputEndpoint] { get } @@ -31,52 +33,94 @@ public protocol MIDIEndpointsProtocol { mutating func updateCachedProperties() } -/// Manages system MIDI endpoints information cache. -public final class MIDIEndpoints: NSObject, MIDIEndpointsProtocol { - /// Weak reference to ``MIDIManager``. - weak var manager: MIDIManager? - - public internal(set) dynamic var inputs: [MIDIInputEndpoint] = [] - public internal(set) dynamic var inputsUnowned: [MIDIInputEndpoint] = [] - - public internal(set) dynamic var outputs: [MIDIOutputEndpoint] = [] - public internal(set) dynamic var outputsUnowned: [MIDIOutputEndpoint] = [] - - override init() { - super.init() +extension MIDIEndpointsProtocol /* : Equatable */ { + public static func == (lhs: any MIDIEndpointsProtocol, rhs: any MIDIEndpointsProtocol) -> Bool { + lhs.inputs == rhs.inputs && + lhs.inputsUnowned == rhs.inputsUnowned && + lhs.outputs == rhs.outputs && + lhs.outputsUnowned == rhs.outputsUnowned } - - init(manager: MIDIManager) { - self.manager = manager - super.init() +} + +extension MIDIEndpointsProtocol /* : Hashable */ { + public func hash(into hasher: inout Hasher) { + hasher.combine(inputs) + hasher.combine(inputsUnowned) + hasher.combine(outputs) + hasher.combine(outputsUnowned) } - - public func updateCachedProperties() { - inputs = getSystemDestinationEndpoints() - +} + +extension MIDIEndpointsProtocol { + internal func _fetchProperties(manager: MIDIManager?) -> ( + inputs: [MIDIInputEndpoint], + inputsUnowned: [MIDIInputEndpoint], + outputs: [MIDIOutputEndpoint], + outputsUnowned: [MIDIOutputEndpoint] + ) { + let inputs = getSystemDestinationEndpoints() + + let inputsUnowned: [MIDIInputEndpoint] if let manager { let managedInputsIDs = manager.managedInputs.values .compactMap { $0.uniqueID } - + inputsUnowned = inputs.filter { !managedInputsIDs.contains($0.uniqueID) } } else { inputsUnowned = inputs } - - outputs = getSystemSourceEndpoints() - + + let outputs = getSystemSourceEndpoints() + + let outputsUnowned: [MIDIOutputEndpoint] if let manager { let managedOutputsIDs = manager.managedOutputs.values .compactMap { $0.uniqueID } - + outputsUnowned = outputs.filter { !managedOutputsIDs.contains($0.uniqueID) } } else { outputsUnowned = outputs } + + return ( + inputs: inputs, + inputsUnowned: inputsUnowned, + outputs: outputs, + outputsUnowned: outputsUnowned + ) + } +} + +/// Manages system MIDI endpoints information cache. +public struct MIDIEndpoints: MIDIEndpointsProtocol { + /// Weak reference to ``MIDIManager``. + weak var manager: MIDIManager? + + public internal(set) var inputs: [MIDIInputEndpoint] = [] + public internal(set) var inputsUnowned: [MIDIInputEndpoint] = [] + + public internal(set) var outputs: [MIDIOutputEndpoint] = [] + public internal(set) var outputsUnowned: [MIDIOutputEndpoint] = [] + + init(manager: MIDIManager?) { + self.manager = manager + } + + /// Manually update the locally cached contents from the system. + /// + /// It is not necessary to call this method as the ``MIDIManager`` will automate updating device + /// cache. + public mutating func updateCachedProperties() { + let fetched = _fetchProperties(manager: manager) + + inputs = fetched.inputs + inputsUnowned = fetched.inputsUnowned + outputs = fetched.outputs + outputsUnowned = fetched.outputsUnowned } } diff --git a/Sources/MIDIKitIO/MIDIIdentifier/MIDIIdentifierPersistence.swift b/Sources/MIDIKitIO/MIDIIdentifier/MIDIIdentifierPersistence.swift index d5220bf5c2..24a2211e0d 100644 --- a/Sources/MIDIKitIO/MIDIIdentifier/MIDIIdentifierPersistence.swift +++ b/Sources/MIDIKitIO/MIDIIdentifier/MIDIIdentifierPersistence.swift @@ -18,7 +18,7 @@ public enum MIDIIdentifierPersistence { /// ⚠️ This is generally not recommended and is provided mainly for testing purposes. /// /// Use ``userDefaultsManaged(key:suite:)`` where possible, or provide your own storage with - /// ``manualStorage(readHandler:storeHandler:)``. + /// ``managedStorage(readHandler:storeHandler:)``. case adHoc /// Unmanaged. diff --git a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Combine-and-SwiftUI-Features.md b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Combine-and-SwiftUI-Features.md index f52d003cb1..00de61401d 100644 --- a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Combine-and-SwiftUI-Features.md +++ b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Combine-and-SwiftUI-Features.md @@ -2,10 +2,9 @@ Certain objects and properties are observable. -``MIDIManager`` contains several instance properties that are observable in a Combine or SwiftUI context. This can be useful in updating user interface displaying a list of MIDI endpoints in real-time as endpoints are added or removed from the system, for example. +``ObservableMIDIManager`` is a ``MIDIManager`` subclass that contains several instance properties that are observable in a Combine or SwiftUI context. This can be useful in updating user interface displaying a list of MIDI endpoints in real-time as endpoints are added or removed from the system, for example. -- ``MIDIManager/devices``.``MIDIDevices/devices`` -- ``MIDIManager/endpoints``.``MIDIEndpoints/inputs`` -- ``MIDIManager/endpoints``.``MIDIEndpoints/outputs`` +- ``ObservableMIDIManager/observableDevices`` +- ``ObservableMIDIManager/observableEndpoints`` Where use of Combine is not possible, notifications of changes can be received by storing a handler closure in ``MIDIManager/notificationHandler`` where you might then update user interface to reflect the new collection of endpoints. diff --git a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Devices.md b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Devices.md index a0329e8f92..42b97887d9 100644 --- a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Devices.md +++ b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Devices.md @@ -25,6 +25,7 @@ In most use cases, it is not necessary work with devices and entities. A single ### Devices in the System - ``MIDIManager/devices`` +- ``ObservableMIDIManager/observableDevices`` ### Device and Entity Objects diff --git a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Endpoints.md b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Endpoints.md index fbabd3c956..e22f66a96f 100644 --- a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Endpoints.md +++ b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Endpoints.md @@ -7,6 +7,7 @@ Endpoints represent both virtual and physical MIDI inputs (destinations) and out ### Endpoints in the System - ``MIDIManager/endpoints`` +- ``ObservableMIDIManager/observableEndpoints`` ### Endpoint Identification @@ -26,3 +27,5 @@ Endpoints represent both virtual and physical MIDI inputs (destinations) and out ### Endpoint Filtering - ``MIDIEndpointFilter`` +- ``MIDIEndpointFilterMask`` +- ``MIDIEndpointMaskedFilter`` diff --git a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Receiving-MIDI-Events.md b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Receiving-MIDI-Events.md index 422986246b..f2df145575 100644 --- a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Receiving-MIDI-Events.md +++ b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Receiving-MIDI-Events.md @@ -12,6 +12,7 @@ In order to begin receiving MIDI events, there are two primary mechanisms: ### Receive Handlers - ``MIDIReceiver`` +- ``MIDIReceiverOptions`` ### Protocols diff --git a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO.md b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO.md index c081d361d0..251fc2012a 100644 --- a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO.md +++ b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO.md @@ -19,6 +19,7 @@ To add additional functionality, import extension modules or import the MIDIKit - - - +- ``ObservableMIDIManager`` - ### Devices & Entities diff --git a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift index a50ec010b3..ec79e59f2c 100644 --- a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift @@ -13,7 +13,12 @@ import Foundation /// /// One ``MIDIManager`` instance stored in a global lifecycle context can manage multiple MIDI ports /// and connections, and is usually sufficient for all of an application's MIDI needs. -public final class MIDIManager: NSObject { +/// +/// > Tip: +/// > +/// > For SwiftUI and Combine environments, see the ``ObservableMIDIManager`` subclass which adds +/// > published devices and endpoints properties. +public class MIDIManager: NSObject { // MARK: - Properties /// MIDI Client Name. @@ -70,22 +75,26 @@ public final class MIDIManager: NSObject { /// /// - Parameter ownerID: reverse-DNS domain that was used when the connection was first made /// - Throws: ``MIDIIOError`` - public func unmanagedPersistentThruConnections(ownerID: String) throws - -> [CoreMIDIThruConnectionRef] { + public func unmanagedPersistentThruConnections( + ownerID: String + ) throws -> [CoreMIDIThruConnectionRef] { try getSystemThruConnectionsPersistentEntries(matching: ownerID) } /// MIDI devices in the system. - public internal(set) var devices: MIDIDevicesProtocol = MIDIDevices() + public internal(set) var devices: MIDIDevices = MIDIDevices() /// MIDI input and output endpoints in the system. - public internal(set) var endpoints: MIDIEndpointsProtocol = MIDIEndpoints() + public internal(set) var endpoints: MIDIEndpoints /// Handler that is called when state has changed in the manager. - public var notificationHandler: (( + public typealias NotificationHandler = ( _ notification: MIDIIONotification, _ manager: MIDIManager - ) -> Void)? + ) -> Void + + /// Handler that is called when state has changed in the manager. + public var notificationHandler: NotificationHandler? /// Internal: system state cache for notification handling. var notificationCache: MIDIIOObjectCache? @@ -111,18 +120,15 @@ public final class MIDIManager: NSObject { clientName: String, model: String, manufacturer: String, - notificationHandler: (( - _ notification: MIDIIONotification, - _ manager: MIDIManager - ) -> Void)? = nil + notificationHandler: NotificationHandler? = nil ) { // API version preferredAPI = .bestForPlatform() - + // queue client name var clientNameForQueue = clientName.onlyAlphanumerics if clientNameForQueue.isEmpty { clientNameForQueue = UUID().uuidString } - + // manager event queue let eventQueueName = (Bundle.main.bundleIdentifier ?? "com.orchetect.midikit") + ".midiManager." + clientNameForQueue + ".events" @@ -133,17 +139,21 @@ public final class MIDIManager: NSObject { autoreleaseFrequency: .workItem, target: .global(qos: .userInitiated) ) - + // assign other properties self.clientName = clientName self.model = model self.manufacturer = manufacturer self.notificationHandler = notificationHandler - + + // endpoints + endpoints = MIDIEndpoints(manager: nil) + super.init() - - endpoints = MIDIEndpoints(manager: self) - + + // we can only add manager reference to endpoints after manager is initialized + endpoints.manager = self + addNetworkSessionObservers() } @@ -155,7 +165,7 @@ public final class MIDIManager: NSObject { // or only client owned by an app, the MIDI server may exit if there are no other // clients remaining in the system" // _ = MIDIClientDispose(coreMIDIClientRef) - + NotificationCenter.default.removeObserver(self) } } @@ -171,33 +181,10 @@ public final class MIDIManager: NSObject { } /// Internal: updates cached properties for all objects. - dynamic func updateObjectsCache() { - #if canImport(Combine) - if #available( - macOS 10.15, - macCatalyst 13, - iOS 13, - /* tvOS 13, watchOS 6, */ * - ) { - // calling this means we don't need to use @Published on local variables in order for - // Combine/SwiftUI to be notified that ObservableObject class property values have - // changed - objectWillChange.send() - } - #endif - + func updateObjectsCache() { devices.updateCachedProperties() endpoints.updateCachedProperties() } } -#if canImport(Combine) -import Combine - -@available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) -extension MIDIManager: ObservableObject { - // nothing here; just add ObservableObject conformance -} -#endif - #endif diff --git a/Sources/MIDIKitIO/MIDIManager/ObservableMIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/ObservableMIDIManager.swift new file mode 100644 index 0000000000..d40aeeff44 --- /dev/null +++ b/Sources/MIDIKitIO/MIDIManager/ObservableMIDIManager.swift @@ -0,0 +1,127 @@ +// +// ObservableMIDIManager.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +#if !os(tvOS) && !os(watchOS) + +@_implementationOnly import CoreMIDI +import Foundation + +#if canImport(Combine) +import Combine + +/// ``MIDIManager`` subclass that is observable in a SwiftUI or Combine context. +/// Two new properties are available: ``observableDevices`` and ``observableEndpoints``. +/// +/// Generally it is recommended to install the manager instance in the `App` struct. +/// +/// ```swift +/// @main +/// struct MyApp: App { +/// @ObservedObject var midiManager = ObservableMIDIManager( +/// clientName: "MyApp", +/// model: "MyApp", +/// manufacturer: "MyCompany" +/// ) +/// +/// WindowGroup { +/// ContentView() +/// .environmentObject(midiManager) +/// } +/// } +/// ``` +/// +/// The observable properties can then be used to update view state as a result of changes in the +/// system's MIDI devices and endpoints. +/// +/// ```swift +/// struct ContentView: View { +/// @EnvironmentObject var midiManager: ObservableMIDIManager +/// +/// var body: some View { +/// List(midiManager.observableDevices.devices) { device in +/// Text(device.name) +/// } +/// List(midiManager.observableEndpoints.inputs) { input in +/// Text(input.name) +/// } +/// List(midiManager.observableEndpoints.outputs) { output in +/// Text(output.name) +/// } +/// } +/// } +/// ``` +@available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) +public final class ObservableMIDIManager: MIDIManager, ObservableObject { + // MARK: - Properties + + /// MIDI devices in the system. + /// This is an observable implementation of ``MIDIManager/devices``. + @Published + public internal(set) var observableDevices = MIDIDevices() + + /// MIDI input and output endpoints in the system. + /// This is an observable implementation of ``MIDIManager/endpoints``. + @Published + public internal(set) var observableEndpoints = MIDIEndpoints(manager: nil) + + /// Handler that is called when state has changed in the manager. + public typealias ObservableNotificationHandler = ( + _ notification: MIDIIONotification, + _ manager: ObservableMIDIManager + ) -> Void + + // MARK: - Init + + /// Initialize the MIDI manager (and Core MIDI client). + /// + /// - Parameters: + /// - clientName: Name identifying this instance, used as Core MIDI client ID. + /// This is internal and not visible to the end-user. + /// - model: The name of your software, which will be visible to the end-user in ports created + /// by the manager. + /// - manufacturer: The name of your company, which may be visible to the end-user in ports + /// created by the manager. + /// - notificationHandler: Optionally supply a callback handler for MIDI system notifications. + public override init( + clientName: String, + model: String, + manufacturer: String, + notificationHandler: ObservableNotificationHandler? = nil + ) { + // wrap base MIDIManager handler with one that supplies an observable manager reference + var notificationHandlerWrapper: NotificationHandler? = nil + if let notificationHandler = notificationHandler { + notificationHandlerWrapper = { notif, manager in + guard let typedManager = manager as? ObservableMIDIManager else { + assertionFailure("MIDI Manager is not expected type ObservableMIDIManager.") + return + } + notificationHandler(notif, typedManager) + } + } + + super.init( + clientName: clientName, + model: model, + manufacturer: manufacturer, + notificationHandler: notificationHandlerWrapper + ) + + observableEndpoints.manager = self + } + + public override func updateObjectsCache() { + // objectWillChange.send() // redundant since all local properties are marked @Published + super.updateObjectsCache() + + observableDevices.updateCachedProperties() + observableEndpoints.updateCachedProperties() + } +} + +#endif + +#endif diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiverOptions.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiverOptions.swift index 3c1717eca6..16a696354c 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiverOptions.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiverOptions.swift @@ -8,10 +8,16 @@ import Foundation +/// Options for ``MIDIReceiver``. public struct MIDIReceiverOptions: OptionSet { public let rawValue: Int + /// For MIDI 1.0 note-on events, translate a velocity value of 0 to be a note-off event instead. public static let translateMIDI1NoteOnZeroVelocityToNoteOff = MIDIReceiverOptions(rawValue: 1 << 0) + + /// Filter (remove) active-sensing and clock messages. + /// This is useful when logging or monitoring incoming MIDI events from MIDI keyboards and devices + /// that send these messages at a fast rate. public static let filterActiveSensingAndClock = MIDIReceiverOptions(rawValue: 1 << 1) public init(rawValue: Int) { diff --git a/Sources/MIDIKitUI/MIDIEndpointsList.swift b/Sources/MIDIKitUI/MIDIEndpointsList.swift index d5c134b841..7aa0b75215 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsList.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsList.swift @@ -10,94 +10,86 @@ import MIDIKitIO import SwiftUI @available(macOS 11.0, iOS 14.0, *) -struct MIDIEndpointsList: View -where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdentifier { - @EnvironmentObject private var midiManager: MIDIManager +struct MIDIEndpointsList: View, MIDIEndpointsSelectable +where Endpoint: MIDIEndpoint & Hashable & Identifiable, + Endpoint.ID == MIDIIdentifier +{ + private weak var midiManager: ObservableMIDIManager? var endpoints: [Endpoint] - @State var filter: MIDIEndpointFilter - @Binding var selection: MIDIIdentifier? - @Binding var cachedSelectionName: String? + var maskedFilter: MIDIEndpointMaskedFilter? + @Binding var selectionID: MIDIIdentifier? + @Binding var selectionDisplayName: String? let showIcons: Bool - @State private var ids: [MIDIIdentifier] = [] + @State var ids: [MIDIIdentifier] = [] init( endpoints: [Endpoint], - filter: MIDIEndpointFilter, - selection: Binding, - cachedSelectionName: Binding, - showIcons: Bool + maskedFilter: MIDIEndpointMaskedFilter?, + selectionID: Binding, + selectionDisplayName: Binding, + showIcons: Bool, + midiManager: ObservableMIDIManager? ) { self.endpoints = endpoints - _filter = State(initialValue: filter) - _selection = selection - _cachedSelectionName = cachedSelectionName + self.maskedFilter = maskedFilter + _selectionID = selectionID + _selectionDisplayName = selectionDisplayName self.showIcons = showIcons - // set up initial data, but skip filter because midiManager is not available yet - _ids = State(initialValue: generateIDs(endpoints: endpoints, filtered: false)) + self.midiManager = midiManager + + // pre-populate IDs + _ids = State(initialValue: generateIDs(endpoints: endpoints, maskedFilter: maskedFilter, midiManager: midiManager)) } public var body: some View { - List(selection: $selection) { + List(selection: $selectionID) { ForEach(ids, id: \.self) { EndpointRow( endpoint: endpoint(for: $0), - cachedSelectionName: $cachedSelectionName, + selectionDisplayName: $selectionDisplayName, showIcon: showIcons ) .tag($0 as MIDIIdentifier?) } } .onAppear { - updateIDs(endpoints: endpoints) + updateID(endpoints: endpoints) + ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter, midiManager: midiManager) } - .onChange(of: filter) { newValue in - updateIDs(endpoints: endpoints) + .onChange(of: maskedFilter) { newValue in + ids = generateIDs(endpoints: endpoints, maskedFilter: newValue, midiManager: midiManager) } .onChange(of: endpoints) { newValue in - updateIDs(endpoints: newValue) + updateID(endpoints: newValue) + ids = generateIDs(endpoints: newValue, maskedFilter: maskedFilter, midiManager: midiManager) } - .onChange(of: selection) { newValue in - updateIDs(endpoints: endpoints) - guard let selection else { - cachedSelectionName = nil + .onChange(of: selectionID) { newValue in + ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter, midiManager: midiManager) + guard let selectionID = newValue else { + selectionDisplayName = nil return } - if let dn = endpoint(for: selection)?.displayName { - cachedSelectionName = dn + if let displayName = endpoint(for: selectionID)?.displayName { + selectionDisplayName = displayName } } } - private func generateIDs( - endpoints: [Endpoint], - filtered: Bool = true - ) -> [MIDIIdentifier] { - let endpointIDs = ( - filtered ? endpoints.filter(using: filter, in: midiManager) : endpoints - ) - .map(\.id) - - if let selection, !endpointIDs.contains(selection) { - return [selection] + endpointIDs - } else { - return endpointIDs + private func updateID(endpoints: [Endpoint]) { + guard let updatedDetails = updatedID(endpoints: endpoints) else { + return } - } - - /// (Don't run from init.) - private func updateIDs(endpoints: [Endpoint]) { - ids = generateIDs(endpoints: endpoints) - } - - private func endpoint(for id: MIDIIdentifier) -> Endpoint? { - endpoints.first(whereUniqueID: id) + + self.selectionDisplayName = updatedDetails.displayName + // update ID in case it changed + if self.selectionID != updatedDetails.id { self.selectionID = updatedDetails.id } } private struct EndpointRow: View { let endpoint: Endpoint? - @Binding var cachedSelectionName: String? + @Binding var selectionDisplayName: String? let showIcon: Bool var body: some View { @@ -125,8 +117,8 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent private var missingText: String { showIcon - ? cachedSelectionName ?? "Missing" - : (cachedSelectionName ?? "") + " (Missing)" + ? selectionDisplayName ?? "Missing" + : (selectionDisplayName ?? "") + " (Missing)" } @ViewBuilder @@ -153,70 +145,120 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent } /// SwiftUI `List` view for selecting MIDI input endpoints. +/// +/// Optionally supply a tag to auto-update an output connection in MIDIManager. +/// +/// ```swift +/// MIDIInputsList( ... ) +/// .updatingOutputConnection(withTag: "MyConnection") +/// ``` @available(macOS 11.0, iOS 14.0, *) -public struct MIDIInputsList: View { - @EnvironmentObject private var midiManager: MIDIManager +public struct MIDIInputsList: View, _MIDIInputsSelectable { + @EnvironmentObject private var midiManager: ObservableMIDIManager + + @Binding public var selectionID: MIDIIdentifier? + @Binding public var selectionDisplayName: String? + public var showIcons: Bool + public var hideOwned: Bool - @Binding public var selection: MIDIIdentifier? - @Binding public var cachedSelectionName: String? - public let showIcons: Bool - public let filterOwned: Bool + internal var updatingOutputConnectionWithTag: String? public init( - selection: Binding, - cachedSelectionName: Binding, + selectionID: Binding, + selectionDisplayName: Binding, showIcons: Bool = true, - filterOwned: Bool = false + hideOwned: Bool = false ) { - _selection = selection - _cachedSelectionName = cachedSelectionName + _selectionID = selectionID + _selectionDisplayName = selectionDisplayName self.showIcons = showIcons - self.filterOwned = filterOwned + self.hideOwned = hideOwned } public var body: some View { MIDIEndpointsList( - endpoints: midiManager.endpoints.inputs, - filter: filterOwned ? .owned() : .default(), - selection: $selection, - cachedSelectionName: $cachedSelectionName, - showIcons: showIcons + endpoints: midiManager.observableEndpoints.inputs, + maskedFilter: maskedFilter, + selectionID: $selectionID, + selectionDisplayName: $selectionDisplayName, + showIcons: showIcons, + midiManager: midiManager ) - Text("Selected: \(cachedSelectionName ?? "None")") + .onAppear { + updateOutputConnection(id: selectionID) + } + .onChange(of: selectionID) { newValue in + updateOutputConnection(id: newValue) + } + } + + private var maskedFilter: MIDIEndpointMaskedFilter? { + hideOwned ? .drop(.owned()) : nil + } + + private func updateOutputConnection(id: MIDIIdentifier?) { + updateOutputConnection(selectedUniqueID: id, + selectedDisplayName: selectionDisplayName, + midiManager: midiManager) } } /// SwiftUI `List` view for selecting MIDI output endpoints. +/// +/// Optionally supply a tag to auto-update an input connection in MIDIManager. +/// +/// ```swift +/// MIDIOutputsList( ... ) +/// .updatingInputConnection(withTag: "MyConnection") +/// ``` @available(macOS 11.0, iOS 14.0, *) -public struct MIDIOutputsList: View { - @EnvironmentObject private var midiManager: MIDIManager +public struct MIDIOutputsList: View, _MIDIOutputsSelectable { + @EnvironmentObject private var midiManager: ObservableMIDIManager + + @Binding public var selectionID: MIDIIdentifier? + @Binding public var selectionDisplayName: String? + public var showIcons: Bool + public var hideOwned: Bool - @Binding public var selection: MIDIIdentifier? - @Binding public var cachedSelectionName: String? - public let showIcons: Bool - public let filterOwned: Bool + internal var updatingInputConnectionWithTag: String? public init( - selection: Binding, - cachedSelectionName: Binding, + selectionID: Binding, + selectionDisplayName: Binding, showIcons: Bool = true, - filterOwned: Bool = false + hideOwned: Bool = false ) { - _selection = selection - _cachedSelectionName = cachedSelectionName + _selectionID = selectionID + _selectionDisplayName = selectionDisplayName self.showIcons = showIcons - self.filterOwned = filterOwned + self.hideOwned = hideOwned } public var body: some View { MIDIEndpointsList( - endpoints: midiManager.endpoints.outputs, - filter: filterOwned ? .owned() : .default(), - selection: $selection, - cachedSelectionName: $cachedSelectionName, - showIcons: showIcons + endpoints: midiManager.observableEndpoints.outputs, + maskedFilter: maskedFilter, + selectionID: $selectionID, + selectionDisplayName: $selectionDisplayName, + showIcons: showIcons, + midiManager: midiManager ) - Text("Selected: \(cachedSelectionName ?? "None")") + .onAppear { + updateInputConnection(id: selectionID) + } + .onChange(of: selectionID) { newValue in + updateInputConnection(id: newValue) + } + } + + private var maskedFilter: MIDIEndpointMaskedFilter? { + hideOwned ? .drop(.owned()) : nil + } + + private func updateInputConnection(id: MIDIIdentifier?) { + updateInputConnection(selectedUniqueID: id, + selectedDisplayName: selectionDisplayName, + midiManager: midiManager) } } diff --git a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift index 27e526bd83..6ebbd3e1bb 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift @@ -10,100 +10,92 @@ import MIDIKitIO import SwiftUI @available(macOS 11.0, iOS 14.0, *) -struct MIDIEndpointsPicker: View -where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdentifier { - @EnvironmentObject private var midiManager: MIDIManager +struct MIDIEndpointsPicker: View, MIDIEndpointsSelectable +where Endpoint: MIDIEndpoint & Hashable & Identifiable, + Endpoint.ID == MIDIIdentifier +{ + private weak var midiManager: ObservableMIDIManager? - var title: String + let title: String var endpoints: [Endpoint] - @State var filter: MIDIEndpointFilter - @Binding var selection: MIDIIdentifier? - @Binding var cachedSelectionName: String? - let showIcons: Bool + var maskedFilter: MIDIEndpointMaskedFilter? + @Binding var selectionID: MIDIIdentifier? + @Binding var selectionDisplayName: String? + var showIcons: Bool - @State private var ids: [MIDIIdentifier] = [] + @State var ids: [MIDIIdentifier] = [] init( title: String, endpoints: [Endpoint], - filter: MIDIEndpointFilter, - selection: Binding, - cachedSelectionName: Binding, - showIcons: Bool + maskedFilter: MIDIEndpointMaskedFilter?, + selectionID: Binding, + selectionDisplayName: Binding, + showIcons: Bool, + midiManager: ObservableMIDIManager? ) { self.title = title self.endpoints = endpoints - _filter = State(initialValue: filter) - _selection = selection - _cachedSelectionName = cachedSelectionName + self.maskedFilter = maskedFilter + _selectionID = selectionID + _selectionDisplayName = selectionDisplayName self.showIcons = showIcons - // set up initial data, but skip filter because midiManager is not available yet - _ids = State(initialValue: generateIDs(endpoints: endpoints, filtered: false)) + self.midiManager = midiManager + + // pre-populate IDs + _ids = State(initialValue: generateIDs(endpoints: endpoints, maskedFilter: maskedFilter, midiManager: midiManager)) } public var body: some View { - Picker(title, selection: $selection) { + Picker(title, selection: $selectionID) { Text("None") .tag(MIDIIdentifier?.none) ForEach(ids, id: \.self) { EndpointRow( endpoint: endpoint(for: $0), - cachedSelectionName: $cachedSelectionName, + selectionDisplayName: $selectionDisplayName, showIcon: showIcons ) .tag($0 as MIDIIdentifier?) } } .onAppear { - updateIDs(endpoints: endpoints) + updateID(endpoints: endpoints) + ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter, midiManager: midiManager) } - .onChange(of: filter) { newValue in - updateIDs(endpoints: endpoints) + .onChange(of: maskedFilter) { newValue in + ids = generateIDs(endpoints: endpoints, maskedFilter: newValue, midiManager: midiManager) } .onChange(of: endpoints) { newValue in - updateIDs(endpoints: newValue) + updateID(endpoints: newValue) + ids = generateIDs(endpoints: newValue, maskedFilter: maskedFilter, midiManager: midiManager) } - .onChange(of: selection) { newValue in - updateIDs(endpoints: endpoints) - guard let selection else { - cachedSelectionName = nil + .onChange(of: selectionID) { newValue in + ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter, midiManager: midiManager) + guard let selectionID = newValue else { + selectionDisplayName = nil return } - if let dn = endpoint(for: selection)?.displayName { - cachedSelectionName = dn + if let displayName = endpoint(for: selectionID)?.displayName { + selectionDisplayName = displayName } } } - private func generateIDs( - endpoints: [Endpoint], - filtered: Bool = true - ) -> [MIDIIdentifier] { - let endpointIDs = ( - filtered ? endpoints.filter(using: filter, in: midiManager) : endpoints - ) - .map(\.id) - - if let selection, !endpointIDs.contains(selection) { - return [selection] + endpointIDs - } else { - return endpointIDs + private func updateID(endpoints: [Endpoint]) { + guard let updatedDetails = updatedID(endpoints: endpoints) else { + return } - } - - /// (Don't run from init.) - private func updateIDs(endpoints: [Endpoint]) { - ids = generateIDs(endpoints: endpoints) - } - - private func endpoint(for id: MIDIIdentifier) -> Endpoint? { - endpoints.first(whereUniqueID: id) + + self.selectionDisplayName = updatedDetails.displayName + // update ID in case it changed + if self.selectionID != updatedDetails.id { self.selectionID = updatedDetails.id } } private struct EndpointRow: View { let endpoint: Endpoint? - @Binding var cachedSelectionName: String? + @Binding var selectionDisplayName: String? let showIcon: Bool var body: some View { @@ -139,8 +131,8 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent private var missingText: String { showIcon - ? cachedSelectionName ?? "Missing" - : (cachedSelectionName ?? "") + " (Missing)" + ? selectionDisplayName ?? "Missing" + : (selectionDisplayName ?? "") + " (Missing)" } @ViewBuilder @@ -163,76 +155,128 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent } /// SwiftUI `Picker` view for selecting MIDI input endpoints. +/// +/// Optionally supply a tag to auto-update an output connection in MIDIManager. +/// +/// ```swift +/// MIDIInputsPicker( ... ) +/// .updatingOutputConnection(withTag: "MyConnection") +/// ``` @available(macOS 11.0, iOS 14.0, *) -public struct MIDIInputsPicker: View { - @EnvironmentObject private var midiManager: MIDIManager +public struct MIDIInputsPicker: View, _MIDIInputsSelectable { + @EnvironmentObject private var midiManager: ObservableMIDIManager public var title: String - @Binding public var selection: MIDIIdentifier? - @Binding public var cachedSelectionName: String? - public let showIcons: Bool - public let filterOwned: Bool + @Binding public var selectionID: MIDIIdentifier? + @Binding public var selectionDisplayName: String? + public var showIcons: Bool + public var hideOwned: Bool + + internal var updatingOutputConnectionWithTag: String? public init( title: String, - selection: Binding, - cachedSelectionName: Binding, + selectionID: Binding, + selectionDisplayName: Binding, showIcons: Bool = true, - filterOwned: Bool = false + hideOwned: Bool = false ) { self.title = title - _selection = selection - _cachedSelectionName = cachedSelectionName + _selectionID = selectionID + _selectionDisplayName = selectionDisplayName self.showIcons = showIcons - self.filterOwned = filterOwned + self.hideOwned = hideOwned } public var body: some View { MIDIEndpointsPicker( title: title, - endpoints: midiManager.endpoints.inputs, - filter: filterOwned ? .owned() : .default(), - selection: $selection, - cachedSelectionName: $cachedSelectionName, - showIcons: showIcons + endpoints: midiManager.observableEndpoints.inputs, + maskedFilter: maskedFilter, + selectionID: $selectionID, + selectionDisplayName: $selectionDisplayName, + showIcons: showIcons, + midiManager: midiManager ) + .onAppear { + updateOutputConnection(id: selectionID) + } + .onChange(of: selectionID) { newValue in + updateOutputConnection(id: newValue) + } + } + + private var maskedFilter: MIDIEndpointMaskedFilter? { + hideOwned ? .drop(.owned()) : nil + } + + private func updateOutputConnection(id: MIDIIdentifier?) { + updateOutputConnection(selectedUniqueID: id, + selectedDisplayName: selectionDisplayName, + midiManager: midiManager) } } /// SwiftUI `Picker` view for selecting MIDI output endpoints. +/// +/// Optionally supply a tag to auto-update an input connection in MIDIManager. +/// +/// ```swift +/// MIDIOutputsPicker( ... ) +/// .updatingInputConnection(withTag: "MyConnection") +/// ``` @available(macOS 11.0, iOS 14.0, *) -public struct MIDIOutputsPicker: View { - @EnvironmentObject private var midiManager: MIDIManager +public struct MIDIOutputsPicker: View, _MIDIOutputsSelectable { + @EnvironmentObject private var midiManager: ObservableMIDIManager public var title: String - @Binding public var selection: MIDIIdentifier? - @Binding public var cachedSelectionName: String? - public let showIcons: Bool - public let filterOwned: Bool + @Binding public var selectionID: MIDIIdentifier? + @Binding public var selectionDisplayName: String? + public var showIcons: Bool + public var hideOwned: Bool + + internal var updatingInputConnectionWithTag: String? public init( title: String, - selection: Binding, - cachedSelectionName: Binding, + selectionID: Binding, + selectionDisplayName: Binding, showIcons: Bool = true, - filterOwned: Bool = false + hideOwned: Bool = false ) { self.title = title - _selection = selection - _cachedSelectionName = cachedSelectionName + _selectionID = selectionID + _selectionDisplayName = selectionDisplayName self.showIcons = showIcons - self.filterOwned = filterOwned + self.hideOwned = hideOwned } public var body: some View { MIDIEndpointsPicker( title: title, - endpoints: midiManager.endpoints.outputs, - filter: filterOwned ? .owned() : .default(), - selection: $selection, - cachedSelectionName: $cachedSelectionName, - showIcons: showIcons + endpoints: midiManager.observableEndpoints.outputs, + maskedFilter: maskedFilter, + selectionID: $selectionID, + selectionDisplayName: $selectionDisplayName, + showIcons: showIcons, + midiManager: midiManager ) + .onAppear { + updateInputConnection(id: selectionID) + } + .onChange(of: selectionID) { newValue in + updateInputConnection(id: newValue) + } + } + + private var maskedFilter: MIDIEndpointMaskedFilter? { + hideOwned ? .drop(.owned()) : nil + } + + private func updateInputConnection(id: MIDIIdentifier?) { + updateInputConnection(selectedUniqueID: id, + selectedDisplayName: selectionDisplayName, + midiManager: midiManager) } } diff --git a/Sources/MIDIKitUI/MIDIKitUI Protocols.swift b/Sources/MIDIKitUI/MIDIKitUI Protocols.swift new file mode 100644 index 0000000000..5fa94c1cfe --- /dev/null +++ b/Sources/MIDIKitUI/MIDIKitUI Protocols.swift @@ -0,0 +1,169 @@ +// +// MIDIKitUI Protocols.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +#if canImport(SwiftUI) && !os(tvOS) && !os(watchOS) + +import SwiftUI +import MIDIKitIO + +// MARK: - MIDIEndpointsSelectable + +@available(macOS 11.0, iOS 14.0, *) +public protocol MIDIEndpointsSelectable where Self: View, Endpoint.ID == MIDIIdentifier { + associatedtype Endpoint: MIDIEndpoint & Hashable & Identifiable + + var endpoints: [Endpoint] { get set } + var maskedFilter: MIDIEndpointMaskedFilter? { get set } + var selectionID: MIDIIdentifier? { get set } + var selectionDisplayName: String? { get set } + + var ids: [MIDIIdentifier] { get set } +} + +@available(macOS 11.0, iOS 14.0, *) +extension MIDIEndpointsSelectable { + /// Returns non-nil if properties require updating. + func updatedID(endpoints: [Endpoint]) -> (id: MIDIIdentifier?, displayName: String?)? { + if selectionID == .invalidMIDIIdentifier { + return (id: nil, displayName: nil) + } + + if let selectionID = selectionID, + let selectionDisplayName = selectionDisplayName, + let found = endpoints.first( + whereUniqueID: selectionID, + fallbackDisplayName: selectionDisplayName + ) + { + return (id: found.uniqueID, displayName: found.displayName) + } + + return nil + } + + func generateIDs( + endpoints: [Endpoint], + maskedFilter: MIDIEndpointMaskedFilter?, + midiManager: ObservableMIDIManager? + ) -> [MIDIIdentifier] { + var endpointIDs: [MIDIIdentifier] = [] + if let maskedFilter = maskedFilter, let midiManager = midiManager { + endpointIDs = endpoints + .filter(maskedFilter, in: midiManager) + .map(\.id) + } else { + endpointIDs = endpoints + .map(\.id) + } + + if let selectionID, !endpointIDs.contains(selectionID) { + return [selectionID] + endpointIDs + } else { + return endpointIDs + } + } + + func endpoint(for id: MIDIIdentifier) -> Endpoint? { + endpoints.first(whereUniqueID: id) + } +} + +// MARK: - MIDIInputsSelectable + +@available(macOS 11.0, iOS 14.0, *) +public protocol MIDIInputsSelectable { + func updatingOutputConnection(withTag tag: String?) -> Self +} + +@available(macOS 11.0, iOS 14.0, *) +protocol _MIDIInputsSelectable: MIDIInputsSelectable { + var updatingOutputConnectionWithTag: String? { get set } +} + +@available(macOS 11.0, iOS 14.0, *) +extension _MIDIInputsSelectable { + public func updatingOutputConnection(withTag tag: String?) -> Self { + var copy = self + copy.updatingOutputConnectionWithTag = tag + return copy + } + + func updateOutputConnection( + selectedUniqueID: MIDIIdentifier?, + selectedDisplayName: String?, + midiManager: ObservableMIDIManager + ) { + guard let tag = updatingOutputConnectionWithTag, + let midiOutputConnection = midiManager.managedOutputConnections[tag] + else { return } + + guard let selectedUniqueID = selectedUniqueID, + let selectedDisplayName = selectedDisplayName, + selectedUniqueID != .invalidMIDIIdentifier + else { + midiOutputConnection.removeAllInputs() + return + } + + let criterium: MIDIEndpointIdentity = .uniqueIDWithFallback( + id: selectedUniqueID, fallbackDisplayName: selectedDisplayName + ) + if midiOutputConnection.inputsCriteria != [criterium] { + midiOutputConnection.removeAllInputs() + midiOutputConnection.add(inputs: [criterium]) + } + } +} + + +// MARK: - MIDIOutputsSelectable + +@available(macOS 11.0, iOS 14.0, *) +public protocol MIDIOutputsSelectable { + func updatingInputConnection(withTag tag: String?) -> Self +} + +@available(macOS 11.0, iOS 14.0, *) +protocol _MIDIOutputsSelectable: MIDIOutputsSelectable { + var updatingInputConnectionWithTag: String? { get set } +} + +@available(macOS 11.0, iOS 14.0, *) +extension _MIDIOutputsSelectable { + public func updatingInputConnection(withTag tag: String?) -> Self { + var copy = self + copy.updatingInputConnectionWithTag = tag + return copy + } + + func updateInputConnection( + selectedUniqueID: MIDIIdentifier?, + selectedDisplayName: String?, + midiManager: ObservableMIDIManager + ) { + guard let tag = updatingInputConnectionWithTag, + let midiInputConnection = midiManager.managedInputConnections[tag] + else { return } + + guard let selectedUniqueID = selectedUniqueID, + let selectedDisplayName = selectedDisplayName, + selectedUniqueID != .invalidMIDIIdentifier + else { + midiInputConnection.removeAllOutputs() + return + } + + let criterium: MIDIEndpointIdentity = .uniqueIDWithFallback( + id: selectedUniqueID, fallbackDisplayName: selectedDisplayName + ) + if midiInputConnection.outputsCriteria != [criterium] { + midiInputConnection.removeAllOutputs() + midiInputConnection.add(outputs: [criterium]) + } + } +} + +#endif diff --git a/Sources/MIDIKitUI/MIDIKitUI.docc/MIDIKitUI-Internals.md b/Sources/MIDIKitUI/MIDIKitUI.docc/MIDIKitUI-Internals.md new file mode 100644 index 0000000000..8331bfcdad --- /dev/null +++ b/Sources/MIDIKitUI/MIDIKitUI.docc/MIDIKitUI-Internals.md @@ -0,0 +1,9 @@ +# Internals + +## Topics + +### Protocols + +- ``MIDIEndpointsSelectable`` +- ``MIDIInputsSelectable`` +- ``MIDIOutputsSelectable`` diff --git a/Sources/MIDIKitUI/MIDIKitUI.docc/MIDIKitUI.md b/Sources/MIDIKitUI/MIDIKitUI.docc/MIDIKitUI.md index f522009f0c..9c622d3a7e 100644 --- a/Sources/MIDIKitUI/MIDIKitUI.docc/MIDIKitUI.md +++ b/Sources/MIDIKitUI/MIDIKitUI.docc/MIDIKitUI.md @@ -17,4 +17,5 @@ MIDIKitUI adds convenient reusable user interface controls to simplify building ### Internals +- - diff --git a/Sources/MIDIKitUI/MIDIKitUI.swift b/Sources/MIDIKitUI/MIDIKitUI.swift index ac222ff27c..58dad76063 100644 --- a/Sources/MIDIKitUI/MIDIKitUI.swift +++ b/Sources/MIDIKitUI/MIDIKitUI.swift @@ -5,4 +5,3 @@ // @_exported import MIDIKitCore -@_exported import MIDIKitIO diff --git a/Tests/MIDIKitIOTests/MIDIEndpoint/MIDIEndpointFilter Tests.swift b/Tests/MIDIKitIOTests/MIDIEndpoint/MIDIEndpointFilter Tests.swift new file mode 100644 index 0000000000..93fe7d4919 --- /dev/null +++ b/Tests/MIDIKitIOTests/MIDIEndpoint/MIDIEndpointFilter Tests.swift @@ -0,0 +1,152 @@ +// +// MIDIEndpointFilter Tests.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +#if shouldTestCurrentPlatform && !os(tvOS) && !os(watchOS) + +@testable import MIDIKitIO +import XCTest + +final class MIDIEndpointFilter_Tests: XCTestCase { + // swiftformat:options --wrapcollections preserve + // swiftformat:disable spaceInsideParens spaceInsideBrackets + // swiftformat:options --maxwidth none + + // MARK: - test data + + let ownedInputEndpoints: [MIDIInputEndpoint] = [ + .init(ref: 1000, name: "Virtual Input A", displayName: "My Virtual Input A", uniqueID: -1000), + .init(ref: 1001, name: "Virtual Input B", displayName: "My Virtual Input B", uniqueID: -1001) + ] + + let unownedInputEndpoints: [MIDIInputEndpoint] = [ + .init(ref: 2000, name: "Input B", displayName: "Unowned Input B", uniqueID: -2000), + .init(ref: 2003, name: "Input A", displayName: "Unowned Input A", uniqueID: -2003), + .init(ref: 2001, name: "Input C", displayName: "Unowned Input C", uniqueID: -2001), + .init(ref: 2002, name: "Input A", displayName: "Unowned Input A", uniqueID: -2002), + .init(ref: 2004, name: "Input D", displayName: "Unowned Input D", uniqueID: -2004) + ] + + var systemInputEndpoints: [MIDIInputEndpoint] { unownedInputEndpoints + ownedInputEndpoints } + + let ownedOutputEndpoints: [MIDIOutputEndpoint] = [ + .init(ref: 3000, name: "Virtual Output A", displayName: "My Virtual Output A", uniqueID: -3000), + .init(ref: 3001, name: "Virtual Output B", displayName: "My Virtual Output B", uniqueID: -3001) + ] + + let unownedOutputEndpoints: [MIDIOutputEndpoint] = [ + .init(ref: 4000, name: "Output B", displayName: "Unowned Output B", uniqueID: -4000), + .init(ref: 4003, name: "Output A", displayName: "Unowned Output A", uniqueID: -4003), + .init(ref: 4001, name: "Output C", displayName: "Unowned Output C", uniqueID: -4001), + .init(ref: 4002, name: "Output A", displayName: "Unowned Output A", uniqueID: -4002), + .init(ref: 4004, name: "Output D", displayName: "Unowned Output D", uniqueID: -4004) + ] + + var systemOutputEndpoints: [MIDIOutputEndpoint] { unownedOutputEndpoints + ownedOutputEndpoints } + + func testMaskedFilter_Inputs() throws { + // only owned + XCTAssertEqual( + systemInputEndpoints.filter( + .owned(), + ownedInputEndpoints: ownedInputEndpoints, + ownedOutputEndpoints: ownedOutputEndpoints + ), + ownedInputEndpoints + ) + + // drop owned + XCTAssertEqual( + systemInputEndpoints.filter( + dropping: .owned(), + ownedInputEndpoints: ownedInputEndpoints, + ownedOutputEndpoints: ownedOutputEndpoints + ), + unownedInputEndpoints + ) + + // only specific endpoints, both owned and unowned + XCTAssertEqual( + systemInputEndpoints.filter( + MIDIEndpointFilter(owned: false, criteria: [.uniqueID(-2000), .uniqueID(-2004), .uniqueID(-1001)]), + ownedInputEndpoints: ownedInputEndpoints, + ownedOutputEndpoints: ownedOutputEndpoints + ), + [ + .init(ref: 2000, name: "Input B", displayName: "Unowned Input B", uniqueID: -2000), + .init(ref: 2004, name: "Input D", displayName: "Unowned Input D", uniqueID: -2004), + .init(ref: 1001, name: "Virtual Input B", displayName: "My Virtual Input B", uniqueID: -1001) + ] + ) + + // drop specific endpoints, both owned and unowned + XCTAssertEqual( + systemInputEndpoints.filter( + dropping: MIDIEndpointFilter(owned: false, criteria: [.uniqueID(-2000), .uniqueID(-2004), .uniqueID(-1001)]), + ownedInputEndpoints: ownedInputEndpoints, + ownedOutputEndpoints: ownedOutputEndpoints + ), + [ + .init(ref: 2003, name: "Input A", displayName: "Unowned Input A", uniqueID: -2003), + .init(ref: 2001, name: "Input C", displayName: "Unowned Input C", uniqueID: -2001), + .init(ref: 2002, name: "Input A", displayName: "Unowned Input A", uniqueID: -2002), + .init(ref: 1000, name: "Virtual Input A", displayName: "My Virtual Input A", uniqueID: -1000) + ] + ) + } + + func testMaskedFilter_Outputs() throws { + // only owned + XCTAssertEqual( + systemOutputEndpoints.filter( + .owned(), + ownedInputEndpoints: ownedInputEndpoints, + ownedOutputEndpoints: ownedOutputEndpoints + ), + ownedOutputEndpoints + ) + + // drop owned + XCTAssertEqual( + systemOutputEndpoints.filter( + dropping: .owned(), + ownedInputEndpoints: ownedInputEndpoints, + ownedOutputEndpoints: ownedOutputEndpoints + ), + unownedOutputEndpoints + ) + + // only specific endpoints, both owned and unowned + XCTAssertEqual( + systemOutputEndpoints.filter( + MIDIEndpointFilter(owned: false, criteria: [.uniqueID(-4000), .uniqueID(-4004), .uniqueID(-3001)]), + ownedInputEndpoints: ownedInputEndpoints, + ownedOutputEndpoints: ownedOutputEndpoints + ), + [ + .init(ref: 4000, name: "Output B", displayName: "Unowned Output B", uniqueID: -4000), + .init(ref: 4004, name: "Output D", displayName: "Unowned Output D", uniqueID: -4004), + .init(ref: 3001, name: "Virtual Output B", displayName: "My Virtual Output B", uniqueID: -3001) + ] + ) + + // drop specific endpoints, both owned and unowned + XCTAssertEqual( + systemOutputEndpoints.filter( + dropping: MIDIEndpointFilter(owned: false, criteria: [.uniqueID(-4000), .uniqueID(-4004), .uniqueID(-3001)]), + ownedInputEndpoints: ownedInputEndpoints, + ownedOutputEndpoints: ownedOutputEndpoints + ), + [ + .init(ref: 4003, name: "Output A", displayName: "Unowned Output A", uniqueID: -4003), + .init(ref: 4001, name: "Output C", displayName: "Unowned Output C", uniqueID: -4001), + .init(ref: 4002, name: "Output A", displayName: "Unowned Output A", uniqueID: -4002), + .init(ref: 3000, name: "Virtual Output A", displayName: "My Virtual Output A", uniqueID: -3000) + ] + ) + } +} + +#endif