From 3b68b797732966b0f3f384274b19b76495c51362 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 14:12:00 -0800 Subject: [PATCH 01/64] `MIDIEndpoints`: Conditionally use `ObservableObject` on supported systems --- .../MIDIEndpoints/MIDIEndpoints.swift | 53 +++++++++++++++++++ .../MIDIKitIO/MIDIManager/MIDIManager.swift | 28 ++++++---- 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift index f7e718d935..826d0b2e46 100644 --- a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift +++ b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift @@ -80,4 +80,57 @@ public final class MIDIEndpoints: NSObject, MIDIEndpointsProtocol { } } +#if canImport(Combine) +import Combine + +@available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) +public final class MIDIPublishedEndpoints: NSObject, ObservableObject, MIDIEndpointsProtocol { + /// Weak reference to ``MIDIManager``. + internal weak var manager: MIDIManager? + + @Published public internal(set) dynamic var inputs: [MIDIInputEndpoint] = [] + @Published public internal(set) dynamic var inputsUnowned: [MIDIInputEndpoint] = [] + + @Published public internal(set) dynamic var outputs: [MIDIOutputEndpoint] = [] + @Published public internal(set) dynamic var outputsUnowned: [MIDIOutputEndpoint] = [] + + override internal init() { + super.init() + } + + internal init(manager: MIDIManager?) { + self.manager = manager + super.init() + } + + public func updateCachedProperties() { + inputs = getSystemDestinationEndpoints() + + if let manager = manager { + let managedInputsIDs = manager.managedInputs.values + .compactMap { $0.uniqueID } + + inputsUnowned = inputs.filter { + !managedInputsIDs.contains($0.uniqueID) + } + } else { + inputsUnowned = inputs + } + + outputs = getSystemSourceEndpoints() + + if let manager = manager { + let managedOutputsIDs = manager.managedOutputs.values + .compactMap { $0.uniqueID } + + outputsUnowned = outputs.filter { + !managedOutputsIDs.contains($0.uniqueID) + } + } else { + outputsUnowned = outputs + } + } +} +#endif + #endif diff --git a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift index a50ec010b3..4c74cf4d78 100644 --- a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift @@ -70,8 +70,9 @@ 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) } @@ -79,7 +80,7 @@ public final class MIDIManager: NSObject { public internal(set) var devices: MIDIDevicesProtocol = MIDIDevices() /// MIDI input and output endpoints in the system. - public internal(set) var endpoints: MIDIEndpointsProtocol = MIDIEndpoints() + public internal(set) var endpoints: MIDIEndpointsProtocol /// Handler that is called when state has changed in the manager. public var notificationHandler: (( @@ -118,11 +119,11 @@ public final class MIDIManager: NSObject { ) { // 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 +134,22 @@ 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 + if #available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) { + endpoints = MIDIPublishedEndpoints() + } else { + endpoints = MIDIEndpoints() + } + super.init() - - endpoints = MIDIEndpoints(manager: self) - + addNetworkSessionObservers() } @@ -155,7 +161,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) } } From 6219c6929993432238280e75f43ae9906978d02a Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 14:31:09 -0800 Subject: [PATCH 02/64] `MIDIEndpointsProtocol`: Added Equatable, Hashable --- Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift | 2 +- Sources/MIDIKitIO/MIDIManager/MIDIManager.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift index 826d0b2e46..94c1ae0bc5 100644 --- a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift +++ b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift @@ -10,7 +10,7 @@ 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 { +public protocol MIDIEndpointsProtocol where Self: Equatable, Self: Hashable { /// List of MIDI input endpoints in the system. var inputs: [MIDIInputEndpoint] { get } diff --git a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift index 4c74cf4d78..8e99caef34 100644 --- a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift @@ -80,7 +80,7 @@ public final class MIDIManager: NSObject { public internal(set) var devices: MIDIDevicesProtocol = MIDIDevices() /// MIDI input and output endpoints in the system. - public internal(set) var endpoints: MIDIEndpointsProtocol + public internal(set) var endpoints: any MIDIEndpointsProtocol /// Handler that is called when state has changed in the manager. public var notificationHandler: (( From 577dfe06b3d2d6ac3221774e70b8aa972a7f3904 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 15:18:09 -0800 Subject: [PATCH 03/64] `MIDIManager`: Refactored to use new `observableEndpoints` computed property --- .../MIDIEndpoints/MIDIEndpoints.swift | 8 ++++--- .../MIDIKitIO/MIDIManager/MIDIManager.swift | 24 ++++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift index 94c1ae0bc5..ade2f15041 100644 --- a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift +++ b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift @@ -8,6 +8,10 @@ import Foundation +#if canImport(Combine) +import Combine +#endif + // 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 where Self: Equatable, Self: Hashable { @@ -81,10 +85,8 @@ public final class MIDIEndpoints: NSObject, MIDIEndpointsProtocol { } #if canImport(Combine) -import Combine - @available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) -public final class MIDIPublishedEndpoints: NSObject, ObservableObject, MIDIEndpointsProtocol { +public final class MIDIObservableEndpoints: NSObject, ObservableObject, MIDIEndpointsProtocol { /// Weak reference to ``MIDIManager``. internal weak var manager: MIDIManager? diff --git a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift index 8e99caef34..ea856f8adb 100644 --- a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift @@ -80,7 +80,10 @@ public final class MIDIManager: NSObject { public internal(set) var devices: MIDIDevicesProtocol = MIDIDevices() /// MIDI input and output endpoints in the system. - public internal(set) var endpoints: any MIDIEndpointsProtocol + public internal(set) var endpoints: MIDIEndpoints + + /// Type-erased internal backing storage for ``observableEndpoints``. + internal var _observableEndpoints: (any MIDIEndpointsProtocol)? /// Handler that is called when state has changed in the manager. public var notificationHandler: (( @@ -143,7 +146,8 @@ public final class MIDIManager: NSObject { // endpoints if #available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) { - endpoints = MIDIPublishedEndpoints() + endpoints = MIDIEndpoints() + _observableEndpoints = MIDIObservableEndpoints() } else { endpoints = MIDIEndpoints() } @@ -194,6 +198,7 @@ public final class MIDIManager: NSObject { devices.updateCachedProperties() endpoints.updateCachedProperties() + _observableEndpoints?.updateCachedProperties() } } @@ -202,7 +207,20 @@ import Combine @available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) extension MIDIManager: ObservableObject { - // nothing here; just add ObservableObject conformance + /// MIDI input and output endpoints in the system. + /// The same as ``endpoints`` but returned as an `ObservableObject` for use in SwiftUI and Combine. + public var observableEndpoints: MIDIObservableEndpoints { + // we can be reasonably guaranteed endpoints will be + guard let typedEndpoints = _observableEndpoints as? MIDIObservableEndpoints else { + assertionFailure( + "MIDI Manager's endpoints instance is not expected type: \(type(of: endpoints))." + ) + // this should never happen, but just in case, return a new class instance + // to avoid halting execution + return MIDIObservableEndpoints(manager: self) + } + return typedEndpoints + } } #endif From 76917537f32c72bb3ec8696dcca14f51fb071341 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 15:27:14 -0800 Subject: [PATCH 04/64] `MIDIManager`: Fixed regression in updating endpoints --- Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift | 8 ++++++++ Sources/MIDIKitIO/MIDIManager/MIDIManager.swift | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift index ade2f15041..d38f521d03 100644 --- a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift +++ b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift @@ -85,6 +85,9 @@ public final class MIDIEndpoints: NSObject, MIDIEndpointsProtocol { } #if canImport(Combine) + +/// Manages system MIDI endpoints information cache. +/// Class and properties are published for use in SwiftUI and Combine. @available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) public final class MIDIObservableEndpoints: NSObject, ObservableObject, MIDIEndpointsProtocol { /// Weak reference to ``MIDIManager``. @@ -106,8 +109,10 @@ public final class MIDIObservableEndpoints: NSObject, ObservableObject, MIDIEndp } public func updateCachedProperties() { + willChangeValue(for: \.inputs) inputs = getSystemDestinationEndpoints() + willChangeValue(for: \.inputsUnowned) if let manager = manager { let managedInputsIDs = manager.managedInputs.values .compactMap { $0.uniqueID } @@ -119,8 +124,10 @@ public final class MIDIObservableEndpoints: NSObject, ObservableObject, MIDIEndp inputsUnowned = inputs } + willChangeValue(for: \.outputs) outputs = getSystemSourceEndpoints() + willChangeValue(for: \.outputsUnowned) if let manager = manager { let managedOutputsIDs = manager.managedOutputs.values .compactMap { $0.uniqueID } @@ -133,6 +140,7 @@ public final class MIDIObservableEndpoints: NSObject, ObservableObject, MIDIEndp } } } + #endif #endif diff --git a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift index ea856f8adb..f7096168cf 100644 --- a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift @@ -150,10 +150,17 @@ public final class MIDIManager: NSObject { _observableEndpoints = MIDIObservableEndpoints() } else { endpoints = MIDIEndpoints() + _observableEndpoints = nil } super.init() + // we can only add manager reference to endpoints after manager is initialized + endpoints.manager = self + if #available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) { + observableEndpoints.manager = self + } + addNetworkSessionObservers() } From b8321c3f6795c17b00b79d83902e8a3d08453278 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 15:33:21 -0800 Subject: [PATCH 05/64] `MIDIManager`: Fixed regression in updating endpoints --- Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift | 6 ++---- Sources/MIDIKitIO/MIDIManager/MIDIManager.swift | 7 +------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift index d38f521d03..d7474531c6 100644 --- a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift +++ b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift @@ -109,10 +109,10 @@ public final class MIDIObservableEndpoints: NSObject, ObservableObject, MIDIEndp } public func updateCachedProperties() { - willChangeValue(for: \.inputs) + objectWillChange.send() + inputs = getSystemDestinationEndpoints() - willChangeValue(for: \.inputsUnowned) if let manager = manager { let managedInputsIDs = manager.managedInputs.values .compactMap { $0.uniqueID } @@ -124,10 +124,8 @@ public final class MIDIObservableEndpoints: NSObject, ObservableObject, MIDIEndp inputsUnowned = inputs } - willChangeValue(for: \.outputs) outputs = getSystemSourceEndpoints() - willChangeValue(for: \.outputsUnowned) if let manager = manager { let managedOutputsIDs = manager.managedOutputs.values .compactMap { $0.uniqueID } diff --git a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift index f7096168cf..0460e07e7f 100644 --- a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift @@ -190,12 +190,7 @@ 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, */ * - ) { + 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 From 94054f1969b79ad8fd04b5d2614d613f60e41cab Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 16:16:31 -0800 Subject: [PATCH 06/64] `MIDIManager`: Refactored ObservableObject to new `ObservableMIDIManager` subclass --- .../MIDIKitIO/MIDIManager/MIDIManager.swift | 82 ++++++++++--------- Sources/MIDIKitUI/MIDIEndpointsList.swift | 6 +- Sources/MIDIKitUI/MIDIEndpointsPicker.swift | 6 +- 3 files changed, 50 insertions(+), 44 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift index 0460e07e7f..c3578e195c 100644 --- a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift @@ -13,7 +13,7 @@ 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 { +public class MIDIManager: NSObject { // MARK: - Properties /// MIDI Client Name. @@ -82,9 +82,6 @@ public final class MIDIManager: NSObject { /// MIDI input and output endpoints in the system. public internal(set) var endpoints: MIDIEndpoints - /// Type-erased internal backing storage for ``observableEndpoints``. - internal var _observableEndpoints: (any MIDIEndpointsProtocol)? - /// Handler that is called when state has changed in the manager. public var notificationHandler: (( _ notification: MIDIIONotification, @@ -145,21 +142,12 @@ public final class MIDIManager: NSObject { self.notificationHandler = notificationHandler // endpoints - if #available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) { - endpoints = MIDIEndpoints() - _observableEndpoints = MIDIObservableEndpoints() - } else { - endpoints = MIDIEndpoints() - _observableEndpoints = nil - } + endpoints = MIDIEndpoints() super.init() // we can only add manager reference to endpoints after manager is initialized endpoints.manager = self - if #available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) { - observableEndpoints.manager = self - } addNetworkSessionObservers() } @@ -189,39 +177,57 @@ 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 - devices.updateCachedProperties() endpoints.updateCachedProperties() - _observableEndpoints?.updateCachedProperties() } } #if canImport(Combine) import Combine +/// ``MIDIManager`` subclass that is observable in a SwiftUI or Combine context. @available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) -extension MIDIManager: ObservableObject { - /// MIDI input and output endpoints in the system. - /// The same as ``endpoints`` but returned as an `ObservableObject` for use in SwiftUI and Combine. - public var observableEndpoints: MIDIObservableEndpoints { - // we can be reasonably guaranteed endpoints will be - guard let typedEndpoints = _observableEndpoints as? MIDIObservableEndpoints else { - assertionFailure( - "MIDI Manager's endpoints instance is not expected type: \(type(of: endpoints))." - ) - // this should never happen, but just in case, return a new class instance - // to avoid halting execution - return MIDIObservableEndpoints(manager: self) - } - return typedEndpoints +public final class ObservableMIDIManager: MIDIManager, ObservableObject { + // MARK: - Properties + + /// Type-erased internal backing storage for ``observableEndpoints``. + @Published public var observableEndpoints = MIDIObservableEndpoints() + + // 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: (( + _ notification: MIDIIONotification, + _ manager: MIDIManager + ) -> Void)? = nil + ) { + super.init( + clientName: clientName, + model: model, + manufacturer: manufacturer, + notificationHandler: notificationHandler + ) + + observableEndpoints.manager = self + } + + public override func updateObjectsCache() { + objectWillChange.send() + super.updateObjectsCache() + observableEndpoints.updateCachedProperties() } } #endif diff --git a/Sources/MIDIKitUI/MIDIEndpointsList.swift b/Sources/MIDIKitUI/MIDIEndpointsList.swift index d5c134b841..4b4aa84b0c 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsList.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsList.swift @@ -12,7 +12,7 @@ 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 + @EnvironmentObject private var midiManager: ObservableMIDIManager var endpoints: [Endpoint] @State var filter: MIDIEndpointFilter @@ -155,7 +155,7 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent /// SwiftUI `List` view for selecting MIDI input endpoints. @available(macOS 11.0, iOS 14.0, *) public struct MIDIInputsList: View { - @EnvironmentObject private var midiManager: MIDIManager + @EnvironmentObject private var midiManager: ObservableMIDIManager @Binding public var selection: MIDIIdentifier? @Binding public var cachedSelectionName: String? @@ -189,7 +189,7 @@ public struct MIDIInputsList: View { /// SwiftUI `List` view for selecting MIDI output endpoints. @available(macOS 11.0, iOS 14.0, *) public struct MIDIOutputsList: View { - @EnvironmentObject private var midiManager: MIDIManager + @EnvironmentObject private var midiManager: ObservableMIDIManager @Binding public var selection: MIDIIdentifier? @Binding public var cachedSelectionName: String? diff --git a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift index 27e526bd83..5dfd63cc21 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift @@ -12,7 +12,7 @@ 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 + @EnvironmentObject private var midiManager: ObservableMIDIManager var title: String var endpoints: [Endpoint] @@ -165,7 +165,7 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent /// SwiftUI `Picker` view for selecting MIDI input endpoints. @available(macOS 11.0, iOS 14.0, *) public struct MIDIInputsPicker: View { - @EnvironmentObject private var midiManager: MIDIManager + @EnvironmentObject private var midiManager: ObservableMIDIManager public var title: String @Binding public var selection: MIDIIdentifier? @@ -202,7 +202,7 @@ public struct MIDIInputsPicker: View { /// SwiftUI `Picker` view for selecting MIDI output endpoints. @available(macOS 11.0, iOS 14.0, *) public struct MIDIOutputsPicker: View { - @EnvironmentObject private var midiManager: MIDIManager + @EnvironmentObject private var midiManager: ObservableMIDIManager public var title: String @Binding public var selection: MIDIIdentifier? From b74e0f6d326a97ae3293916cee0c4294117be1cc Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 16:18:10 -0800 Subject: [PATCH 07/64] Commented out `MIDIEndpointsProtocol` as it is no longer used --- .../MIDIEndpoints/MIDIEndpoints.swift | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift index d7474531c6..eaa3e8c9ae 100644 --- a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift +++ b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift @@ -12,31 +12,31 @@ import Foundation import Combine #endif -// 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 where Self: Equatable, Self: Hashable { - /// List of MIDI input endpoints in the system. - var inputs: [MIDIInputEndpoint] { get } - - /// List of MIDI input endpoints in the system omitting virtual endpoints owned by the - /// ``MIDIManager`` instance. - var inputsUnowned: [MIDIInputEndpoint] { get } - - /// List of MIDI output endpoints in the system. - var outputs: [MIDIOutputEndpoint] { get } - - /// List of MIDI output endpoints in the system omitting virtual endpoints owned by the - /// ``MIDIManager`` instance. - var outputsUnowned: [MIDIOutputEndpoint] { get } - - /// Manually update the locally cached contents from the system. - /// This method does not need to be manually invoked, as it is called automatically by the - /// ``MIDIManager`` when MIDI system endpoints change. - mutating func updateCachedProperties() -} +// // 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 where Self: Equatable, Self: Hashable { +// /// List of MIDI input endpoints in the system. +// var inputs: [MIDIInputEndpoint] { get } +// +// /// List of MIDI input endpoints in the system omitting virtual endpoints owned by the +// /// ``MIDIManager`` instance. +// var inputsUnowned: [MIDIInputEndpoint] { get } +// +// /// List of MIDI output endpoints in the system. +// var outputs: [MIDIOutputEndpoint] { get } +// +// /// List of MIDI output endpoints in the system omitting virtual endpoints owned by the +// /// ``MIDIManager`` instance. +// var outputsUnowned: [MIDIOutputEndpoint] { get } +// +// /// Manually update the locally cached contents from the system. +// /// This method does not need to be manually invoked, as it is called automatically by the +// /// ``MIDIManager`` when MIDI system endpoints change. +// mutating func updateCachedProperties() +// } /// Manages system MIDI endpoints information cache. -public final class MIDIEndpoints: NSObject, MIDIEndpointsProtocol { +public final class MIDIEndpoints: NSObject /* , MIDIEndpointsProtocol */ { /// Weak reference to ``MIDIManager``. weak var manager: MIDIManager? @@ -89,7 +89,7 @@ public final class MIDIEndpoints: NSObject, MIDIEndpointsProtocol { /// Manages system MIDI endpoints information cache. /// Class and properties are published for use in SwiftUI and Combine. @available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) -public final class MIDIObservableEndpoints: NSObject, ObservableObject, MIDIEndpointsProtocol { +public final class MIDIObservableEndpoints: NSObject, ObservableObject /* , MIDIEndpointsProtocol */ { /// Weak reference to ``MIDIManager``. internal weak var manager: MIDIManager? From b8fe68913b36b1a6bef6c4c16f016d855b633599 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 16:27:41 -0800 Subject: [PATCH 08/64] `MIDIEndpoints`: Refactored endpoints data fetching --- .../MIDIEndpoints/MIDIEndpoints.swift | 156 ++++++++++-------- 1 file changed, 84 insertions(+), 72 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift index eaa3e8c9ae..530ddcc910 100644 --- a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift +++ b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift @@ -12,75 +12,102 @@ import Foundation import Combine #endif -// // 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 where Self: Equatable, Self: Hashable { -// /// List of MIDI input endpoints in the system. -// var inputs: [MIDIInputEndpoint] { get } -// -// /// List of MIDI input endpoints in the system omitting virtual endpoints owned by the -// /// ``MIDIManager`` instance. -// var inputsUnowned: [MIDIInputEndpoint] { get } -// -// /// List of MIDI output endpoints in the system. -// var outputs: [MIDIOutputEndpoint] { get } -// -// /// List of MIDI output endpoints in the system omitting virtual endpoints owned by the -// /// ``MIDIManager`` instance. -// var outputsUnowned: [MIDIOutputEndpoint] { get } -// -// /// Manually update the locally cached contents from the system. -// /// This method does not need to be manually invoked, as it is called automatically by the -// /// ``MIDIManager`` when MIDI system endpoints change. -// 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 protocol MIDIEndpointsProtocol where Self: Equatable, Self: Hashable { + /// List of MIDI input endpoints in the system. + var inputs: [MIDIInputEndpoint] { get } - public internal(set) dynamic var outputs: [MIDIOutputEndpoint] = [] - public internal(set) dynamic var outputsUnowned: [MIDIOutputEndpoint] = [] + /// List of MIDI input endpoints in the system omitting virtual endpoints owned by the + /// ``MIDIManager`` instance. + var inputsUnowned: [MIDIInputEndpoint] { get } - override init() { - super.init() - } + /// List of MIDI output endpoints in the system. + var outputs: [MIDIOutputEndpoint] { get } - init(manager: MIDIManager) { - self.manager = manager - super.init() - } - - public func updateCachedProperties() { - inputs = getSystemDestinationEndpoints() + /// List of MIDI output endpoints in the system omitting virtual endpoints owned by the + /// ``MIDIManager`` instance. + var outputsUnowned: [MIDIOutputEndpoint] { get } + /// Manually update the locally cached contents from the system. + /// This method does not need to be manually invoked, as it is called automatically by the + /// ``MIDIManager`` when MIDI system endpoints change. + mutating func updateCachedProperties() +} + +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 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() + } + + init(manager: MIDIManager) { + self.manager = manager + super.init() + } + + /// 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() { + let fetched = _fetchProperties(manager: manager) + + inputs = fetched.inputs + inputsUnowned = fetched.inputsUnowned + outputs = fetched.outputs + outputsUnowned = fetched.outputsUnowned } } @@ -89,7 +116,7 @@ public final class MIDIEndpoints: NSObject /* , MIDIEndpointsProtocol */ { /// Manages system MIDI endpoints information cache. /// Class and properties are published for use in SwiftUI and Combine. @available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) -public final class MIDIObservableEndpoints: NSObject, ObservableObject /* , MIDIEndpointsProtocol */ { +public final class MIDIObservableEndpoints: NSObject, ObservableObject, MIDIEndpointsProtocol { /// Weak reference to ``MIDIManager``. internal weak var manager: MIDIManager? @@ -108,34 +135,19 @@ public final class MIDIObservableEndpoints: NSObject, ObservableObject /* , MIDI super.init() } + /// 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() { objectWillChange.send() - inputs = getSystemDestinationEndpoints() + let fetched = _fetchProperties(manager: manager) - if let manager = manager { - let managedInputsIDs = manager.managedInputs.values - .compactMap { $0.uniqueID } - - inputsUnowned = inputs.filter { - !managedInputsIDs.contains($0.uniqueID) - } - } else { - inputsUnowned = inputs - } - - outputs = getSystemSourceEndpoints() - - if let manager = manager { - let managedOutputsIDs = manager.managedOutputs.values - .compactMap { $0.uniqueID } - - outputsUnowned = outputs.filter { - !managedOutputsIDs.contains($0.uniqueID) - } - } else { - outputsUnowned = outputs - } + inputs = fetched.inputs + inputsUnowned = fetched.inputsUnowned + outputs = fetched.outputs + outputsUnowned = fetched.outputsUnowned } } From b749882314218e334d56892e0f4d7e53fcd1865b Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 16:35:28 -0800 Subject: [PATCH 09/64] Added `MIDIObservableDevices` --- .../MIDIKitIO/MIDIDevices/MIDIDevices.swift | 37 ++++++++++++++++--- .../MIDIKitIO/MIDIManager/MIDIManager.swift | 12 ++++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift b/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift index a3f32c0c7c..5d3ea4d517 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,7 @@ 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 { @@ -68,4 +69,30 @@ public final class MIDIDevices: NSObject, MIDIDevicesProtocol { } } +#if canImport(Combine) + +/// Manages system MIDI devices information cache. +/// Class and properties are published for use in SwiftUI and Combine. +/// +/// Do not instance this class directly. Instead, access the ``MIDIManager/devices`` property of +/// your central ``MIDIManager`` instance. +@available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) +public final class MIDIObservableDevices: NSObject, ObservableObject, MIDIDevicesProtocol { + @Published public internal(set) dynamic var devices: [MIDIDevice] = [] + + override internal init() { + super.init() + } + + /// 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() { + objectWillChange.send() + devices = getSystemDevices() + } +} +#endif + #endif diff --git a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift index c3578e195c..80348b8d76 100644 --- a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift @@ -77,7 +77,7 @@ public class MIDIManager: NSObject { } /// 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: MIDIEndpoints @@ -186,12 +186,18 @@ public class MIDIManager: NSObject { import Combine /// ``MIDIManager`` subclass that is observable in a SwiftUI or Combine context. +/// Two new properties are available: ``observableDevices`` and ``observableEndpoints``. @available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) public final class ObservableMIDIManager: MIDIManager, ObservableObject { // MARK: - Properties - /// Type-erased internal backing storage for ``observableEndpoints``. - @Published public var observableEndpoints = MIDIObservableEndpoints() + /// MIDI devices in the system. + /// This is an observable implementation of ``devices``. + @Published public internal(set) var observableDevices = MIDIObservableDevices() + + /// MIDI input and output endpoints in the system. + /// This is an observable implementation of ``endpoints``. + @Published public internal(set) var observableEndpoints = MIDIObservableEndpoints() // MARK: - Init From f32b4d5adc78c5a1dcea6ab3317d861010673009 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 16:39:58 -0800 Subject: [PATCH 10/64] `MIDIManager`: `updateObjectsCache()` now updates `observableDevices` --- Sources/MIDIKitIO/MIDIManager/MIDIManager.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift index 80348b8d76..eb7c7f8af2 100644 --- a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift @@ -233,6 +233,8 @@ public final class ObservableMIDIManager: MIDIManager, ObservableObject { public override func updateObjectsCache() { objectWillChange.send() super.updateObjectsCache() + + observableDevices.updateCachedProperties() observableEndpoints.updateCachedProperties() } } From ea0d0e991b4408021486ce6cdd5e932406bdd13e Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 16:45:39 -0800 Subject: [PATCH 11/64] `MIDIEndpointsProtocol`: Added custom Equatable and Hashable --- .../MIDIEndpoints/MIDIEndpoints.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift index 530ddcc910..c7b8e11fbb 100644 --- a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift +++ b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift @@ -33,6 +33,24 @@ public protocol MIDIEndpointsProtocol where Self: Equatable, Self: Hashable { mutating func updateCachedProperties() } +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 + } +} + +extension MIDIEndpointsProtocol /* : Hashable */ { + public func hash(into hasher: inout Hasher) { + hasher.combine(inputs) + hasher.combine(inputsUnowned) + hasher.combine(outputs) + hasher.combine(outputsUnowned) + } +} + extension MIDIEndpointsProtocol { internal func _fetchProperties(manager: MIDIManager?) -> ( inputs: [MIDIInputEndpoint], From 8d014efa46c8564174b9a0b1d948d488758dd644 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 16:45:56 -0800 Subject: [PATCH 12/64] `MIDIDevicesProtocol`: Added custom Equatable and Hashable --- Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift b/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift index 5d3ea4d517..c92576734c 100644 --- a/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift +++ b/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift @@ -25,6 +25,18 @@ public protocol MIDIDevicesProtocol where Self: Equatable, Self: Hashable { 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 { /// Returns a dictionary keyed by device with value of an array containing all the input /// endpoints for the device. (Convenience) From feca32ccc79bdcd22d3700c1fc6113de904589ce Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 18:13:50 -0800 Subject: [PATCH 13/64] Refactored `MIDIDevices` and `MIDIEndpoints` as structs --- .../MIDIKitIO/MIDIDevices/MIDIDevices.swift | 36 +---------- .../MIDIEndpoints/MIDIEndpoints.swift | 61 +++---------------- .../MIDIKitIO/MIDIManager/MIDIManager.swift | 12 ++-- 3 files changed, 17 insertions(+), 92 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift b/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift index c92576734c..885b4755ed 100644 --- a/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift +++ b/Sources/MIDIKitIO/MIDIDevices/MIDIDevices.swift @@ -65,46 +65,16 @@ 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() - } - - /// 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() { - devices = getSystemDevices() - } -} - -#if canImport(Combine) - -/// Manages system MIDI devices information cache. -/// Class and properties are published for use in SwiftUI and Combine. -/// -/// Do not instance this class directly. Instead, access the ``MIDIManager/devices`` property of -/// your central ``MIDIManager`` instance. -@available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) -public final class MIDIObservableDevices: NSObject, ObservableObject, MIDIDevicesProtocol { - @Published public internal(set) dynamic var devices: [MIDIDevice] = [] - - override internal 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() { - objectWillChange.send() + public mutating func updateCachedProperties() { devices = getSystemDevices() } } -#endif #endif diff --git a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift index c7b8e11fbb..461576d843 100644 --- a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift +++ b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpoints.swift @@ -96,70 +96,25 @@ extension MIDIEndpointsProtocol { } /// Manages system MIDI endpoints information cache. -public final class MIDIEndpoints: NSObject, MIDIEndpointsProtocol { +public struct MIDIEndpoints: 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) var inputs: [MIDIInputEndpoint] = [] + public internal(set) var inputsUnowned: [MIDIInputEndpoint] = [] - public internal(set) dynamic var outputs: [MIDIOutputEndpoint] = [] - public internal(set) dynamic var outputsUnowned: [MIDIOutputEndpoint] = [] + public internal(set) var outputs: [MIDIOutputEndpoint] = [] + public internal(set) var outputsUnowned: [MIDIOutputEndpoint] = [] - override init() { - super.init() - } - - init(manager: MIDIManager) { - self.manager = manager - super.init() - } - - /// 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() { - let fetched = _fetchProperties(manager: manager) - - inputs = fetched.inputs - inputsUnowned = fetched.inputsUnowned - outputs = fetched.outputs - outputsUnowned = fetched.outputsUnowned - } -} - -#if canImport(Combine) - -/// Manages system MIDI endpoints information cache. -/// Class and properties are published for use in SwiftUI and Combine. -@available(macOS 10.15, macCatalyst 13, iOS 13, /* tvOS 13, watchOS 6, */ *) -public final class MIDIObservableEndpoints: NSObject, ObservableObject, MIDIEndpointsProtocol { - /// Weak reference to ``MIDIManager``. - internal weak var manager: MIDIManager? - - @Published public internal(set) dynamic var inputs: [MIDIInputEndpoint] = [] - @Published public internal(set) dynamic var inputsUnowned: [MIDIInputEndpoint] = [] - - @Published public internal(set) dynamic var outputs: [MIDIOutputEndpoint] = [] - @Published public internal(set) dynamic var outputsUnowned: [MIDIOutputEndpoint] = [] - - override internal init() { - super.init() - } - - internal init(manager: MIDIManager?) { + init(manager: MIDIManager?) { self.manager = manager - super.init() } /// 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() { - objectWillChange.send() - + public mutating func updateCachedProperties() { let fetched = _fetchProperties(manager: manager) inputs = fetched.inputs @@ -170,5 +125,3 @@ public final class MIDIObservableEndpoints: NSObject, ObservableObject, MIDIEndp } #endif - -#endif diff --git a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift index eb7c7f8af2..98310572c2 100644 --- a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift @@ -142,7 +142,7 @@ public class MIDIManager: NSObject { self.notificationHandler = notificationHandler // endpoints - endpoints = MIDIEndpoints() + endpoints = MIDIEndpoints(manager: nil) super.init() @@ -176,7 +176,7 @@ public class MIDIManager: NSObject { } /// Internal: updates cached properties for all objects. - dynamic func updateObjectsCache() { + func updateObjectsCache() { devices.updateCachedProperties() endpoints.updateCachedProperties() } @@ -193,11 +193,13 @@ public final class ObservableMIDIManager: MIDIManager, ObservableObject { /// MIDI devices in the system. /// This is an observable implementation of ``devices``. - @Published public internal(set) var observableDevices = MIDIObservableDevices() + @Published + public internal(set) var observableDevices = MIDIDevices() /// MIDI input and output endpoints in the system. /// This is an observable implementation of ``endpoints``. - @Published public internal(set) var observableEndpoints = MIDIObservableEndpoints() + @Published + public internal(set) var observableEndpoints = MIDIEndpoints(manager: nil) // MARK: - Init @@ -231,7 +233,7 @@ public final class ObservableMIDIManager: MIDIManager, ObservableObject { } public override func updateObjectsCache() { - objectWillChange.send() + // objectWillChange.send() // redundant since all local properties are marked @Published super.updateObjectsCache() observableDevices.updateCachedProperties() From 64cef5738f78aac78aea907ea28aa2396f6b6df4 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 18:36:10 -0800 Subject: [PATCH 14/64] `ObservableMIDIManager`: Refactored `notificationHandler` to pass a typed reference to the subclass --- .../MIDIKitIO/MIDIManager/MIDIManager.swift | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift index 98310572c2..4eaf7edee1 100644 --- a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift @@ -83,10 +83,13 @@ public class MIDIManager: NSObject { 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? @@ -112,10 +115,7 @@ public class MIDIManager: NSObject { clientName: String, model: String, manufacturer: String, - notificationHandler: (( - _ notification: MIDIIONotification, - _ manager: MIDIManager - ) -> Void)? = nil + notificationHandler: NotificationHandler? = nil ) { // API version preferredAPI = .bestForPlatform() @@ -201,6 +201,12 @@ public final class ObservableMIDIManager: MIDIManager, ObservableObject { @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). @@ -217,16 +223,25 @@ public final class ObservableMIDIManager: MIDIManager, ObservableObject { clientName: String, model: String, manufacturer: String, - notificationHandler: (( - _ notification: MIDIIONotification, - _ manager: MIDIManager - ) -> Void)? = nil + 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: notificationHandler + notificationHandler: notificationHandlerWrapper ) observableEndpoints.manager = self From e2d9adbf4786eb6494232d988e3077590d1a117b Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 18:37:30 -0800 Subject: [PATCH 15/64] File organization --- .../MIDIKitIO/MIDIManager/MIDIManager.swift | 75 ---------------- .../MIDIManager/ObservableMIDIManager.swift | 88 +++++++++++++++++++ 2 files changed, 88 insertions(+), 75 deletions(-) create mode 100644 Sources/MIDIKitIO/MIDIManager/ObservableMIDIManager.swift diff --git a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift index 4eaf7edee1..2c28d478f6 100644 --- a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift @@ -182,79 +182,4 @@ public class MIDIManager: NSObject { } } -#if canImport(Combine) -import Combine - -/// ``MIDIManager`` subclass that is observable in a SwiftUI or Combine context. -/// Two new properties are available: ``observableDevices`` and ``observableEndpoints``. -@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 ``devices``. - @Published - public internal(set) var observableDevices = MIDIDevices() - - /// MIDI input and output endpoints in the system. - /// This is an observable implementation of ``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/MIDIManager/ObservableMIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/ObservableMIDIManager.swift new file mode 100644 index 0000000000..aa21d2a108 --- /dev/null +++ b/Sources/MIDIKitIO/MIDIManager/ObservableMIDIManager.swift @@ -0,0 +1,88 @@ +// +// 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``. +@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 ``devices``. + @Published + public internal(set) var observableDevices = MIDIDevices() + + /// MIDI input and output endpoints in the system. + /// This is an observable implementation of ``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 From 1885d666eb4f158f496d55404b2621e257d248a4 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 18:41:07 -0800 Subject: [PATCH 16/64] Updated `EndpointPickers` example project to use fallback display name --- .../EndpointPickers.xcodeproj/project.pbxproj | 34 +-- .../xcschemes/EndpointPickers.xcscheme | 9 +- .../EndpointPickers/EndpointPickersApp.swift | 90 +++++--- .../EndpointPickers/MIDIHelper.swift | 121 ++++++----- .../EndpointPickers/Utilities.swift | 14 ++ .../EndpointPickers/iOS/ContentView-iOS.swift | 45 ++-- .../iOS/MIDIEndpointSelectionView-iOS.swift | 30 ++- .../macOS/ContentView-macOS.swift | 202 ++++++++++-------- .../macOS/EndpointPickers.entitlements | 6 +- .../MIDIEndpointSelectionView-macOS.swift | 32 ++- .../EndpointPickers/README.md | 20 +- 11 files changed, 370 insertions(+), 233 deletions(-) diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj index 2e2a551620..11e2fe82e9 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -30,7 +30,6 @@ 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 */ @@ -88,7 +87,6 @@ E27D0E61284F3FB600F43247 /* EndpointPickers */ = { isa = PBXGroup; children = ( - E2841ABA2989CAF5006907BD /* EndpointPickers.entitlements */, E2496A892989087F003FD165 /* iOS */, E2496A8D2989087F003FD165 /* macOS */, E27D0E62284F3FB600F43247 /* EndpointPickersApp.swift */, @@ -131,7 +129,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E27D0E5E284F3FB600F43247 = { CreatedOnToolsVersion = 14.0; @@ -192,6 +190,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 +223,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; @@ -253,6 +253,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 +286,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; @@ -309,7 +311,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 +319,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 +349,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 +357,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", @@ -408,8 +410,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + branch = endpoints; + kind = branch; }; }; /* End XCRemoteSwiftPackageReference section */ 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..85b7277d9e 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift @@ -10,7 +10,7 @@ import MIDIKitIO import SwiftUI struct ContentView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @EnvironmentObject var midiHelper: MIDIHelper @Binding var midiInSelectedID: MIDIIdentifier @@ -27,7 +27,7 @@ struct ContentView: View { InfoView() } } - + Section() { MIDIEndpointSelectionView( midiInSelectedID: $midiInSelectedID, @@ -35,53 +35,50 @@ struct ContentView: View { midiOutSelectedID: $midiOutSelectedID, midiOutSelectedDisplayName: $midiOutSelectedDisplayName ) - + Group { Button("Send Note On C3") { - sendToConnection(.noteOn(60, velocity: .midi1(127), channel: 0)) + 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(64), channel: 0)) + sendToConnection(.cc(1, value: .midi1(UInt7.random()), channel: 0)) } } - .disabled( - midiOutSelectedID == .invalidMIDIIdentifier || - !midiManager.endpoints.inputs.contains(whereUniqueID: midiOutSelectedID) - ) + .disabled(isMIDIOutDisabled) } - + 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)) + 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(64), channel: 0)) + sendToVirtuals(.cc(1, value: .midi1(UInt7.random()), channel: 0)) } } .disabled(!midiHelper.virtualsExist) } - + Section(header: Text("Received Events")) { Toggle( "Filter Active Sensing and Clock", @@ -109,6 +106,16 @@ struct ContentView: View { .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .padding() } +} + +extension ContentView { + private var isMIDIOutDisabled: Bool { + midiOutSelectedID == .invalidMIDIIdentifier || + !midiManager.observableEndpoints.inputs.contains( + whereUniqueID: midiOutSelectedID, + fallbackDisplayName: midiOutSelectedDisplayName + ) + } func sendToConnection(_ event: MIDIEvent) { try? midiHelper.midiOutputConnection?.send(event: event) 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..42fbd5b463 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift @@ -10,7 +10,7 @@ import MIDIKitIO import SwiftUI struct ContentView: View { - @EnvironmentObject var midiManager: MIDIManager + @EnvironmentObject var midiManager: ObservableMIDIManager @EnvironmentObject var midiHelper: MIDIHelper @Binding var midiInSelectedID: MIDIIdentifier @@ -21,112 +21,136 @@ struct ContentView: View { 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")) { + MIDIInSelectionView( + midiInSelectedID: $midiInSelectedID, + midiInSelectedDisplayName: $midiInSelectedDisplayName + ) + .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")) { + MIDIOutSelectionView( + midiOutSelectedID: $midiOutSelectedID, + midiOutSelectedDisplayName: $midiOutSelectedDisplayName + ) + .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)) + } - // 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 Note Off C3") { + sendToVirtuals(.noteOff(60, velocity: .midi1(0), channel: 0)) + } + + 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 || + !midiManager.observableEndpoints.inputs.contains( + whereUniqueID: midiOutSelectedID, + fallbackDisplayName: midiOutSelectedDisplayName + ) + } + 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..72597408b5 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/README.md +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/README.md @@ -4,35 +4,31 @@ 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. > 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. - - 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 From 10d2715c73967593400114956d533f1be0cb2d95 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 19:32:28 -0800 Subject: [PATCH 17/64] Updated docs --- .../MIDIKitIO-Receiving-MIDI-Events.md | 1 + .../MIDIKit.docc/MIDIKitIO/MIDIKitIO.md | 3 +- .../MIDIIdentifierPersistence.swift | 2 +- .../MIDIKitIO-Receiving-MIDI-Events.md | 1 + Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO.md | 1 + .../MIDIKitIO/MIDIManager/MIDIManager.swift | 5 +++ .../MIDIManager/ObservableMIDIManager.swift | 43 ++++++++++++++++++- .../MIDIReceiverOptions.swift | 6 +++ 8 files changed, 58 insertions(+), 4 deletions(-) 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..033cf05b3f 100644 --- a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO.md +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO.md @@ -15,6 +15,7 @@ To add additional functionality, import extension modules or import the MIDIKit ### Manager - ``MIDIManager`` +- ``ObservableMIDIManager`` - - - @@ -46,4 +47,4 @@ To add additional functionality, import extension modules or import the MIDIKit ### Internals -- \ No newline at end of file +- 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-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..f9931a3e95 100644 --- a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO.md +++ b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO.md @@ -15,6 +15,7 @@ To add additional functionality, import extension modules or import the MIDIKit ### Manager - ``MIDIManager`` +- ``ObservableMIDIManager`` - - - diff --git a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift index 2c28d478f6..ec79e59f2c 100644 --- a/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/MIDIManager.swift @@ -13,6 +13,11 @@ 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. +/// +/// > Tip: +/// > +/// > For SwiftUI and Combine environments, see the ``ObservableMIDIManager`` subclass which adds +/// > published devices and endpoints properties. public class MIDIManager: NSObject { // MARK: - Properties diff --git a/Sources/MIDIKitIO/MIDIManager/ObservableMIDIManager.swift b/Sources/MIDIKitIO/MIDIManager/ObservableMIDIManager.swift index aa21d2a108..d40aeeff44 100644 --- a/Sources/MIDIKitIO/MIDIManager/ObservableMIDIManager.swift +++ b/Sources/MIDIKitIO/MIDIManager/ObservableMIDIManager.swift @@ -14,17 +14,56 @@ 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 ``devices``. + /// 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 ``endpoints``. + /// This is an observable implementation of ``MIDIManager/endpoints``. @Published public internal(set) var observableEndpoints = MIDIEndpoints(manager: nil) 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) { From b35afe1b9869d6d8c262fc969192892c467f3fb5 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 19:33:37 -0800 Subject: [PATCH 18/64] Refactored `EndpointPickers` example project --- .../EndpointPickers/iOS/ContentView-iOS.swift | 196 ++++++++++-------- 1 file changed, 105 insertions(+), 91 deletions(-) diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift index 85b7277d9e..7b2ecb33a4 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift @@ -22,89 +22,121 @@ struct ContentView: View { var body: some View { NavigationView { Form { - Section() { - NavigationLink("Info") { - InfoView() - } + infoSection + + endpointSelectionSection + + virtualEndpointsSection + + eventLogSection + } + .navigationBarTitle("Endpoint Pickers") + + infoView + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .padding() + } + + private var infoSection: some View { + Section() { + NavigationLink("Info") { + infoView + } + } + } + + 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) + } + + private var endpointSelectionSection: some View { + Section() { + MIDIEndpointSelectionView( + midiInSelectedID: $midiInSelectedID, + midiInSelectedDisplayName: $midiInSelectedDisplayName, + midiOutSelectedID: $midiOutSelectedID, + midiOutSelectedDisplayName: $midiOutSelectedDisplayName + ) + + Group { + Button("Send Note On C3") { + sendToConnection(.noteOn(60, velocity: .midi1(UInt7.random(in: 20 ... 127)), channel: 0)) } - Section() { - MIDIEndpointSelectionView( - midiInSelectedID: $midiInSelectedID, - midiInSelectedDisplayName: $midiInSelectedDisplayName, - midiOutSelectedID: $midiOutSelectedID, - midiOutSelectedDisplayName: $midiOutSelectedDisplayName - ) - - Group { - 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(isMIDIOutDisabled) + Button("Send Note Off C3") { + sendToConnection(.noteOff(60, velocity: .midi1(0), 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(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)) - } - } - .disabled(!midiHelper.virtualsExist) + Button("Send CC1") { + sendToConnection(.cc(1, value: .midi1(UInt7.random()), channel: 0)) + } + } + .disabled(isMIDIOutDisabled) + } + } + + 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)) } - 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])) - } + 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 + ) - InfoView() + 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(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) - .padding() } } @@ -136,22 +168,4 @@ extension ContentView { } } -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 From 06793f4b36a78a1a9782cd15ee5c281cedf1c286 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 19:46:51 -0800 Subject: [PATCH 19/64] Updated `EndpointPickers` example project README --- .../EndpointPickers/README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/README.md b/Examples/SwiftUI Multiplatform/EndpointPickers/README.md index 72597408b5..1c93db0c71 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/README.md +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/README.md @@ -5,22 +5,30 @@ This example demonstrates best practises when creating MIDI input and output sel ## Key Features - 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.observableEndpoints.inputs` or `midiManager.observableEndpoints.outputs` > Changes in MIDI endpoints in the system will trigger these arrays to refresh your view. > - `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. + +- 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. > - > 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. + > 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 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 From 03797e72d881545210171f3d99959cb740ea4045 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 20:00:08 -0800 Subject: [PATCH 20/64] Updated `EventParsing` example project --- .../EventParsing.xcodeproj/project.pbxproj | 20 +++-- .../xcschemes/EventParsing.xcscheme | 84 +++++++++++++++++++ .../EventParsing/ContentView.swift | 2 + .../EventParsing/EventParsingApp.swift | 4 +- .../EventParsing/MIDIHelper.swift | 6 +- 5 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/xcshareddata/xcschemes/EventParsing.xcscheme diff --git a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj index 0cee04a2c4..23f5f4b08c 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", @@ -374,8 +376,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + branch = endpoints; + kind = branch; }; }; 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 { From 7134c071334497e054a1aa2efddc3ebe10b2a1af Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 21:34:37 -0800 Subject: [PATCH 21/64] Updated docs --- Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Devices.md | 1 + Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Endpoints.md | 3 +++ Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Devices.md | 1 + Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Endpoints.md | 3 +++ 4 files changed, 8 insertions(+) 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/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`` From c911d6417b90907e1d0cf26b4a6d117316a39322 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 22:40:06 -0800 Subject: [PATCH 22/64] `MIDIEndpointFilter`: refactored and fixed Collection filter methods --- .../API Evolution/MIDIKit-0.9.4.swift | 30 ++++ .../MIDIEndpoints/MIDIEndpointFilter.swift | 158 +++++++++++++++++- Sources/MIDIKitUI/MIDIEndpointsList.swift | 47 ++++-- Sources/MIDIKitUI/MIDIEndpointsPicker.swift | 51 +++--- 4 files changed, 241 insertions(+), 45 deletions(-) create mode 100644 Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.4.swift 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/MIDIEndpoints/MIDIEndpointFilter.swift b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpointFilter.swift index 3f17e88a02..bfd0c1d54b 100644 --- a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpointFilter.swift +++ b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpointFilter.swift @@ -77,25 +77,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( - using endpointFilter: MIDIEndpointFilter, - in manager: MIDIManager, - isIncluded: Bool = true + _ 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( + _ 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/MIDIKitUI/MIDIEndpointsList.swift b/Sources/MIDIKitUI/MIDIEndpointsList.swift index 4b4aa84b0c..50ba6699d6 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsList.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsList.swift @@ -15,7 +15,7 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent @EnvironmentObject private var midiManager: ObservableMIDIManager var endpoints: [Endpoint] - @State var filter: MIDIEndpointFilter + var maskedFilter: MIDIEndpointMaskedFilter? @Binding var selection: MIDIIdentifier? @Binding var cachedSelectionName: String? let showIcons: Bool @@ -24,18 +24,16 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent init( endpoints: [Endpoint], - filter: MIDIEndpointFilter, + maskedFilter: MIDIEndpointMaskedFilter?, selection: Binding, cachedSelectionName: Binding, showIcons: Bool ) { self.endpoints = endpoints - _filter = State(initialValue: filter) + self.maskedFilter = maskedFilter _selection = selection _cachedSelectionName = cachedSelectionName self.showIcons = showIcons - // set up initial data, but skip filter because midiManager is not available yet - _ids = State(initialValue: generateIDs(endpoints: endpoints, filtered: false)) } public var body: some View { @@ -52,7 +50,7 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent .onAppear { updateIDs(endpoints: endpoints) } - .onChange(of: filter) { newValue in + .onChange(of: maskedFilter) { newValue in updateIDs(endpoints: endpoints) } .onChange(of: endpoints) { newValue in @@ -74,10 +72,15 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent endpoints: [Endpoint], filtered: Bool = true ) -> [MIDIIdentifier] { - let endpointIDs = ( - filtered ? endpoints.filter(using: filter, in: midiManager) : endpoints - ) - .map(\.id) + var endpointIDs: [MIDIIdentifier] = [] + if filtered, let maskedFilter = maskedFilter { + endpointIDs = endpoints + .filter(maskedFilter, in: midiManager) + .map(\.id) + } else { + endpointIDs = endpoints + .map(\.id) + } if let selection, !endpointIDs.contains(selection) { return [selection] + endpointIDs @@ -159,8 +162,8 @@ public struct MIDIInputsList: View { @Binding public var selection: MIDIIdentifier? @Binding public var cachedSelectionName: String? - public let showIcons: Bool - public let filterOwned: Bool + public var showIcons: Bool + public var filterOwned: Bool public init( selection: Binding, @@ -176,14 +179,18 @@ public struct MIDIInputsList: View { public var body: some View { MIDIEndpointsList( - endpoints: midiManager.endpoints.inputs, - filter: filterOwned ? .owned() : .default(), + endpoints: midiManager.observableEndpoints.inputs, + maskedFilter: maskedFilter, selection: $selection, cachedSelectionName: $cachedSelectionName, showIcons: showIcons ) Text("Selected: \(cachedSelectionName ?? "None")") } + + private var maskedFilter: MIDIEndpointMaskedFilter { + .drop(filterOwned ? .owned() : .default()) + } } /// SwiftUI `List` view for selecting MIDI output endpoints. @@ -193,8 +200,8 @@ public struct MIDIOutputsList: View { @Binding public var selection: MIDIIdentifier? @Binding public var cachedSelectionName: String? - public let showIcons: Bool - public let filterOwned: Bool + public var showIcons: Bool + public var filterOwned: Bool public init( selection: Binding, @@ -210,14 +217,18 @@ public struct MIDIOutputsList: View { public var body: some View { MIDIEndpointsList( - endpoints: midiManager.endpoints.outputs, - filter: filterOwned ? .owned() : .default(), + endpoints: midiManager.observableEndpoints.outputs, + maskedFilter: maskedFilter, selection: $selection, cachedSelectionName: $cachedSelectionName, showIcons: showIcons ) Text("Selected: \(cachedSelectionName ?? "None")") } + + private var maskedFilter: MIDIEndpointMaskedFilter { + .drop(filterOwned ? .owned() : .default()) + } } #endif diff --git a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift index 5dfd63cc21..4d59b08497 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift @@ -14,31 +14,29 @@ struct MIDIEndpointsPicker: View where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdentifier { @EnvironmentObject private var midiManager: ObservableMIDIManager - var title: String + let title: String var endpoints: [Endpoint] - @State var filter: MIDIEndpointFilter + var maskedFilter: MIDIEndpointMaskedFilter? @Binding var selection: MIDIIdentifier? @Binding var cachedSelectionName: String? - let showIcons: Bool + var showIcons: Bool @State private var ids: [MIDIIdentifier] = [] init( title: String, endpoints: [Endpoint], - filter: MIDIEndpointFilter, + maskedFilter: MIDIEndpointMaskedFilter?, selection: Binding, cachedSelectionName: Binding, showIcons: Bool ) { self.title = title self.endpoints = endpoints - _filter = State(initialValue: filter) + self.maskedFilter = maskedFilter _selection = selection _cachedSelectionName = cachedSelectionName self.showIcons = showIcons - // set up initial data, but skip filter because midiManager is not available yet - _ids = State(initialValue: generateIDs(endpoints: endpoints, filtered: false)) } public var body: some View { @@ -58,7 +56,7 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent .onAppear { updateIDs(endpoints: endpoints) } - .onChange(of: filter) { newValue in + .onChange(of: maskedFilter) { newValue in updateIDs(endpoints: endpoints) } .onChange(of: endpoints) { newValue in @@ -80,10 +78,15 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent endpoints: [Endpoint], filtered: Bool = true ) -> [MIDIIdentifier] { - let endpointIDs = ( - filtered ? endpoints.filter(using: filter, in: midiManager) : endpoints - ) - .map(\.id) + var endpointIDs: [MIDIIdentifier] = [] + if filtered, let maskedFilter = maskedFilter { + endpointIDs = endpoints + .filter(maskedFilter, in: midiManager) + .map(\.id) + } else { + endpointIDs = endpoints + .map(\.id) + } if let selection, !endpointIDs.contains(selection) { return [selection] + endpointIDs @@ -170,8 +173,8 @@ public struct MIDIInputsPicker: View { public var title: String @Binding public var selection: MIDIIdentifier? @Binding public var cachedSelectionName: String? - public let showIcons: Bool - public let filterOwned: Bool + public var showIcons: Bool + public var filterOwned: Bool public init( title: String, @@ -190,13 +193,17 @@ public struct MIDIInputsPicker: View { public var body: some View { MIDIEndpointsPicker( title: title, - endpoints: midiManager.endpoints.inputs, - filter: filterOwned ? .owned() : .default(), + endpoints: midiManager.observableEndpoints.inputs, + maskedFilter: maskedFilter, selection: $selection, cachedSelectionName: $cachedSelectionName, showIcons: showIcons ) } + + private var maskedFilter: MIDIEndpointMaskedFilter { + .drop(filterOwned ? .owned() : .default()) + } } /// SwiftUI `Picker` view for selecting MIDI output endpoints. @@ -207,8 +214,8 @@ public struct MIDIOutputsPicker: View { public var title: String @Binding public var selection: MIDIIdentifier? @Binding public var cachedSelectionName: String? - public let showIcons: Bool - public let filterOwned: Bool + public var showIcons: Bool + public var filterOwned: Bool public init( title: String, @@ -227,13 +234,17 @@ public struct MIDIOutputsPicker: View { public var body: some View { MIDIEndpointsPicker( title: title, - endpoints: midiManager.endpoints.outputs, - filter: filterOwned ? .owned() : .default(), + endpoints: midiManager.observableEndpoints.outputs, + maskedFilter: maskedFilter, selection: $selection, cachedSelectionName: $cachedSelectionName, showIcons: showIcons ) } + + private var maskedFilter: MIDIEndpointMaskedFilter { + .drop(filterOwned ? .owned() : .default()) + } } #endif From a69f40591c61f028ba0d755e9d78a96ceb017f16 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 22:40:49 -0800 Subject: [PATCH 23/64] `MIDIEndpointFilter`: Added `apply(to:mask:in:)` method --- .../MIDIEndpoints/MIDIEndpointFilter.swift | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpointFilter.swift b/Sources/MIDIKitIO/MIDIEndpoints/MIDIEndpointFilter.swift index bfd0c1d54b..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 { From c4a00459b5419c15469e364d93f5658d1d1370f4 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 22:41:03 -0800 Subject: [PATCH 24/64] Updated unit tests --- .../MIDIEndpointFilter Tests.swift | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 Tests/MIDIKitIOTests/MIDIEndpoint/MIDIEndpointFilter Tests.swift 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 From 49b9303fa6630d46288d3ec585c3599eac319ca0 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 23:13:53 -0800 Subject: [PATCH 25/64] Refactored `MIDIEndpointsList` and resolved issues --- Sources/MIDIKitUI/MIDIEndpointsList.swift | 56 +++++++++++++---------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/Sources/MIDIKitUI/MIDIEndpointsList.swift b/Sources/MIDIKitUI/MIDIEndpointsList.swift index 50ba6699d6..1c78f2bafb 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsList.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsList.swift @@ -12,7 +12,7 @@ import SwiftUI @available(macOS 11.0, iOS 14.0, *) struct MIDIEndpointsList: View where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdentifier { - @EnvironmentObject private var midiManager: ObservableMIDIManager + private weak var midiManager: ObservableMIDIManager? var endpoints: [Endpoint] var maskedFilter: MIDIEndpointMaskedFilter? @@ -27,13 +27,18 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent maskedFilter: MIDIEndpointMaskedFilter?, selection: Binding, cachedSelectionName: Binding, - showIcons: Bool + showIcons: Bool, + midiManager: ObservableMIDIManager? ) { self.endpoints = endpoints self.maskedFilter = maskedFilter _selection = selection _cachedSelectionName = cachedSelectionName self.showIcons = showIcons + self.midiManager = midiManager + + // pre-populate IDs + _ids = State(initialValue: generateIDs(endpoints: endpoints, maskedFilter: maskedFilter)) } public var body: some View { @@ -48,17 +53,17 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent } } .onAppear { - updateIDs(endpoints: endpoints) + updateIDs(endpoints: endpoints, maskedFilter: maskedFilter) } .onChange(of: maskedFilter) { newValue in - updateIDs(endpoints: endpoints) + updateIDs(endpoints: endpoints, maskedFilter: newValue) } .onChange(of: endpoints) { newValue in - updateIDs(endpoints: newValue) + updateIDs(endpoints: newValue, maskedFilter: maskedFilter) } .onChange(of: selection) { newValue in - updateIDs(endpoints: endpoints) - guard let selection else { + updateIDs(endpoints: endpoints, maskedFilter: maskedFilter) + guard let selection = newValue else { cachedSelectionName = nil return } @@ -70,10 +75,10 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent private func generateIDs( endpoints: [Endpoint], - filtered: Bool = true + maskedFilter: MIDIEndpointMaskedFilter? ) -> [MIDIIdentifier] { var endpointIDs: [MIDIIdentifier] = [] - if filtered, let maskedFilter = maskedFilter { + if let maskedFilter = maskedFilter, let midiManager = midiManager { endpointIDs = endpoints .filter(maskedFilter, in: midiManager) .map(\.id) @@ -90,8 +95,11 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent } /// (Don't run from init.) - private func updateIDs(endpoints: [Endpoint]) { - ids = generateIDs(endpoints: endpoints) + private func updateIDs( + endpoints: [Endpoint], + maskedFilter: MIDIEndpointMaskedFilter? + ) { + ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter) } private func endpoint(for id: MIDIIdentifier) -> Endpoint? { @@ -163,18 +171,18 @@ public struct MIDIInputsList: View { @Binding public var selection: MIDIIdentifier? @Binding public var cachedSelectionName: String? public var showIcons: Bool - public var filterOwned: Bool + public var hideOwned: Bool public init( selection: Binding, cachedSelectionName: Binding, showIcons: Bool = true, - filterOwned: Bool = false + hideOwned: Bool = false ) { _selection = selection _cachedSelectionName = cachedSelectionName self.showIcons = showIcons - self.filterOwned = filterOwned + self.hideOwned = hideOwned } public var body: some View { @@ -183,13 +191,14 @@ public struct MIDIInputsList: View { maskedFilter: maskedFilter, selection: $selection, cachedSelectionName: $cachedSelectionName, - showIcons: showIcons + showIcons: showIcons, + midiManager: midiManager ) Text("Selected: \(cachedSelectionName ?? "None")") } - private var maskedFilter: MIDIEndpointMaskedFilter { - .drop(filterOwned ? .owned() : .default()) + private var maskedFilter: MIDIEndpointMaskedFilter? { + hideOwned ? .drop(.owned()) : nil } } @@ -201,18 +210,18 @@ public struct MIDIOutputsList: View { @Binding public var selection: MIDIIdentifier? @Binding public var cachedSelectionName: String? public var showIcons: Bool - public var filterOwned: Bool + public var hideOwned: Bool public init( selection: Binding, cachedSelectionName: Binding, showIcons: Bool = true, - filterOwned: Bool = false + hideOwned: Bool = false ) { _selection = selection _cachedSelectionName = cachedSelectionName self.showIcons = showIcons - self.filterOwned = filterOwned + self.hideOwned = hideOwned } public var body: some View { @@ -221,13 +230,14 @@ public struct MIDIOutputsList: View { maskedFilter: maskedFilter, selection: $selection, cachedSelectionName: $cachedSelectionName, - showIcons: showIcons + showIcons: showIcons, + midiManager: midiManager ) Text("Selected: \(cachedSelectionName ?? "None")") } - private var maskedFilter: MIDIEndpointMaskedFilter { - .drop(filterOwned ? .owned() : .default()) + private var maskedFilter: MIDIEndpointMaskedFilter? { + hideOwned ? .drop(.owned()) : nil } } From 7efe4ab60d9a4a41b2cea7e9c66e1a830f380701 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 23:14:08 -0800 Subject: [PATCH 26/64] Refactored `MIDIEndpointsPicker` and resolved issues --- Sources/MIDIKitUI/MIDIEndpointsPicker.swift | 56 ++++++++++++--------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift index 4d59b08497..3867d890a6 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift @@ -12,7 +12,7 @@ import SwiftUI @available(macOS 11.0, iOS 14.0, *) struct MIDIEndpointsPicker: View where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdentifier { - @EnvironmentObject private var midiManager: ObservableMIDIManager + private weak var midiManager: ObservableMIDIManager? let title: String var endpoints: [Endpoint] @@ -29,7 +29,8 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent maskedFilter: MIDIEndpointMaskedFilter?, selection: Binding, cachedSelectionName: Binding, - showIcons: Bool + showIcons: Bool, + midiManager: ObservableMIDIManager? ) { self.title = title self.endpoints = endpoints @@ -37,6 +38,10 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent _selection = selection _cachedSelectionName = cachedSelectionName self.showIcons = showIcons + self.midiManager = midiManager + + // pre-populate IDs + _ids = State(initialValue: generateIDs(endpoints: endpoints, maskedFilter: maskedFilter)) } public var body: some View { @@ -54,17 +59,17 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent } } .onAppear { - updateIDs(endpoints: endpoints) + updateIDs(endpoints: endpoints, maskedFilter: maskedFilter) } .onChange(of: maskedFilter) { newValue in - updateIDs(endpoints: endpoints) + updateIDs(endpoints: endpoints, maskedFilter: newValue) } .onChange(of: endpoints) { newValue in - updateIDs(endpoints: newValue) + updateIDs(endpoints: newValue, maskedFilter: maskedFilter) } .onChange(of: selection) { newValue in - updateIDs(endpoints: endpoints) - guard let selection else { + updateIDs(endpoints: endpoints, maskedFilter: maskedFilter) + guard let selection = newValue else { cachedSelectionName = nil return } @@ -76,10 +81,10 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent private func generateIDs( endpoints: [Endpoint], - filtered: Bool = true + maskedFilter: MIDIEndpointMaskedFilter? ) -> [MIDIIdentifier] { var endpointIDs: [MIDIIdentifier] = [] - if filtered, let maskedFilter = maskedFilter { + if let maskedFilter = maskedFilter, let midiManager = midiManager { endpointIDs = endpoints .filter(maskedFilter, in: midiManager) .map(\.id) @@ -96,8 +101,11 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent } /// (Don't run from init.) - private func updateIDs(endpoints: [Endpoint]) { - ids = generateIDs(endpoints: endpoints) + private func updateIDs( + endpoints: [Endpoint], + maskedFilter: MIDIEndpointMaskedFilter? + ) { + ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter) } private func endpoint(for id: MIDIIdentifier) -> Endpoint? { @@ -174,20 +182,20 @@ public struct MIDIInputsPicker: View { @Binding public var selection: MIDIIdentifier? @Binding public var cachedSelectionName: String? public var showIcons: Bool - public var filterOwned: Bool + public var hideOwned: Bool public init( title: String, selection: Binding, cachedSelectionName: Binding, showIcons: Bool = true, - filterOwned: Bool = false + hideOwned: Bool = false ) { self.title = title _selection = selection _cachedSelectionName = cachedSelectionName self.showIcons = showIcons - self.filterOwned = filterOwned + self.hideOwned = hideOwned } public var body: some View { @@ -197,12 +205,13 @@ public struct MIDIInputsPicker: View { maskedFilter: maskedFilter, selection: $selection, cachedSelectionName: $cachedSelectionName, - showIcons: showIcons + showIcons: showIcons, + midiManager: midiManager ) } - private var maskedFilter: MIDIEndpointMaskedFilter { - .drop(filterOwned ? .owned() : .default()) + private var maskedFilter: MIDIEndpointMaskedFilter? { + hideOwned ? .drop(.owned()) : nil } } @@ -215,20 +224,20 @@ public struct MIDIOutputsPicker: View { @Binding public var selection: MIDIIdentifier? @Binding public var cachedSelectionName: String? public var showIcons: Bool - public var filterOwned: Bool + public var hideOwned: Bool public init( title: String, selection: Binding, cachedSelectionName: Binding, showIcons: Bool = true, - filterOwned: Bool = false + hideOwned: Bool = false ) { self.title = title _selection = selection _cachedSelectionName = cachedSelectionName self.showIcons = showIcons - self.filterOwned = filterOwned + self.hideOwned = hideOwned } public var body: some View { @@ -238,12 +247,13 @@ public struct MIDIOutputsPicker: View { maskedFilter: maskedFilter, selection: $selection, cachedSelectionName: $cachedSelectionName, - showIcons: showIcons + showIcons: showIcons, + midiManager: midiManager ) } - private var maskedFilter: MIDIEndpointMaskedFilter { - .drop(filterOwned ? .owned() : .default()) + private var maskedFilter: MIDIEndpointMaskedFilter? { + hideOwned ? .drop(.owned()) : nil } } From 91046e7ee0c49875febdfd2f64bbf9a644cb519f Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Nov 2023 23:27:19 -0800 Subject: [PATCH 27/64] Updated `MIDIKitUIExample` example project --- .../MIDIKitUIExample.xcodeproj/project.pbxproj | 10 +++++++++- .../xcshareddata/xcschemes/MIDIKitUIExample.xcscheme | 9 ++++++++- .../MIDIKitUIExample/ContentView.swift | 1 + .../MIDIKitUIExample/ListsExampleView.swift | 11 ++++------- .../MIDIKitUIExample/MIDIHelper.swift | 4 ++-- .../MIDIKitUIExample/MIDIKitUIExampleApp.swift | 4 ++-- .../MIDIKitUIExample/PickersExampleView.swift | 10 ++++------ 7 files changed, 30 insertions(+), 19 deletions(-) diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj index fbcaefe5c3..af4649f1d4 100644 --- a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj @@ -121,7 +121,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1500; TargetAttributes = { E2F1FE6F29B2DF5800054467 = { CreatedOnToolsVersion = 14.2; @@ -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; 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 @@ + + + + Date: Thu, 9 Nov 2023 23:35:44 -0800 Subject: [PATCH 28/64] Updated `MIDIKitUIExample` example project README --- .../MIDIKitUIExample/README.md | 16 ++++++++++++++++ .../MIDIKitUIExample/To Do.md | 11 ----------- 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 Examples/SwiftUI Multiplatform/MIDIKitUIExample/README.md delete mode 100644 Examples/SwiftUI Multiplatform/MIDIKitUIExample/To Do.md diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/README.md b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/README.md new file mode 100644 index 0000000000..83f2b6e008 --- /dev/null +++ b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/README.md @@ -0,0 +1,16 @@ +# MIDIKitUI Example + +This example shows usage of the MIDIKitUI package target SwiftUI controls. + +These bundled UI components provide easy-to-use implementations of endpoint selector lists and pickers. + +- Updates the UI in realtime to reflect changes in system endpoints. +- Remembers selections between app launches. +- Shows visual feedback when a selection is an endpoint that is currently not present in the system, by displaying a caution symbol next to the list item. This selection will be fully restored when the endpoint reappears in the system. + +## Future Development To Do List + +Aspects of this example project that are currently not yet built, but may be added in future: + +- Multiple-selection list control +- Demonstrate updating connections in the `MIDIManager` on endpoint selection change \ No newline at end of file diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/To Do.md b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/To Do.md deleted file mode 100644 index 5a65df41f9..0000000000 --- a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/To Do.md +++ /dev/null @@ -1,11 +0,0 @@ -# MIDIKitUI Example - -This example shows usage of the MIDIKitUI SwiftUI controls. - -> **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 From 5597e612bf17411aea629a40532a9f10b9513a14 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 00:26:19 -0800 Subject: [PATCH 29/64] Updated `MIDIKitUIExample` example project --- .../project.pbxproj | 36 +-- .../MIDISystemInfo.xcodeproj/project.pbxproj | 93 ++++++- .../xcschemes/MIDISystemInfo iOS.xcscheme | 9 +- .../xcschemes/MIDISystemInfo macOS.xcscheme | 9 +- .../MIDISystemInfo/ContentView.swift | 230 ------------------ .../Details/Details Content.swift | 51 ++++ .../MIDISystemInfo/Details/DetailsView.swift | 41 ++++ .../Details/EmptyDetailsView.swift | 26 ++ .../Details/LegacyDetailsView.swift | 63 +++++ .../MIDISystemInfo/Details/Property.swift | 16 ++ .../Details/TableDetailsView.swift | 67 +++++ .../MIDISystemInfo/DetailsView.swift | 198 --------------- .../Navigation/ContentView.swift | 68 ++++++ .../Navigation/DeviceTreeView.swift | 75 ++++++ .../MIDISystemInfo/Navigation/ItemIcon.swift | 36 +++ .../Navigation/OtherInputsView.swift | 42 ++++ .../Navigation/OtherOutputsView.swift | 42 ++++ .../MIDISystemInfo/iOS/SceneDelegate.swift | 20 -- .../MIDISystemInfo/README.md | 2 + 19 files changed, 650 insertions(+), 474 deletions(-) delete mode 100644 Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/ContentView.swift create mode 100644 Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/Details Content.swift create mode 100644 Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/DetailsView.swift create mode 100644 Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/EmptyDetailsView.swift create mode 100644 Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/LegacyDetailsView.swift create mode 100644 Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/Property.swift create mode 100644 Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Details/TableDetailsView.swift delete mode 100644 Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/DetailsView.swift create mode 100644 Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/ContentView.swift create mode 100644 Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/DeviceTreeView.swift create mode 100644 Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/ItemIcon.swift create mode 100644 Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherInputsView.swift create mode 100644 Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherOutputsView.swift diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj index af4649f1d4..e215adfe4a 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 */ @@ -29,7 +28,6 @@ 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 +37,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E2B8BDAD29B356CE00B92BDF /* MIDIKitUI in Frameworks */, - E2F1FE8729B2E04A00054467 /* MIDIKitIO in Frameworks */, + E23AD72D2AFE21A400A69A01 /* MIDIKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -50,7 +47,6 @@ E2F1FE6729B2DF5800054467 = { isa = PBXGroup; children = ( - E2F1FE8429B2DFEF00054467 /* MIDIKit */, E2F1FE7229B2DF5800054467 /* MIDIKitUIExample */, E2F1FE7129B2DF5800054467 /* Products */, ); @@ -67,7 +63,6 @@ E2F1FE7229B2DF5800054467 /* MIDIKitUIExample */ = { isa = PBXGroup; children = ( - E2B8BDAA29B3301300B92BDF /* Info.plist */, E2F1FE7329B2DF5800054467 /* MIDIKitUIExampleApp.swift */, E2F1FE7529B2DF5800054467 /* ContentView.swift */, E2B8BD9D29B308D600B92BDF /* ListsExampleView.swift */, @@ -75,6 +70,7 @@ E2F1FE8829B2E3F600054467 /* MIDIHelper.swift */, E2F1FE8A29B2F2ED00054467 /* Utilities.swift */, E2F1FE7729B2DF5A00054467 /* Assets.xcassets */, + E2B8BDAA29B3301300B92BDF /* Info.plist */, E2F1FE7929B2DF5A00054467 /* MIDIKitUIExample.entitlements */, E2F1FE7A29B2DF5A00054467 /* Preview Content */, ); @@ -106,8 +102,7 @@ ); name = MIDIKitUIExample; packageProductDependencies = ( - E2F1FE8629B2E04A00054467 /* MIDIKitIO */, - E2B8BDAC29B356CE00B92BDF /* MIDIKitUI */, + E23AD72C2AFE21A400A69A01 /* MIDIKit */, ); productName = MIDIKitUI; productReference = E2F1FE7029B2DF5800054467 /* MIDIKitUIExample.app */; @@ -137,6 +132,9 @@ Base, ); mainGroup = E2F1FE6729B2DF5800054467; + packageReferences = ( + E23AD72A2AFE218A00A69A01 /* XCRemoteSwiftPackageReference "MIDIKit" */, + ); productRefGroup = E2F1FE7129B2DF5800054467 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -401,14 +399,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 = { + branch = endpoints; + kind = branch; + }; }; - 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/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj index f3a77bd261..d72abd3a89 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,17 @@ 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -104,13 +131,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 +213,9 @@ E230C49725AE7B31005DCB55 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E20CF4B7299C7F6200F0E003 = { CreatedOnToolsVersion = 14.2; @@ -221,9 +274,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 +293,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 +401,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 +436,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 +466,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 +501,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; 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..ebb5429a94 --- /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: 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 ContentViewPreviews: PreviewProvider { + static let midiManager = MIDIManager( + 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..ee340e0ad9 --- /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: 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() + } + } + } +} 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..b2b7e5d09b --- /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: 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 } + } +} 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..20538fd2d8 --- /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: 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 } + } +} 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/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 From 512782956894e89c66f07fccb0bec34a144dca59 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 00:35:05 -0800 Subject: [PATCH 30/64] Updated `SystemNotifications` example project --- .../project.pbxproj | 12 ++- .../xcschemes/SystemNotifications.xcscheme | 84 +++++++++++++++++++ .../SystemNotifications/ContentView.swift | 4 +- .../SystemNotifications/MIDIHelper.swift | 6 +- .../SystemNotificationsApp.swift | 6 +- 5 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/xcshareddata/xcschemes/SystemNotifications.xcscheme diff --git a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj index 3d424d079a..9142b6c187 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; @@ -372,8 +376,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + branch = endpoints; + kind = branch; }; }; /* 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) From 0e3c484806fd76beb4c2f49ff9cc511c6c26cd80 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 00:38:43 -0800 Subject: [PATCH 31/64] Updated `VirtualInput` example project --- .../VirtualInput.xcodeproj/project.pbxproj | 6 +- .../xcschemes/VirtualInput.xcscheme | 84 +++++++++++++++++++ .../VirtualInput/MIDIHelper.swift | 6 +- .../VirtualInput/VirtualInputApp.swift | 6 +- 4 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/xcshareddata/xcschemes/VirtualInput.xcscheme diff --git a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj index c9df1607c2..14dbc5656f 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 */ @@ -372,8 +372,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + branch = endpoints; + kind = branch; }; }; /* 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) From fef5032c7987d83c3850d9f4acf978983c98144f Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 00:41:59 -0800 Subject: [PATCH 32/64] Updated `VirtualOutput` example project --- .../VirtualOutput.xcodeproj/project.pbxproj | 12 ++- .../xcschemes/VirtualOutput.xcscheme | 84 +++++++++++++++++++ .../VirtualOutput/ContentView.swift | 4 +- .../VirtualOutput/MIDIHelper.swift | 6 +- .../VirtualOutput/VirtualOutputApp.swift | 6 +- 5 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/xcshareddata/xcschemes/VirtualOutput.xcscheme diff --git a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj index f8aa010e5a..e22f362b8b 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; @@ -370,8 +374,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + branch = endpoints; + kind = branch; }; }; /* 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) From 89013591a3a8405b73be8f92f68919b304b45bfb Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 00:46:08 -0800 Subject: [PATCH 33/64] Updated `BluetoothMIDI` example project --- .../BluetoothMIDI.xcodeproj/project.pbxproj | 14 ++-- .../xcschemes/BluetoothMIDI.xcscheme | 84 +++++++++++++++++++ .../BluetoothMIDI/BluetoothMIDIApp.swift | 6 +- .../BluetoothMIDI/ContentView.swift | 4 +- .../BluetoothMIDI/MIDIHelper.swift | 6 +- Examples/SwiftUI iOS/BluetoothMIDI/README.md | 4 +- 6 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/xcshareddata/xcschemes/BluetoothMIDI.xcscheme diff --git a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj index b01d8891ab..99a217e22c 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; @@ -363,8 +367,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + branch = endpoints; + kind = branch; }; }; /* 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 From a0fa6254ebd8912a23c3a85b2e294693ccf297e5 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 00:50:22 -0800 Subject: [PATCH 34/64] Updated `USB iOS to Mac` example project --- .../USB iOS to Mac.xcodeproj/project.pbxproj | 12 ++++++++---- .../xcshareddata/xcschemes/USB iOS to Mac.xcscheme | 9 ++++++++- .../USB iOS to Mac/USB iOS to Mac/ContentView.swift | 6 +++--- .../USB iOS to Mac/USB iOS to Mac/Info.plist | 5 +---- .../USB iOS to Mac/USB iOS to Mac/MIDIHelper.swift | 6 +++--- .../USB iOS to Mac/USBiOStoMacApp.swift | 6 +++--- 6 files changed, 26 insertions(+), 18 deletions(-) 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..94c93ec8ab 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; @@ -356,8 +360,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + branch = endpoints; + kind = branch; }; }; /* 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) From e64b1c4733ce516adf464c3be29f3394b1bbc0ff Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 01:00:01 -0800 Subject: [PATCH 35/64] Updated `BluetoothMIDI` (UIKit) example project --- .../BluetoothMIDI.xcodeproj/project.pbxproj | 26 ++++++++++--------- .../BluetoothMIDI/AppDelegate.swift | 2 +- .../BluetoothMIDI/BluetoothMIDI/Info.plist | 4 --- .../BluetoothMIDI/ViewController.swift | 2 +- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj index 76192483d8..e956368e70 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", @@ -397,7 +399,7 @@ /* 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/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 { From 4d1124b805273a8b2677d914ea3b26d5622e99e3 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 01:02:22 -0800 Subject: [PATCH 36/64] Updated `EventParsing` (UIKit) example project --- .../EventParsing.xcodeproj/project.pbxproj | 12 ++- .../xcschemes/EventParsing.xcscheme | 84 +++++++++++++++++++ .../EventParsing/AppDelegate.swift | 14 ++-- 3 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 Examples/UIKit/EventParsing/EventParsing.xcodeproj/xcshareddata/xcschemes/EventParsing.xcscheme diff --git a/Examples/UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj b/Examples/UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj index 6e0561ee20..ffcf1b9c80 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; 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): From cbec1670c3bf5fda4a1eb631d8701320b0be8769 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 01:02:53 -0800 Subject: [PATCH 37/64] Updated `BluetoothMIDI` (UIKit) example project --- .../xcschemes/BluetoothMIDI.xcscheme | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/xcshareddata/xcschemes/BluetoothMIDI.xcscheme 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 3fda35b35d5fb072ad6fee31d35cfcb67d372927 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 01:04:33 -0800 Subject: [PATCH 38/64] Updated `VirtualInput` (UIKit) example project --- .../VirtualInput.xcodeproj/project.pbxproj | 18 ++-- .../xcschemes/VirtualInput.xcscheme | 84 +++++++++++++++++++ .../VirtualInput/AppDelegate.swift | 6 +- 3 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/xcshareddata/xcschemes/VirtualInput.xcscheme diff --git a/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj b/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj index 2f4afd9a6b..abf443d955 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", 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]? From 67fbf2e894a96fdc225f750146e3fff137bf6393 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 01:06:17 -0800 Subject: [PATCH 39/64] Updated `VirtualOutput` (UIKit) example project --- .../VirtualOutput.xcodeproj/project.pbxproj | 12 ++- .../xcschemes/VirtualOutput.xcscheme | 84 +++++++++++++++++++ .../VirtualOutput/AppDelegate.swift | 14 ++-- 3 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/xcshareddata/xcschemes/VirtualOutput.xcscheme diff --git a/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj b/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj index 459d2d1473..b1f5d86192 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; 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] From 5a9452e1707eca15bde4854cb3e1130d1fa552a6 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 01:24:07 -0800 Subject: [PATCH 40/64] Updated `EndpointMenus` (AppKit) example project --- .../EndpointMenus.xcodeproj/project.pbxproj | 24 ++---- .../xcschemes/EndpointMenus.xcscheme | 84 +++++++++++++++++++ .../EndpointMenus/AppDelegate.swift | 2 +- 3 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/xcshareddata/xcschemes/EndpointMenus.xcscheme diff --git a/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj b/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj index 208201d024..763b0a56cf 100644 --- a/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj +++ b/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj @@ -3,15 +3,14 @@ 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 */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -28,8 +27,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E29AC924285BF271009D1C2C /* MIDIKit in Frameworks */, - E24BA251284F1C5B00AA6767 /* MIDIKit in Frameworks */, + E2CEA4922AFE2DD800BE92F7 /* MIDIKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -81,8 +79,7 @@ ); name = EndpointMenus; packageProductDependencies = ( - E24BA250284F1C5B00AA6767 /* MIDIKit */, - E29AC923285BF271009D1C2C /* MIDIKit */, + E2CEA4912AFE2DD800BE92F7 /* MIDIKit */, ); productName = EndpointMenus; productReference = E24BA23A284F1A3300AA6767 /* EndpointMenus.app */; @@ -113,7 +110,7 @@ ); mainGroup = E24BA231284F1A3300AA6767; packageReferences = ( - E29AC922285BF271009D1C2C /* XCRemoteSwiftPackageReference "MIDIKit" */, + E2CEA48D2AFE2DB700BE92F7 /* XCRemoteSwiftPackageReference "MIDIKit" */, ); productRefGroup = E24BA23B284F1A3300AA6767 /* Products */; projectDirPath = ""; @@ -358,23 +355,20 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - E29AC922285BF271009D1C2C /* XCRemoteSwiftPackageReference "MIDIKit" */ = { + E2CEA48D2AFE2DB700BE92F7 /* XCRemoteSwiftPackageReference "MIDIKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { + kind = upToNextMajorVersion; minimumVersion = 0.9.3; }; }; /* 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..5006c18af4 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 { From e8d09f5f8f4c00c03cc859499373b7de73c2123e Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 01:43:36 -0800 Subject: [PATCH 41/64] Refactored `EndpointMenus` (AppKit) example project --- .../EndpointMenus.xcodeproj/project.pbxproj | 14 +- .../EndpointMenus/AppDelegate.swift | 318 +-------------- .../EndpointMenus/Base.lproj/Main.storyboard | 14 +- .../EndpointMenus/EndpointMenus.entitlements | 6 +- .../MIDIEndpointsMenusHelper.swift | 381 ++++++++++++++++++ Examples/AppKit/EndpointMenus/README.md | 22 +- 6 files changed, 434 insertions(+), 321 deletions(-) create mode 100644 Examples/AppKit/EndpointMenus/EndpointMenus/MIDIEndpointsMenusHelper.swift diff --git a/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj b/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj index 763b0a56cf..d6c1b22832 100644 --- a/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj +++ b/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ E24BA245284F1A3400AA6767 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E24BA243284F1A3400AA6767 /* Main.storyboard */; }; 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 */ @@ -20,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 */ @@ -55,6 +57,7 @@ isa = PBXGroup; children = ( E24BA23D284F1A3300AA6767 /* AppDelegate.swift */, + E2CEA4932AFE2E6400BE92F7 /* MIDIEndpointsMenusHelper.swift */, E24BA243284F1A3400AA6767 /* Main.storyboard */, E24BA246284F1A3400AA6767 /* EndpointMenus.entitlements */, E29FF29E2880BC99005E2BC2 /* Images.xcassets */, @@ -93,7 +96,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + LastUpgradeCheck = 1500; TargetAttributes = { E24BA239284F1A3300AA6767 = { CreatedOnToolsVersion = 14.0; @@ -138,6 +141,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E2CEA4942AFE2E6400BE92F7 /* MIDIEndpointsMenusHelper.swift in Sources */, E24BA23E284F1A3300AA6767 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -160,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"; @@ -193,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; @@ -207,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; @@ -221,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"; @@ -254,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; @@ -262,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; diff --git a/Examples/AppKit/EndpointMenus/EndpointMenus/AppDelegate.swift b/Examples/AppKit/EndpointMenus/EndpointMenus/AppDelegate.swift index 5006c18af4..6a17022ba8 100644 --- a/Examples/AppKit/EndpointMenus/EndpointMenus/AppDelegate.swift +++ b/Examples/AppKit/EndpointMenus/EndpointMenus/AppDelegate.swift @@ -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 From 038e9784ee41c6047eb0896df13f2045319c30d8 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 02:12:45 -0800 Subject: [PATCH 42/64] Updated `EndpointPickers` example project README --- Examples/SwiftUI Multiplatform/EndpointPickers/README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/README.md b/Examples/SwiftUI Multiplatform/EndpointPickers/README.md index 1c93db0c71..f189e7ca7d 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/README.md +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/README.md @@ -6,20 +6,23 @@ This example demonstrates best practises when creating MIDI input and output sel - 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. + > In SwiftUI, this happens automatically when using certain data-source properties of the `ObservableMIDIManager` class. > > - `midiManager.observableEndpoints.inputs` or `midiManager.observableEndpoints.outputs` + > > Changes in MIDI endpoints in the system will trigger these arrays to refresh your view. + > > - `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. > 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 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. + > 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. From 4d3e6499ef223aaef072bd2514b97d165a7a0087 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 02:18:31 -0800 Subject: [PATCH 43/64] Updated `EventParsing` (AppKit) example project --- .../EventParsing.xcodeproj/project.pbxproj | 12 ++- .../xcschemes/EventParsing.xcscheme | 84 +++++++++++++++++++ .../EventParsing/AppDelegate.swift | 14 +++- .../EventParsing/EventParsing.entitlements | 6 +- 4 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 Examples/AppKit/EventParsing/EventParsing.xcodeproj/xcshareddata/xcschemes/EventParsing.xcscheme diff --git a/Examples/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj b/Examples/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj index 46a603cee9..cf0a7b7c1b 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; 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 + From 75627ad7557e9d9ea22108b67eb16520a1080944 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 02:20:18 -0800 Subject: [PATCH 44/64] Updated `VirtualInput` (AppKit) example project --- .../VirtualInput.xcodeproj/project.pbxproj | 12 ++- .../xcschemes/VirtualInput.xcscheme | 84 +++++++++++++++++++ .../VirtualInput/AppDelegate.swift | 2 +- 3 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/xcshareddata/xcschemes/VirtualInput.xcscheme diff --git a/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj b/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj index 71cb088e1e..1b70348b6f 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; 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 { From d8fb5cb97cf55fb14b4857758cf9b36528988ad1 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 02:22:57 -0800 Subject: [PATCH 45/64] Updated `VirtualOutput` (AppKit) example project --- .../VirtualOutput.xcodeproj/project.pbxproj | 16 +++- .../xcschemes/VirtualOutput.xcscheme | 84 +++++++++++++++++++ .../VirtualOutput/AppDelegate.swift | 10 ++- .../VirtualOutput/VirtualOutput.entitlements | 2 - 4 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/xcshareddata/xcschemes/VirtualOutput.xcscheme diff --git a/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj b/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj index d8407fc833..2c51196b4c 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; 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 - From c6a611ab1e0ccad0d36f0c1be787da310e99f7e5 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 02:24:18 -0800 Subject: [PATCH 46/64] Updated `MIDIKitUIExample` example project --- .../MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj index e215adfe4a..ac32839ffd 100644 --- a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 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 = ""; }; @@ -47,6 +48,7 @@ E2F1FE6729B2DF5800054467 = { isa = PBXGroup; children = ( + E2CEA4952AFE3C3200BE92F7 /* README.md */, E2F1FE7229B2DF5800054467 /* MIDIKitUIExample */, E2F1FE7129B2DF5800054467 /* Products */, ); From 45efec8f4c3c8388a523165049c6b586a92d1857 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 02:24:55 -0800 Subject: [PATCH 47/64] Updated `MIDISystemInfo` example project --- .../MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj index d72abd3a89..734a4143b8 100644 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj @@ -64,6 +64,7 @@ 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 /* 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 */ @@ -112,6 +113,7 @@ E230C49625AE7B31005DCB55 = { isa = PBXGroup; children = ( + E2CEA4962AFE3C6100BE92F7 /* README.md */, E230C4A125AE7B31005DCB55 /* MIDISystemInfo */, E230C4A025AE7B31005DCB55 /* Products */, ); From 6ed9e2c669e3f1c313f31f8142c74de6481bbae3 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 03:00:42 -0800 Subject: [PATCH 48/64] Updated `HUITest` example project --- .../HUITest/HUITest.xcodeproj/project.pbxproj | 40 +- .../xcshareddata/xcschemes/HUITest.xcscheme | 9 +- .../HUITest/HUITest/AppDelegate.swift | 95 --- .../HUITest/Base.lproj/Main.storyboard | 683 ------------------ .../HUITest/HUIHostView/HUIHostHelper.swift | 6 +- .../HUITest/HUIHostView/HUIHostView.swift | 4 +- .../HUISurfaceView/HUIClientView.swift | 6 +- .../Advanced/HUITest/HUITest/HUITestApp.swift | 73 ++ .../Advanced/HUITest/HUITest/Utilities.swift | 9 + 9 files changed, 113 insertions(+), 812 deletions(-) delete mode 100644 Examples/Advanced/HUITest/HUITest/AppDelegate.swift delete mode 100644 Examples/Advanced/HUITest/HUITest/Base.lproj/Main.storyboard create mode 100644 Examples/Advanced/HUITest/HUITest/HUITestApp.swift diff --git a/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj b/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj index be77e42c51..e92748e236 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; @@ -563,8 +553,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + branch = endpoints; + kind = branch; }; }; /* 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 + } +} From d928cbb7c4d94d43bcb0dbd29dd975b922e1da50 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 03:19:21 -0800 Subject: [PATCH 49/64] Updated `MIDIEventLogger` example project --- .../MIDIEventLogger.xcodeproj/project.pbxproj | 18 ++++++--- .../xcschemes/MIDIEventLogger.xcscheme | 9 ++++- .../MIDIEventLogger/AppDelegate.swift | 22 +++++++---- .../MIDIEventLogger/MIDIHelper.swift | 6 +-- .../MIDIEventLogger/Views/ContentView.swift | 32 +++++++++++----- .../Views/MIDISubsystemStatusView.swift | 4 +- .../Views/ReceiveMIDIEventsView.swift | 37 ++++++++++++------- .../SendMIDIEventsChannelVoiceView.swift | 2 +- .../SendMIDIEventsMIDI2ChannelVoiceView.swift | 2 +- .../SendMIDIEventsSystemCommonView.swift | 2 +- .../SendMIDIEventsSystemExclusiveView.swift | 2 +- .../SendMIDIEventsSystemRealTimeView.swift | 2 +- .../Views/SendMIDIEventsView.swift | 4 +- Examples/Advanced/MIDIEventLogger/README.md | 2 +- 14 files changed, 92 insertions(+), 52 deletions(-) diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj index b28f700565..c887b51b23 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj @@ -46,6 +46,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 */ @@ -83,6 +84,7 @@ E26A25A326C586F600FFCF40 = { isa = PBXGroup; children = ( + E2CEA4972AFE44FE00BE92F7 /* README.md */, E26A25AE26C586F600FFCF40 /* MIDIEventLogger */, E26A25AD26C586F600FFCF40 /* Products */, ); @@ -99,11 +101,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 */, @@ -146,7 +148,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1300; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1500; TargetAttributes = { E26A25AB26C586F600FFCF40 = { CreatedOnToolsVersion = 13.0; @@ -217,6 +219,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; 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 +283,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 +318,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 +434,7 @@ repositoryURL = "https://github.com/orchetect/SwiftRadix.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.0; + minimumVersion = 1.3.1; }; }; E26A25C726C5875A00FFCF40 /* XCRemoteSwiftPackageReference "OTCore" */ = { @@ -436,15 +442,15 @@ repositoryURL = "https://github.com/orchetect/OTCore"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.4.8; + minimumVersion = 1.4.13; }; }; E29AC90A285BEFB1009D1C2C /* XCRemoteSwiftPackageReference "MIDIKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + branch = endpoints; + kind = branch; }; }; /* End XCRemoteSwiftPackageReference section */ 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..303f931d95 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 diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ContentView.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ContentView.swift index 5d244eb716..365ac1e86e 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,7 +65,7 @@ struct ContentView: View { .onAppear { setInputConnectionToVirtual() } - .onChange(of: midiInputConnectionEndpoint) { _ in + .onChange(of: midiInputConnectionID) { _ in updateInputConnection() } } @@ -72,15 +74,17 @@ struct ContentView: View { /// Auto-select the virtual endpoint as our input connection source. func setInputConnectionToVirtual() { - midiInputConnectionEndpoint = midiHelper.midiOutput?.endpoint + guard let midiOutputEndpoint = midiHelper.midiOutput?.endpoint else { return } + midiInputConnectionID = midiOutputEndpoint.uniqueID + midiInputConnectionDisplayName = midiOutputEndpoint.displayName } /// 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")" + "Updating input connection to endpoint: \(midiInputConnectionDisplayName?.quoted ?? "None")" ) - midiHelper.updateInputConnection(selectedUniqueID: midiInputConnectionEndpoint?.uniqueID) + midiHelper.updateInputConnection(selectedUniqueID: midiInputConnectionID) } /// Send a MIDI event using our virtual output endpoint. @@ -91,11 +95,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..b779da7de5 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,24 @@ 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: "", + selection: $midiInputConnectionID, + cachedSelectionName: $midiInputConnectionDisplayName, + showIcons: true, + hideOwned: false + ) +// Picker("", selection: $midiInputConnectionEndpoint) { +// Text("None") +// .tag(MIDIOutputEndpoint?.none) +// +// VStack { Divider().padding(.leading) } +// +// ForEach(midiManager.endpoints.outputs) { +// Text("🎹 " + ($0.displayName)) +// .tag(MIDIOutputEndpoint?.some($0)) +// } +// } .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. From a7108f4f4790df44ac86db34c485743e225c42b7 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 03:53:04 -0800 Subject: [PATCH 50/64] Updated `MTCExample` example project --- .../MTCExample.xcodeproj/project.pbxproj | 18 +- .../xcschemes/MTCExample.xcscheme | 9 +- .../MTCExample/MTCExample/AppDelegate.swift | 33 +- .../MTCExample/MTCGenContentView.swift | 336 +++++++++------- .../MTCExample/MTCRecContentView.swift | 376 ++++++++++-------- 5 files changed, 437 insertions(+), 335 deletions(-) diff --git a/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj b/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj index ca4fd4ef9a..99efb80d88 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; @@ -401,8 +407,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + branch = endpoints; + kind = branch; }; }; 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() From 46aaa02dc99c00d4d803ef10aefcc4ce0926ed74 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 03:57:48 -0800 Subject: [PATCH 51/64] Bumped example projects to MIDIKit 0.9.4 --- Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj | 4 ++-- .../MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj | 4 ++-- .../Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj | 4 ++-- .../EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj | 2 +- .../EventParsing/EventParsing.xcodeproj/project.pbxproj | 2 +- .../VirtualInput/VirtualInput.xcodeproj/project.pbxproj | 2 +- .../VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj | 2 +- .../EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj | 4 ++-- .../EventParsing/EventParsing.xcodeproj/project.pbxproj | 4 ++-- .../MIDIKitUIExample.xcodeproj/project.pbxproj | 4 ++-- .../MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj | 2 +- .../SystemNotifications.xcodeproj/project.pbxproj | 4 ++-- .../VirtualInput/VirtualInput.xcodeproj/project.pbxproj | 4 ++-- .../VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj | 4 ++-- .../BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj | 4 ++-- .../USB iOS to Mac/USB iOS to Mac.xcodeproj/project.pbxproj | 4 ++-- .../BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj | 2 +- .../UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj | 2 +- .../UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj | 2 +- .../VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj | 2 +- 20 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj b/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj index e92748e236..c0220aaae5 100644 --- a/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj +++ b/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj @@ -553,8 +553,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - branch = endpoints; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj index c887b51b23..3878a6f8b0 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj @@ -449,8 +449,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - branch = endpoints; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj b/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj index 99efb80d88..03f0c20ab5 100644 --- a/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj +++ b/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj @@ -407,8 +407,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - branch = endpoints; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; }; }; E29EF02D267BF47F00282F94 /* XCRemoteSwiftPackageReference "SwiftRadix" */ = { diff --git a/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj b/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj index d6c1b22832..90fc0e6875 100644 --- a/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj +++ b/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj @@ -368,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/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj b/Examples/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj index cf0a7b7c1b..bec29e55f4 100644 --- a/Examples/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj +++ b/Examples/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj @@ -371,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/VirtualInput/VirtualInput.xcodeproj/project.pbxproj b/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj index 1b70348b6f..beebd5237c 100644 --- a/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj +++ b/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj @@ -367,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/project.pbxproj b/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj index 2c51196b4c..6054dc4538 100644 --- a/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj +++ b/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj @@ -367,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/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj index 11e2fe82e9..17dabd7437 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj @@ -410,8 +410,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - branch = endpoints; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj index 23f5f4b08c..ab2d65b6a5 100644 --- a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj @@ -376,8 +376,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - branch = endpoints; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; }; }; E2D7FF1A29754911003212AF /* XCRemoteSwiftPackageReference "SwiftRadix" */ = { diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj index ac32839ffd..f7d83daf5a 100644 --- a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj @@ -406,8 +406,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - branch = endpoints; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj index 734a4143b8..5032a8f1f5 100644 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj @@ -620,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/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj index 9142b6c187..d999493e80 100644 --- a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj @@ -376,8 +376,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - branch = endpoints; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj index 14dbc5656f..c8a729eba7 100644 --- a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj @@ -372,8 +372,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - branch = endpoints; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj index e22f362b8b..84f49f370b 100644 --- a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj @@ -374,8 +374,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - branch = endpoints; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj index 99a217e22c..4fe3f8fdfe 100644 --- a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj @@ -367,8 +367,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - branch = endpoints; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ 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 94c93ec8ab..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 @@ -360,8 +360,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { - branch = endpoints; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj index e956368e70..0e405e28a2 100644 --- a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj +++ b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj @@ -393,7 +393,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/EventParsing/EventParsing.xcodeproj/project.pbxproj b/Examples/UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj index ffcf1b9c80..c95da5773e 100644 --- a/Examples/UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj +++ b/Examples/UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj @@ -380,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/VirtualInput/VirtualInput.xcodeproj/project.pbxproj b/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj index abf443d955..6c94aad4fa 100644 --- a/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj +++ b/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj @@ -376,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/project.pbxproj b/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj index b1f5d86192..3d1ae9bdd2 100644 --- a/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj +++ b/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj @@ -376,7 +376,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.3; + minimumVersion = 0.9.4; }; }; /* End XCRemoteSwiftPackageReference section */ From 687065051606578dbbd628c28a7362ba5bb169f3 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 04:03:26 -0800 Subject: [PATCH 52/64] Updated docs --- .../MIDIKitIO/MIDIKitIO-Combine-and-SwiftUI-Features.md | 7 +++---- Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO.md | 4 ++-- .../MIDIKitIO-Combine-and-SwiftUI-Features.md | 7 +++---- Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO.md | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) 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.md b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO.md index 033cf05b3f..38636c026f 100644 --- a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO.md +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO.md @@ -15,11 +15,11 @@ To add additional functionality, import extension modules or import the MIDIKit ### Manager - ``MIDIManager`` -- ``ObservableMIDIManager`` - - - - +- ``ObservableMIDIManager`` - ### Devices & Entities @@ -47,4 +47,4 @@ To add additional functionality, import extension modules or import the MIDIKit ### Internals -- +- \ No newline at end of file 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.md b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO.md index f9931a3e95..251fc2012a 100644 --- a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO.md +++ b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO.md @@ -15,11 +15,11 @@ To add additional functionality, import extension modules or import the MIDIKit ### Manager - ``MIDIManager`` -- ``ObservableMIDIManager`` - - - - +- ``ObservableMIDIManager`` - ### Devices & Entities From 50f7fe93eb5f64b7f05354983c41684528afbd1b Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 04:17:05 -0800 Subject: [PATCH 53/64] Updated `MIDISystemInfo` example project --- .../MIDISystemInfo/Navigation/ContentView.swift | 4 ++-- .../MIDISystemInfo/Navigation/DeviceTreeView.swift | 2 +- .../MIDISystemInfo/Navigation/OtherInputsView.swift | 4 ++-- .../MIDISystemInfo/Navigation/OtherOutputsView.swift | 4 ++-- .../MIDISystemInfo/MIDISystemInfo/iOS/AppDelegate.swift | 2 +- .../MIDISystemInfo/MIDISystemInfo/macOS/AppDelegate.swift | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/ContentView.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/ContentView.swift index ebb5429a94..af9cd1a489 100644 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/ContentView.swift +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/ContentView.swift @@ -23,7 +23,7 @@ struct ContentViewForCurrentPlatform: View { } struct ContentView: View { - @EnvironmentObject private var midiManager: MIDIManager + @EnvironmentObject private var midiManager: ObservableMIDIManager let detailsContent: ( _ object: AnyMIDIIOObject?, @@ -55,7 +55,7 @@ struct ContentView: View { } struct ContentViewPreviews: PreviewProvider { - static let midiManager = MIDIManager( + static let midiManager = ObservableMIDIManager( clientName: "Preview", model: "Preview", manufacturer: "MyCompany" diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/DeviceTreeView.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/DeviceTreeView.swift index ee340e0ad9..d3deaf5335 100644 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/DeviceTreeView.swift +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/DeviceTreeView.swift @@ -8,7 +8,7 @@ import MIDIKitIO import SwiftUI struct DeviceTreeView: View { - @EnvironmentObject private var midiManager: MIDIManager + @EnvironmentObject private var midiManager: ObservableMIDIManager let detailsContent: ( _ object: AnyMIDIIOObject?, diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherInputsView.swift b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherInputsView.swift index b2b7e5d09b..1506494329 100644 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherInputsView.swift +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherInputsView.swift @@ -8,7 +8,7 @@ import MIDIKitIO import SwiftUI struct OtherInputsView: View { - @EnvironmentObject private var midiManager: MIDIManager + @EnvironmentObject private var midiManager: ObservableMIDIManager let detailsContent: ( _ object: AnyMIDIIOObject?, @@ -36,7 +36,7 @@ struct OtherInputsView: View { 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() + 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 index 20538fd2d8..24d427d257 100644 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherOutputsView.swift +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo/Navigation/OtherOutputsView.swift @@ -8,7 +8,7 @@ import MIDIKitIO import SwiftUI struct OtherOutputsView: View { - @EnvironmentObject private var midiManager: MIDIManager + @EnvironmentObject private var midiManager: ObservableMIDIManager let detailsContent: ( _ object: AnyMIDIIOObject?, @@ -36,7 +36,7 @@ struct OtherOutputsView: View { 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() + 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/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" From 3646b0cf4aaf50c5b533d52aa6ca13bba92956f0 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 05:29:04 -0800 Subject: [PATCH 54/64] MIDIKitUI endpoint controls now implement fallback display name --- Sources/MIDIKitUI/MIDIEndpointsList.swift | 94 +++++++++++++-------- Sources/MIDIKitUI/MIDIEndpointsPicker.swift | 90 ++++++++++++-------- 2 files changed, 114 insertions(+), 70 deletions(-) diff --git a/Sources/MIDIKitUI/MIDIEndpointsList.swift b/Sources/MIDIKitUI/MIDIEndpointsList.swift index 1c78f2bafb..6bd007876b 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsList.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsList.swift @@ -16,8 +16,8 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent var endpoints: [Endpoint] var maskedFilter: MIDIEndpointMaskedFilter? - @Binding var selection: MIDIIdentifier? - @Binding var cachedSelectionName: String? + @Binding var selectionID: MIDIIdentifier? + @Binding var selectionDisplayName: String? let showIcons: Bool @State private var ids: [MIDIIdentifier] = [] @@ -25,15 +25,15 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent init( endpoints: [Endpoint], maskedFilter: MIDIEndpointMaskedFilter?, - selection: Binding, - cachedSelectionName: Binding, + selectionID: Binding, + selectionDisplayName: Binding, showIcons: Bool, midiManager: ObservableMIDIManager? ) { self.endpoints = endpoints self.maskedFilter = maskedFilter - _selection = selection - _cachedSelectionName = cachedSelectionName + _selectionID = selectionID + _selectionDisplayName = selectionDisplayName self.showIcons = showIcons self.midiManager = midiManager @@ -42,33 +42,35 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent } 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 { + updateID(endpoints: endpoints) updateIDs(endpoints: endpoints, maskedFilter: maskedFilter) } .onChange(of: maskedFilter) { newValue in updateIDs(endpoints: endpoints, maskedFilter: newValue) } .onChange(of: endpoints) { newValue in + updateID(endpoints: newValue) updateIDs(endpoints: newValue, maskedFilter: maskedFilter) } - .onChange(of: selection) { newValue in + .onChange(of: selectionID) { newValue in updateIDs(endpoints: endpoints, maskedFilter: maskedFilter) - guard let selection = newValue else { - cachedSelectionName = nil + guard let selectionID = newValue else { + selectionDisplayName = nil return } - if let dn = endpoint(for: selection)?.displayName { - cachedSelectionName = dn + if let dn = endpoint(for: selectionID)?.displayName { + selectionDisplayName = dn } } } @@ -87,13 +89,33 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent .map(\.id) } - if let selection, !endpointIDs.contains(selection) { - return [selection] + endpointIDs + if let selectionID, !endpointIDs.contains(selectionID) { + return [selectionID] + endpointIDs } else { return endpointIDs } } + private func updateID(endpoints: [Endpoint]) { + if selectionID == .invalidMIDIIdentifier { + selectionID = nil + selectionDisplayName = nil + return + } + + if let selectionID = selectionID, + let selectionDisplayName = selectionDisplayName, + let found = endpoints.first( + whereUniqueID: selectionID, + fallbackDisplayName: selectionDisplayName + ) + { + self.selectionDisplayName = found.displayName + // update ID in case it changed + if self.selectionID != found.uniqueID { self.selectionID = found.uniqueID } + } + } + /// (Don't run from init.) private func updateIDs( endpoints: [Endpoint], @@ -108,7 +130,7 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent private struct EndpointRow: View { let endpoint: Endpoint? - @Binding var cachedSelectionName: String? + @Binding var selectionDisplayName: String? let showIcon: Bool var body: some View { @@ -136,8 +158,8 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent private var missingText: String { showIcon - ? cachedSelectionName ?? "Missing" - : (cachedSelectionName ?? "") + " (Missing)" + ? selectionDisplayName ?? "Missing" + : (selectionDisplayName ?? "") + " (Missing)" } @ViewBuilder @@ -168,19 +190,19 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent public struct MIDIInputsList: View { @EnvironmentObject private var midiManager: ObservableMIDIManager - @Binding public var selection: MIDIIdentifier? - @Binding public var cachedSelectionName: String? + @Binding public var selectionID: MIDIIdentifier? + @Binding public var selectionDisplayName: String? public var showIcons: Bool public var hideOwned: Bool public init( - selection: Binding, - cachedSelectionName: Binding, + selectionID: Binding, + selectionDisplayName: Binding, showIcons: Bool = true, hideOwned: Bool = false ) { - _selection = selection - _cachedSelectionName = cachedSelectionName + _selectionID = selectionID + _selectionDisplayName = selectionDisplayName self.showIcons = showIcons self.hideOwned = hideOwned } @@ -189,12 +211,12 @@ public struct MIDIInputsList: View { MIDIEndpointsList( endpoints: midiManager.observableEndpoints.inputs, maskedFilter: maskedFilter, - selection: $selection, - cachedSelectionName: $cachedSelectionName, + selectionID: $selectionID, + selectionDisplayName: $selectionDisplayName, showIcons: showIcons, midiManager: midiManager ) - Text("Selected: \(cachedSelectionName ?? "None")") + Text("Selected: \(selectionDisplayName ?? "None")") } private var maskedFilter: MIDIEndpointMaskedFilter? { @@ -207,19 +229,19 @@ public struct MIDIInputsList: View { public struct MIDIOutputsList: View { @EnvironmentObject private var midiManager: ObservableMIDIManager - @Binding public var selection: MIDIIdentifier? - @Binding public var cachedSelectionName: String? + @Binding public var selectionID: MIDIIdentifier? + @Binding public var selectionDisplayName: String? public var showIcons: Bool public var hideOwned: Bool public init( - selection: Binding, - cachedSelectionName: Binding, + selectionID: Binding, + selectionDisplayName: Binding, showIcons: Bool = true, hideOwned: Bool = false ) { - _selection = selection - _cachedSelectionName = cachedSelectionName + _selectionID = selectionID + _selectionDisplayName = selectionDisplayName self.showIcons = showIcons self.hideOwned = hideOwned } @@ -228,12 +250,12 @@ public struct MIDIOutputsList: View { MIDIEndpointsList( endpoints: midiManager.observableEndpoints.outputs, maskedFilter: maskedFilter, - selection: $selection, - cachedSelectionName: $cachedSelectionName, + selectionID: $selectionID, + selectionDisplayName: $selectionDisplayName, showIcons: showIcons, midiManager: midiManager ) - Text("Selected: \(cachedSelectionName ?? "None")") + Text("Selected: \(selectionDisplayName ?? "None")") } private var maskedFilter: MIDIEndpointMaskedFilter? { diff --git a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift index 3867d890a6..01530e1822 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift @@ -17,8 +17,8 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent let title: String var endpoints: [Endpoint] var maskedFilter: MIDIEndpointMaskedFilter? - @Binding var selection: MIDIIdentifier? - @Binding var cachedSelectionName: String? + @Binding var selectionID: MIDIIdentifier? + @Binding var selectionDisplayName: String? var showIcons: Bool @State private var ids: [MIDIIdentifier] = [] @@ -27,16 +27,16 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent title: String, endpoints: [Endpoint], maskedFilter: MIDIEndpointMaskedFilter?, - selection: Binding, - cachedSelectionName: Binding, + selectionID: Binding, + selectionDisplayName: Binding, showIcons: Bool, midiManager: ObservableMIDIManager? ) { self.title = title self.endpoints = endpoints self.maskedFilter = maskedFilter - _selection = selection - _cachedSelectionName = cachedSelectionName + _selectionID = selectionID + _selectionDisplayName = selectionDisplayName self.showIcons = showIcons self.midiManager = midiManager @@ -45,40 +45,62 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent } 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 { + updateID(endpoints: endpoints) updateIDs(endpoints: endpoints, maskedFilter: maskedFilter) } .onChange(of: maskedFilter) { newValue in updateIDs(endpoints: endpoints, maskedFilter: newValue) } .onChange(of: endpoints) { newValue in + updateID(endpoints: newValue) updateIDs(endpoints: newValue, maskedFilter: maskedFilter) } - .onChange(of: selection) { newValue in + .onChange(of: selectionID) { newValue in updateIDs(endpoints: endpoints, maskedFilter: maskedFilter) - guard let selection = newValue else { - cachedSelectionName = nil + guard let selectionID = newValue else { + selectionDisplayName = nil return } - if let dn = endpoint(for: selection)?.displayName { - cachedSelectionName = dn + if let dn = endpoint(for: selectionID)?.displayName { + selectionDisplayName = dn } } } + private func updateID(endpoints: [Endpoint]) { + if selectionID == .invalidMIDIIdentifier { + selectionID = nil + selectionDisplayName = nil + return + } + + if let selectionID = selectionID, + let selectionDisplayName = selectionDisplayName, + let found = endpoints.first( + whereUniqueID: selectionID, + fallbackDisplayName: selectionDisplayName + ) + { + self.selectionDisplayName = found.displayName + // update ID in case it changed + if self.selectionID != found.uniqueID { self.selectionID = found.uniqueID } + } + } + private func generateIDs( endpoints: [Endpoint], maskedFilter: MIDIEndpointMaskedFilter? @@ -93,8 +115,8 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent .map(\.id) } - if let selection, !endpointIDs.contains(selection) { - return [selection] + endpointIDs + if let selectionID, !endpointIDs.contains(selectionID) { + return [selectionID] + endpointIDs } else { return endpointIDs } @@ -114,7 +136,7 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent private struct EndpointRow: View { let endpoint: Endpoint? - @Binding var cachedSelectionName: String? + @Binding var selectionDisplayName: String? let showIcon: Bool var body: some View { @@ -150,8 +172,8 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent private var missingText: String { showIcon - ? cachedSelectionName ?? "Missing" - : (cachedSelectionName ?? "") + " (Missing)" + ? selectionDisplayName ?? "Missing" + : (selectionDisplayName ?? "") + " (Missing)" } @ViewBuilder @@ -179,21 +201,21 @@ public struct MIDIInputsPicker: View { @EnvironmentObject private var midiManager: ObservableMIDIManager public var title: String - @Binding public var selection: MIDIIdentifier? - @Binding public var cachedSelectionName: String? + @Binding public var selectionID: MIDIIdentifier? + @Binding public var selectionDisplayName: String? public var showIcons: Bool public var hideOwned: Bool public init( title: String, - selection: Binding, - cachedSelectionName: Binding, + selectionID: Binding, + selectionDisplayName: Binding, showIcons: Bool = true, hideOwned: Bool = false ) { self.title = title - _selection = selection - _cachedSelectionName = cachedSelectionName + _selectionID = selectionID + _selectionDisplayName = selectionDisplayName self.showIcons = showIcons self.hideOwned = hideOwned } @@ -203,8 +225,8 @@ public struct MIDIInputsPicker: View { title: title, endpoints: midiManager.observableEndpoints.inputs, maskedFilter: maskedFilter, - selection: $selection, - cachedSelectionName: $cachedSelectionName, + selectionID: $selectionID, + selectionDisplayName: $selectionDisplayName, showIcons: showIcons, midiManager: midiManager ) @@ -221,21 +243,21 @@ public struct MIDIOutputsPicker: View { @EnvironmentObject private var midiManager: ObservableMIDIManager public var title: String - @Binding public var selection: MIDIIdentifier? - @Binding public var cachedSelectionName: String? + @Binding public var selectionID: MIDIIdentifier? + @Binding public var selectionDisplayName: String? public var showIcons: Bool public var hideOwned: Bool public init( title: String, - selection: Binding, - cachedSelectionName: Binding, + selectionID: Binding, + selectionDisplayName: Binding, showIcons: Bool = true, hideOwned: Bool = false ) { self.title = title - _selection = selection - _cachedSelectionName = cachedSelectionName + _selectionID = selectionID + _selectionDisplayName = selectionDisplayName self.showIcons = showIcons self.hideOwned = hideOwned } @@ -245,8 +267,8 @@ public struct MIDIOutputsPicker: View { title: title, endpoints: midiManager.observableEndpoints.outputs, maskedFilter: maskedFilter, - selection: $selection, - cachedSelectionName: $cachedSelectionName, + selectionID: $selectionID, + selectionDisplayName: $selectionDisplayName, showIcons: showIcons, midiManager: midiManager ) From 09ab18ec01968d819e51008b4b2d687aed6dabf0 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 05:30:45 -0800 Subject: [PATCH 55/64] Updated `MIDIKitUIExample` example project --- .../MIDIKitUIExample/MIDIKitUIExample/ContentView.swift | 6 ++++-- .../MIDIKitUIExample/ListsExampleView.swift | 8 ++++---- .../MIDIKitUIExample/PickersExampleView.swift | 8 ++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/ContentView.swift b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/ContentView.swift index ee03548739..9061a8a82f 100644 --- a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/ContentView.swift +++ b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/ContentView.swift @@ -77,11 +77,13 @@ struct ContentView: View { Group { Button("Create Test Endpoints") { try? midiHelper.createVirtuals() - }.disabled(midiHelper.virtualsExist) + } + .disabled(midiHelper.virtualsExist) Button("Remove Test Endpoints") { try? midiHelper.destroyVirtuals() - }.disabled(!midiHelper.virtualsExist) + } + .disabled(!midiHelper.virtualsExist) } .buttonStyle(.bordered) } diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/ListsExampleView.swift b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/ListsExampleView.swift index e0bd41947c..f12e998fb9 100644 --- a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/ListsExampleView.swift +++ b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/ListsExampleView.swift @@ -82,8 +82,8 @@ struct ListsExampleView: View { private var inputsList: some View { MIDIInputsList( - selection: $midiInput, - cachedSelectionName: $midiInputName, + selectionID: $midiInput, + selectionDisplayName: $midiInputName, showIcons: showIcons, hideOwned: hideCreated ) @@ -94,8 +94,8 @@ struct ListsExampleView: View { private var outputsList: some View { MIDIOutputsList( - selection: $midiOutput, - cachedSelectionName: $midiOutputName, + selectionID: $midiOutput, + selectionDisplayName: $midiOutputName, showIcons: showIcons, hideOwned: hideCreated ) diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/PickersExampleView.swift b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/PickersExampleView.swift index e8cf3888a0..460d1de3d2 100644 --- a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/PickersExampleView.swift +++ b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/PickersExampleView.swift @@ -110,8 +110,8 @@ struct PickersExampleView: View { private var inputsList: some View { MIDIInputsPicker( title: "Input", - selection: $midiInput, - cachedSelectionName: $midiInputName, + selectionID: $midiInput, + selectionDisplayName: $midiInputName, showIcons: showIcons, hideOwned: hideCreated ) @@ -121,8 +121,8 @@ struct PickersExampleView: View { private var outputsList: some View { MIDIOutputsPicker( title: "Output", - selection: $midiOutput, - cachedSelectionName: $midiOutputName, + selectionID: $midiOutput, + selectionDisplayName: $midiOutputName, showIcons: showIcons, hideOwned: hideCreated ) From 7b7f7ca780062289507fd419de562144991d0982 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 05:58:22 -0800 Subject: [PATCH 56/64] Refactored MIDIKitUI controls ID update --- Sources/MIDIKitUI/MIDIEndpointsList.swift | 61 ++++++++++++--------- Sources/MIDIKitUI/MIDIEndpointsPicker.swift | 41 ++++++++------ 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/Sources/MIDIKitUI/MIDIEndpointsList.swift b/Sources/MIDIKitUI/MIDIEndpointsList.swift index 6bd007876b..4c7894efa5 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsList.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsList.swift @@ -75,45 +75,33 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent } } - private func generateIDs( - endpoints: [Endpoint], - maskedFilter: MIDIEndpointMaskedFilter? - ) -> [MIDIIdentifier] { - var endpointIDs: [MIDIIdentifier] = [] - if let maskedFilter = maskedFilter, let midiManager = midiManager { - endpointIDs = endpoints - .filter(maskedFilter, in: midiManager) - .map(\.id) - } else { - endpointIDs = endpoints - .map(\.id) + private func updateID(endpoints: [Endpoint]) { + guard let updatedDetails = updatedID(endpoints: endpoints) else { + return } - if let selectionID, !endpointIDs.contains(selectionID) { - return [selectionID] + endpointIDs - } else { - return endpointIDs - } + self.selectionDisplayName = updatedDetails.displayName + // update ID in case it changed + if self.selectionID != updatedDetails.id { self.selectionID = updatedDetails.id } } - private func updateID(endpoints: [Endpoint]) { + /// Returns non-nil if properties require updating. + private func updatedID(endpoints: [Endpoint]) -> (id: MIDIIdentifier?, displayName: String?)? { if selectionID == .invalidMIDIIdentifier { - selectionID = nil - selectionDisplayName = nil - return + return (id: nil, displayName: nil) } if let selectionID = selectionID, let selectionDisplayName = selectionDisplayName, let found = endpoints.first( - whereUniqueID: selectionID, - fallbackDisplayName: selectionDisplayName + whereUniqueID: selectionID, + fallbackDisplayName: selectionDisplayName ) { - self.selectionDisplayName = found.displayName - // update ID in case it changed - if self.selectionID != found.uniqueID { self.selectionID = found.uniqueID } + return (id: found.uniqueID, displayName: found.displayName) } + + return nil } /// (Don't run from init.) @@ -124,6 +112,27 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter) } + private func generateIDs( + endpoints: [Endpoint], + maskedFilter: MIDIEndpointMaskedFilter? + ) -> [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 + } + } + private func endpoint(for id: MIDIIdentifier) -> Endpoint? { endpoints.first(whereUniqueID: id) } diff --git a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift index 01530e1822..bfc00f4cc8 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift @@ -82,23 +82,40 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent } private func updateID(endpoints: [Endpoint]) { - if selectionID == .invalidMIDIIdentifier { - selectionID = nil - selectionDisplayName = nil + guard let updatedDetails = updatedID(endpoints: endpoints) else { return } + self.selectionDisplayName = updatedDetails.displayName + // update ID in case it changed + if self.selectionID != updatedDetails.id { self.selectionID = updatedDetails.id } + } + + /// Returns non-nil if properties require updating. + private 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 + whereUniqueID: selectionID, + fallbackDisplayName: selectionDisplayName ) { - self.selectionDisplayName = found.displayName - // update ID in case it changed - if self.selectionID != found.uniqueID { self.selectionID = found.uniqueID } + return (id: found.uniqueID, displayName: found.displayName) } + + return nil + } + + /// (Don't run from init.) + private func updateIDs( + endpoints: [Endpoint], + maskedFilter: MIDIEndpointMaskedFilter? + ) { + ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter) } private func generateIDs( @@ -122,14 +139,6 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent } } - /// (Don't run from init.) - private func updateIDs( - endpoints: [Endpoint], - maskedFilter: MIDIEndpointMaskedFilter? - ) { - ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter) - } - private func endpoint(for id: MIDIIdentifier) -> Endpoint? { endpoints.first(whereUniqueID: id) } From 1631ff615d37e2aa06be66cba8a2d5f83c9a41cd Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 06:00:05 -0800 Subject: [PATCH 57/64] Updated `EndpointPickers` example project --- .../EndpointPickers.xcodeproj/project.pbxproj | 23 ++------- .../EndpointPickers/EndpointPickersApp.swift | 48 +++---------------- .../EndpointPickers/MIDIHelper.swift | 47 ++++++++++-------- .../EndpointPickers/iOS/ContentView-iOS.swift | 33 ++++++++----- .../macOS/ContentView-macOS.swift | 32 +++++++------ 5 files changed, 76 insertions(+), 107 deletions(-) diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj index 17dabd7437..c6955b52c9 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj @@ -8,23 +8,18 @@ /* 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 = ""; }; @@ -40,7 +35,6 @@ buildActionMask = 2147483647; files = ( E29AC912285BF048009D1C2C /* MIDIKit in Frameworks */, - E27D0E75284F409600F43247 /* MIDIKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -51,7 +45,6 @@ isa = PBXGroup; children = ( E2496A8B2989087F003FD165 /* ContentView-iOS.swift */, - E2496A8A2989087F003FD165 /* MIDIEndpointSelectionView-iOS.swift */, E2496A8C2989087F003FD165 /* Info.plist */, ); path = iOS; @@ -61,7 +54,6 @@ isa = PBXGroup; children = ( E2496A902989087F003FD165 /* ContentView-macOS.swift */, - E2496A8E2989087F003FD165 /* MIDIEndpointSelectionView-macOS.swift */, E2496A8F2989087F003FD165 /* EndpointPickers.entitlements */, ); path = macOS; @@ -114,7 +106,6 @@ ); name = EndpointPickers; packageProductDependencies = ( - E27D0E74284F409600F43247 /* MIDIKit */, E29AC911285BF048009D1C2C /* MIDIKit */, ); productName = EndpointPickers; @@ -178,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; }; @@ -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; @@ -295,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; @@ -417,10 +406,6 @@ /* 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/EndpointPickersApp.swift b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/EndpointPickersApp.swift index 99a7c62e62..bd0968c29d 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/EndpointPickersApp.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/EndpointPickersApp.swift @@ -4,7 +4,7 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // -import MIDIKit +import MIDIKitIO import SwiftUI import Combine @@ -19,16 +19,16 @@ struct EndpointPickersApp: App { @ObservedObject var midiHelper = MIDIHelper() @AppStorage(MIDIHelper.PrefKeys.midiInID) - var midiInSelectedID: MIDIIdentifier = .invalidMIDIIdentifier + var midiInSelectedID: MIDIIdentifier? @AppStorage(MIDIHelper.PrefKeys.midiInDisplayName) - var midiInSelectedDisplayName: String = MIDIHelper.Defaults.selectedDisplayName + var midiInSelectedDisplayName: String? @AppStorage(MIDIHelper.PrefKeys.midiOutID) - var midiOutSelectedID: MIDIIdentifier = .invalidMIDIIdentifier + var midiOutSelectedID: MIDIIdentifier? @AppStorage(MIDIHelper.PrefKeys.midiOutDisplayName) - var midiOutSelectedDisplayName: String = MIDIHelper.Defaults.selectedDisplayName + var midiOutSelectedDisplayName: String? init() { midiHelper.setup(midiManager: midiManager) @@ -55,14 +55,6 @@ struct EndpointPickersApp: App { .environmentObject(midiManager) .environmentObject(midiHelper) } - .onChange(of: midiManager.observableEndpoints.inputs) { _ in - print("Inputs changed in system") - midiOutSelectedIDChanged(to: midiOutSelectedID) - } - .onChange(of: midiManager.observableEndpoints.outputs) { _ in - print("Outputs changed in system") - midiInSelectedIDChanged(to: midiInSelectedID) - } .onChange(of: midiInSelectedID) { midiInSelectedIDChanged(to: $0) } .onChange(of: midiOutSelectedID) { midiOutSelectedIDChanged(to: $0) } } @@ -71,40 +63,14 @@ struct EndpointPickersApp: App { // MARK: - Helpers extension EndpointPickersApp { - private func midiInSelectedIDChanged(to newOutputEndpointID: MIDIIdentifier) { - // cache endpoint details persistently so we can show it in the event the endpoint disappears - if newOutputEndpointID == .invalidMIDIIdentifier { - midiInSelectedDisplayName = MIDIHelper.Defaults.selectedDisplayName - } else if let found = midiManager.observableEndpoints.outputs.first( - whereUniqueID: newOutputEndpointID, - fallbackDisplayName: midiInSelectedDisplayName - ) { - midiInSelectedDisplayName = found.displayName - // update ID in case it changed - if midiInSelectedID != found.uniqueID { midiInSelectedID = found.uniqueID } - } - - // update the connection + private func midiInSelectedIDChanged(to newOutputEndpointID: MIDIIdentifier?) { midiHelper.midiInUpdateConnection( selectedUniqueID: newOutputEndpointID, selectedDisplayName: midiInSelectedDisplayName ) } - private func midiOutSelectedIDChanged(to newInputEndpointID: MIDIIdentifier) { - // cache endpoint details persistently so we can show it in the event the endpoint disappears - if newInputEndpointID == .invalidMIDIIdentifier { - midiOutSelectedDisplayName = MIDIHelper.Defaults.selectedDisplayName - } else if let found = midiManager.observableEndpoints.inputs.first( - whereUniqueID: newInputEndpointID, - fallbackDisplayName: midiOutSelectedDisplayName - ) { - midiOutSelectedDisplayName = found.displayName - // update ID in case it changed - if midiOutSelectedID != found.uniqueID { midiOutSelectedID = found.uniqueID } - } - - // update the connection + private func midiOutSelectedIDChanged(to newInputEndpointID: MIDIIdentifier?) { midiHelper.midiOutUpdateConnection( selectedUniqueID: newInputEndpointID, selectedDisplayName: midiOutSelectedDisplayName diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/MIDIHelper.swift b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/MIDIHelper.swift index 1282e49e51..ba49ca81c5 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/MIDIHelper.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/MIDIHelper.swift @@ -24,7 +24,7 @@ final class MIDIHelper: ObservableObject { public func setup(midiManager: ObservableMIDIManager) { self.midiManager = midiManager - // update a local `@Published` property in response to when + // update a local `@Published` property in response to when // MIDI devices/endpoints change in system midiManager.notificationHandler = { [weak self] notif, _ in switch notif { @@ -79,24 +79,27 @@ final class MIDIHelper: ObservableObject { midiManager?.managedInputConnections[Tags.midiIn] } + // TODO: refactor as `.updatingInputConnection(withTag: String)` view modifier on MIDIOutputsPicker public func midiInUpdateConnection( - selectedUniqueID: MIDIIdentifier, - selectedDisplayName: String + selectedUniqueID: MIDIIdentifier?, + selectedDisplayName: String? ) { guard let midiInputConnection else { return } - guard selectedUniqueID != .invalidMIDIIdentifier else { + guard let selectedUniqueID = selectedUniqueID, + let selectedDisplayName = selectedDisplayName, + selectedUniqueID != .invalidMIDIIdentifier + else { midiInputConnection.removeAllOutputs() return } - if midiInputConnection.outputsCriteria != [ - .uniqueIDWithFallback(id: selectedUniqueID, fallbackDisplayName: selectedDisplayName) - ] { + let criterium: MIDIEndpointIdentity = .uniqueIDWithFallback( + id: selectedUniqueID, fallbackDisplayName: selectedDisplayName + ) + if midiInputConnection.outputsCriteria != [criterium] { midiInputConnection.removeAllOutputs() - midiInputConnection.add(outputs: [ - .uniqueIDWithFallback(id: selectedUniqueID, fallbackDisplayName: selectedDisplayName) - ]) + midiInputConnection.add(outputs: [criterium]) } } @@ -106,24 +109,28 @@ final class MIDIHelper: ObservableObject { midiManager?.managedOutputConnections[Tags.midiOut] } + // TODO: refactor as `.updatingOutputConnection(withTag: String)` view modifier on MIDIInputsPicker public func midiOutUpdateConnection( - selectedUniqueID: MIDIIdentifier, - selectedDisplayName: String + selectedUniqueID: MIDIIdentifier?, + selectedDisplayName: String? ) { guard let midiOutputConnection else { return } - guard selectedUniqueID != .invalidMIDIIdentifier else { + guard let selectedUniqueID = selectedUniqueID, + let selectedDisplayName = selectedDisplayName, + selectedUniqueID != .invalidMIDIIdentifier + else { midiOutputConnection.removeAllInputs() return } - if midiOutputConnection.inputsCriteria != [ - .uniqueIDWithFallback(id: selectedUniqueID, fallbackDisplayName: selectedDisplayName) - ] { + let criterium: MIDIEndpointIdentity = .uniqueIDWithFallback( + id: selectedUniqueID, + fallbackDisplayName: selectedDisplayName + ) + if midiOutputConnection.inputsCriteria != [criterium] { midiOutputConnection.removeAllInputs() - midiOutputConnection.add(inputs: [ - .uniqueIDWithFallback(id: selectedUniqueID, fallbackDisplayName: selectedDisplayName) - ]) + midiOutputConnection.add(inputs: [criterium]) } } @@ -190,7 +197,7 @@ final class MIDIHelper: ObservableObject { public private(set) var virtualsExist: Bool = false private func updateVirtualsExist() { - virtualsExist = + virtualsExist = midiTestIn1 != nil && midiTestIn2 != nil && midiTestOut1 != nil && diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift index 7b2ecb33a4..c538109c21 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/iOS/ContentView-iOS.swift @@ -7,17 +7,18 @@ #if os(iOS) import MIDIKitIO +import MIDIKitUI import SwiftUI struct ContentView: View { @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 { @@ -64,11 +65,20 @@ struct ContentView: View { private var endpointSelectionSection: some View { Section() { - MIDIEndpointSelectionView( - midiInSelectedID: $midiInSelectedID, - midiInSelectedDisplayName: $midiInSelectedDisplayName, - midiOutSelectedID: $midiOutSelectedID, - midiOutSelectedDisplayName: $midiOutSelectedDisplayName + 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 { @@ -143,10 +153,7 @@ struct ContentView: View { extension ContentView { private var isMIDIOutDisabled: Bool { midiOutSelectedID == .invalidMIDIIdentifier || - !midiManager.observableEndpoints.inputs.contains( - whereUniqueID: midiOutSelectedID, - fallbackDisplayName: midiOutSelectedDisplayName - ) + midiOutSelectedID == nil } func sendToConnection(_ event: MIDIEvent) { diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift index 42fbd5b463..40e8cc1aef 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift @@ -7,17 +7,18 @@ #if os(macOS) import MIDIKitIO +import MIDIKitUI import SwiftUI struct ContentView: View { @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 { @@ -54,9 +55,12 @@ struct ContentView: View { private var midiInConnectionView: some View { GroupBox(label: Text("MIDI In Connection")) { - MIDIInSelectionView( - midiInSelectedID: $midiInSelectedID, - midiInSelectedDisplayName: $midiInSelectedDisplayName + MIDIOutputsPicker( + title: "MIDI In", + selectionID: $midiInSelectedID, + selectionDisplayName: $midiInSelectedDisplayName, + showIcons: true, + hideOwned: false ) .padding([.leading, .trailing], 60) @@ -69,9 +73,12 @@ struct ContentView: View { private var midiOutConnectionView: some View { GroupBox(label: Text("MIDI Out Connection")) { - MIDIOutSelectionView( - midiOutSelectedID: $midiOutSelectedID, - midiOutSelectedDisplayName: $midiOutSelectedDisplayName + MIDIInputsPicker( + title: "MIDI Out", + selectionID: $midiOutSelectedID, + selectionDisplayName: $midiOutSelectedDisplayName, + showIcons: true, + hideOwned: false ) .padding([.leading, .trailing], 60) @@ -145,10 +152,7 @@ struct ContentView: View { extension ContentView { private var isMIDIOutDisabled: Bool { midiOutSelectedID == .invalidMIDIIdentifier || - !midiManager.observableEndpoints.inputs.contains( - whereUniqueID: midiOutSelectedID, - fallbackDisplayName: midiOutSelectedDisplayName - ) + midiOutSelectedID == nil } private func sendToConnection(_ event: MIDIEvent) { From 127afcbece802288c3c809cad70fbbfa156bdd3d Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 06:20:57 -0800 Subject: [PATCH 58/64] Added connection-updating view modifiers to MIDIKitUI controls --- Sources/MIDIKitUI/MIDIEndpointsList.swift | 108 +++++-------- Sources/MIDIKitUI/MIDIEndpointsPicker.swift | 106 +++++-------- Sources/MIDIKitUI/MIDIKitUI Protocols.swift | 165 ++++++++++++++++++++ 3 files changed, 249 insertions(+), 130 deletions(-) create mode 100644 Sources/MIDIKitUI/MIDIKitUI Protocols.swift diff --git a/Sources/MIDIKitUI/MIDIEndpointsList.swift b/Sources/MIDIKitUI/MIDIEndpointsList.swift index 4c7894efa5..80d76b815b 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsList.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsList.swift @@ -10,8 +10,10 @@ import MIDIKitIO import SwiftUI @available(macOS 11.0, iOS 14.0, *) -struct MIDIEndpointsList: View -where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdentifier { +struct MIDIEndpointsList: View, MIDIEndpointsSelectable +where Endpoint: MIDIEndpoint & Hashable & Identifiable, + Endpoint.ID == MIDIIdentifier +{ private weak var midiManager: ObservableMIDIManager? var endpoints: [Endpoint] @@ -20,7 +22,7 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent @Binding var selectionDisplayName: String? let showIcons: Bool - @State private var ids: [MIDIIdentifier] = [] + @State var ids: [MIDIIdentifier] = [] init( endpoints: [Endpoint], @@ -38,7 +40,7 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent self.midiManager = midiManager // pre-populate IDs - _ids = State(initialValue: generateIDs(endpoints: endpoints, maskedFilter: maskedFilter)) + _ids = State(initialValue: generateIDs(endpoints: endpoints, maskedFilter: maskedFilter, midiManager: midiManager)) } public var body: some View { @@ -54,23 +56,23 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent } .onAppear { updateID(endpoints: endpoints) - updateIDs(endpoints: endpoints, maskedFilter: maskedFilter) + ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter, midiManager: midiManager) } .onChange(of: maskedFilter) { newValue in - updateIDs(endpoints: endpoints, maskedFilter: newValue) + ids = generateIDs(endpoints: endpoints, maskedFilter: newValue, midiManager: midiManager) } .onChange(of: endpoints) { newValue in updateID(endpoints: newValue) - updateIDs(endpoints: newValue, maskedFilter: maskedFilter) + ids = generateIDs(endpoints: newValue, maskedFilter: maskedFilter, midiManager: midiManager) } .onChange(of: selectionID) { newValue in - updateIDs(endpoints: endpoints, maskedFilter: maskedFilter) + ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter, midiManager: midiManager) guard let selectionID = newValue else { selectionDisplayName = nil return } - if let dn = endpoint(for: selectionID)?.displayName { - selectionDisplayName = dn + if let displayName = endpoint(for: selectionID)?.displayName { + selectionDisplayName = displayName } } } @@ -85,58 +87,6 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent if self.selectionID != updatedDetails.id { self.selectionID = updatedDetails.id } } - /// Returns non-nil if properties require updating. - private 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 - } - - /// (Don't run from init.) - private func updateIDs( - endpoints: [Endpoint], - maskedFilter: MIDIEndpointMaskedFilter? - ) { - ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter) - } - - private func generateIDs( - endpoints: [Endpoint], - maskedFilter: MIDIEndpointMaskedFilter? - ) -> [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 - } - } - - private func endpoint(for id: MIDIIdentifier) -> Endpoint? { - endpoints.first(whereUniqueID: id) - } - private struct EndpointRow: View { let endpoint: Endpoint? @Binding var selectionDisplayName: String? @@ -196,7 +146,7 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent /// SwiftUI `List` view for selecting MIDI input endpoints. @available(macOS 11.0, iOS 14.0, *) -public struct MIDIInputsList: View { +public struct MIDIInputsList: View, _MIDIInputsSelectable { @EnvironmentObject private var midiManager: ObservableMIDIManager @Binding public var selectionID: MIDIIdentifier? @@ -204,6 +154,8 @@ public struct MIDIInputsList: View { public var showIcons: Bool public var hideOwned: Bool + internal var updatingOutputConnectionWithTag: String? + public init( selectionID: Binding, selectionDisplayName: Binding, @@ -225,17 +177,28 @@ public struct MIDIInputsList: View { showIcons: showIcons, midiManager: midiManager ) - Text("Selected: \(selectionDisplayName ?? "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. @available(macOS 11.0, iOS 14.0, *) -public struct MIDIOutputsList: View { +public struct MIDIOutputsList: View, _MIDIOutputsSelectable { @EnvironmentObject private var midiManager: ObservableMIDIManager @Binding public var selectionID: MIDIIdentifier? @@ -243,6 +206,8 @@ public struct MIDIOutputsList: View { public var showIcons: Bool public var hideOwned: Bool + internal var updatingInputConnectionWithTag: String? + public init( selectionID: Binding, selectionDisplayName: Binding, @@ -264,12 +229,23 @@ public struct MIDIOutputsList: View { showIcons: showIcons, midiManager: midiManager ) - Text("Selected: \(selectionDisplayName ?? "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) + } } #endif diff --git a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift index bfc00f4cc8..5a1166da87 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift @@ -10,8 +10,10 @@ import MIDIKitIO import SwiftUI @available(macOS 11.0, iOS 14.0, *) -struct MIDIEndpointsPicker: View -where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdentifier { +struct MIDIEndpointsPicker: View, MIDIEndpointsSelectable +where Endpoint: MIDIEndpoint & Hashable & Identifiable, + Endpoint.ID == MIDIIdentifier +{ private weak var midiManager: ObservableMIDIManager? let title: String @@ -21,7 +23,7 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent @Binding var selectionDisplayName: String? var showIcons: Bool - @State private var ids: [MIDIIdentifier] = [] + @State var ids: [MIDIIdentifier] = [] init( title: String, @@ -41,7 +43,7 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent self.midiManager = midiManager // pre-populate IDs - _ids = State(initialValue: generateIDs(endpoints: endpoints, maskedFilter: maskedFilter)) + _ids = State(initialValue: generateIDs(endpoints: endpoints, maskedFilter: maskedFilter, midiManager: midiManager)) } public var body: some View { @@ -60,23 +62,23 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent } .onAppear { updateID(endpoints: endpoints) - updateIDs(endpoints: endpoints, maskedFilter: maskedFilter) + ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter, midiManager: midiManager) } .onChange(of: maskedFilter) { newValue in - updateIDs(endpoints: endpoints, maskedFilter: newValue) + ids = generateIDs(endpoints: endpoints, maskedFilter: newValue, midiManager: midiManager) } .onChange(of: endpoints) { newValue in updateID(endpoints: newValue) - updateIDs(endpoints: newValue, maskedFilter: maskedFilter) + ids = generateIDs(endpoints: newValue, maskedFilter: maskedFilter, midiManager: midiManager) } .onChange(of: selectionID) { newValue in - updateIDs(endpoints: endpoints, maskedFilter: maskedFilter) + ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter, midiManager: midiManager) guard let selectionID = newValue else { selectionDisplayName = nil return } - if let dn = endpoint(for: selectionID)?.displayName { - selectionDisplayName = dn + if let displayName = endpoint(for: selectionID)?.displayName { + selectionDisplayName = displayName } } } @@ -91,58 +93,6 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent if self.selectionID != updatedDetails.id { self.selectionID = updatedDetails.id } } - /// Returns non-nil if properties require updating. - private 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 - } - - /// (Don't run from init.) - private func updateIDs( - endpoints: [Endpoint], - maskedFilter: MIDIEndpointMaskedFilter? - ) { - ids = generateIDs(endpoints: endpoints, maskedFilter: maskedFilter) - } - - private func generateIDs( - endpoints: [Endpoint], - maskedFilter: MIDIEndpointMaskedFilter? - ) -> [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 - } - } - - private func endpoint(for id: MIDIIdentifier) -> Endpoint? { - endpoints.first(whereUniqueID: id) - } - private struct EndpointRow: View { let endpoint: Endpoint? @Binding var selectionDisplayName: String? @@ -206,7 +156,7 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, Endpoint.ID == MIDIIdent /// SwiftUI `Picker` view for selecting MIDI input endpoints. @available(macOS 11.0, iOS 14.0, *) -public struct MIDIInputsPicker: View { +public struct MIDIInputsPicker: View, _MIDIInputsSelectable { @EnvironmentObject private var midiManager: ObservableMIDIManager public var title: String @@ -215,6 +165,8 @@ public struct MIDIInputsPicker: View { public var showIcons: Bool public var hideOwned: Bool + internal var updatingOutputConnectionWithTag: String? + public init( title: String, selectionID: Binding, @@ -239,16 +191,28 @@ public struct MIDIInputsPicker: View { 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. @available(macOS 11.0, iOS 14.0, *) -public struct MIDIOutputsPicker: View { +public struct MIDIOutputsPicker: View, _MIDIOutputsSelectable { @EnvironmentObject private var midiManager: ObservableMIDIManager public var title: String @@ -257,6 +221,8 @@ public struct MIDIOutputsPicker: View { public var showIcons: Bool public var hideOwned: Bool + internal var updatingInputConnectionWithTag: String? + public init( title: String, selectionID: Binding, @@ -281,11 +247,23 @@ public struct MIDIOutputsPicker: View { 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) + } } #endif diff --git a/Sources/MIDIKitUI/MIDIKitUI Protocols.swift b/Sources/MIDIKitUI/MIDIKitUI Protocols.swift new file mode 100644 index 0000000000..b8712ffd72 --- /dev/null +++ b/Sources/MIDIKitUI/MIDIKitUI Protocols.swift @@ -0,0 +1,165 @@ +// +// MIDIKitUI Protocols.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +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]) + } + } +} From 2ee0025f35b8fd6e3fc38ff00d9850227664d206 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 07:18:35 -0800 Subject: [PATCH 59/64] Updated `EndpointPickers` example project --- .../EndpointPickers/EndpointPickersApp.swift | 30 ------------ .../EndpointPickers/MIDIHelper.swift | 49 ------------------- .../macOS/ContentView-macOS.swift | 2 + 3 files changed, 2 insertions(+), 79 deletions(-) diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/EndpointPickersApp.swift b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/EndpointPickersApp.swift index bd0968c29d..674d1b428d 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/EndpointPickersApp.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/EndpointPickersApp.swift @@ -32,16 +32,6 @@ struct EndpointPickersApp: App { init() { midiHelper.setup(midiManager: midiManager) - - // restore saved MIDI endpoint selections and connections - midiHelper.midiInUpdateConnection( - selectedUniqueID: midiInSelectedID, - selectedDisplayName: midiInSelectedDisplayName - ) - midiHelper.midiOutUpdateConnection( - selectedUniqueID: midiOutSelectedID, - selectedDisplayName: midiOutSelectedDisplayName - ) } var body: some Scene { @@ -55,25 +45,5 @@ struct EndpointPickersApp: App { .environmentObject(midiManager) .environmentObject(midiHelper) } - .onChange(of: midiInSelectedID) { midiInSelectedIDChanged(to: $0) } - .onChange(of: midiOutSelectedID) { midiOutSelectedIDChanged(to: $0) } - } -} - -// MARK: - Helpers - -extension EndpointPickersApp { - private func midiInSelectedIDChanged(to newOutputEndpointID: MIDIIdentifier?) { - midiHelper.midiInUpdateConnection( - selectedUniqueID: newOutputEndpointID, - selectedDisplayName: midiInSelectedDisplayName - ) - } - - private func midiOutSelectedIDChanged(to newInputEndpointID: MIDIIdentifier?) { - midiHelper.midiOutUpdateConnection( - selectedUniqueID: newInputEndpointID, - selectedDisplayName: midiOutSelectedDisplayName - ) } } diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/MIDIHelper.swift b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/MIDIHelper.swift index ba49ca81c5..5c091ac369 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/MIDIHelper.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/MIDIHelper.swift @@ -79,61 +79,12 @@ final class MIDIHelper: ObservableObject { midiManager?.managedInputConnections[Tags.midiIn] } - // TODO: refactor as `.updatingInputConnection(withTag: String)` view modifier on MIDIOutputsPicker - public func midiInUpdateConnection( - selectedUniqueID: MIDIIdentifier?, - selectedDisplayName: String? - ) { - guard let midiInputConnection 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]) - } - } - // MARK: - MIDI Output Connection public var midiOutputConnection: MIDIOutputConnection? { midiManager?.managedOutputConnections[Tags.midiOut] } - // TODO: refactor as `.updatingOutputConnection(withTag: String)` view modifier on MIDIInputsPicker - public func midiOutUpdateConnection( - selectedUniqueID: MIDIIdentifier?, - selectedDisplayName: String? - ) { - guard let midiOutputConnection 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: - Test Virtual Endpoints public var midiTestIn1: MIDIInput? { diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift index 40e8cc1aef..66d21e3c69 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/macOS/ContentView-macOS.swift @@ -62,6 +62,7 @@ struct ContentView: View { showIcons: true, hideOwned: false ) + .updatingInputConnection(withTag: MIDIHelper.Tags.midiIn) .padding([.leading, .trailing], 60) Toggle( @@ -80,6 +81,7 @@ struct ContentView: View { showIcons: true, hideOwned: false ) + .updatingOutputConnection(withTag: MIDIHelper.Tags.midiOut) .padding([.leading, .trailing], 60) HStack { From e9219c6c8e16be3480b1d93dd4cefa0306fd1890 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 07:27:24 -0800 Subject: [PATCH 60/64] Updated `MIDIEventLogger` example project --- .../MIDIEventLogger.xcodeproj/project.pbxproj | 7 ---- .../MIDIEventLogger/MIDIHelper.swift | 38 ------------------- .../MIDIEventLogger/Views/ContentView.swift | 11 ------ .../Views/ReceiveMIDIEventsView.swift | 16 ++------ 4 files changed, 3 insertions(+), 69 deletions(-) diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj index 3878a6f8b0..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 */; }; @@ -56,7 +55,6 @@ buildActionMask = 2147483647; files = ( E29AC90C285BEFB1009D1C2C /* MIDIKit in Frameworks */, - E26A25CD26C5876400FFCF40 /* MIDIKit in Frameworks */, E26A25C926C5875A00FFCF40 /* OTCore in Frameworks */, E26604E029888DA500177FC9 /* SwiftRadix in Frameworks */, ); @@ -132,7 +130,6 @@ name = MIDIEventLogger; packageProductDependencies = ( E26A25C826C5875A00FFCF40 /* OTCore */, - E26A25CC26C5876400FFCF40 /* MIDIKit */, E29AC90B285BEFB1009D1C2C /* MIDIKit */, E26604DF29888DA500177FC9 /* SwiftRadix */, ); @@ -466,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/MIDIHelper.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift index 303f931d95..f1f556e268 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift @@ -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 365ac1e86e..6c16071442 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ContentView.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ContentView.swift @@ -65,9 +65,6 @@ struct ContentView: View { .onAppear { setInputConnectionToVirtual() } - .onChange(of: midiInputConnectionID) { _ in - updateInputConnection() - } } // MARK: - Helper Methods @@ -79,14 +76,6 @@ struct ContentView: View { midiInputConnectionDisplayName = midiOutputEndpoint.displayName } - /// Update the MIDI manager's input connection to connect to the selected output endpoint. - func updateInputConnection() { - logger.debug( - "Updating input connection to endpoint: \(midiInputConnectionDisplayName?.quoted ?? "None")" - ) - midiHelper.updateInputConnection(selectedUniqueID: midiInputConnectionID) - } - /// Send a MIDI event using our virtual output endpoint. func sendEvent(_ event: MIDIEvent) { logIfThrowsError { diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ReceiveMIDIEventsView.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ReceiveMIDIEventsView.swift index b779da7de5..96e7752fce 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ReceiveMIDIEventsView.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/Views/ReceiveMIDIEventsView.swift @@ -36,22 +36,12 @@ extension ContentView { GroupBox(label: Text("Source: Connection")) { MIDIOutputsPicker( title: "", - selection: $midiInputConnectionID, - cachedSelectionName: $midiInputConnectionDisplayName, + selectionID: $midiInputConnectionID, + selectionDisplayName: $midiInputConnectionDisplayName, showIcons: true, hideOwned: false ) -// Picker("", selection: $midiInputConnectionEndpoint) { -// Text("None") -// .tag(MIDIOutputEndpoint?.none) -// -// VStack { Divider().padding(.leading) } -// -// ForEach(midiManager.endpoints.outputs) { -// Text("🎹 " + ($0.displayName)) -// .tag(MIDIOutputEndpoint?.some($0)) -// } -// } + .updatingInputConnection(withTag: ConnectionTags.inputConnectionTag) .padding() .frame(maxWidth: 400) .frame( From 82e69cc9534ec846f0a9f15f76a709c06d2ca3e1 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 07:37:15 -0800 Subject: [PATCH 61/64] Fixed tvOS and watchOS build --- Sources/MIDIKitUI/MIDIKitUI Protocols.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/MIDIKitUI/MIDIKitUI Protocols.swift b/Sources/MIDIKitUI/MIDIKitUI Protocols.swift index b8712ffd72..5fa94c1cfe 100644 --- a/Sources/MIDIKitUI/MIDIKitUI Protocols.swift +++ b/Sources/MIDIKitUI/MIDIKitUI Protocols.swift @@ -4,6 +4,8 @@ // © 2021-2023 Steffan Andrews • Licensed under MIT License // +#if canImport(SwiftUI) && !os(tvOS) && !os(watchOS) + import SwiftUI import MIDIKitIO @@ -163,3 +165,5 @@ extension _MIDIOutputsSelectable { } } } + +#endif From 264ece055e92be1e688be3293d75f4735c5f39d5 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 07:48:11 -0800 Subject: [PATCH 62/64] Updated docs --- .../MIDIKit.docc/MIDIKitUI/MIDIKitUI-Internals.md | 9 +++++++++ Sources/MIDIKit/MIDIKit.docc/MIDIKitUI/MIDIKitUI.md | 2 ++ Sources/MIDIKitUI/MIDIKitUI.docc/MIDIKitUI-Internals.md | 9 +++++++++ Sources/MIDIKitUI/MIDIKitUI.docc/MIDIKitUI.md | 1 + Sources/MIDIKitUI/MIDIKitUI.swift | 1 - 5 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 Sources/MIDIKit/MIDIKit.docc/MIDIKitUI/MIDIKitUI-Internals.md create mode 100644 Sources/MIDIKitUI/MIDIKitUI.docc/MIDIKitUI-Internals.md 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/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 From 9b8bc351482145d9ddf45a39abb72f09d32b3bf3 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 08:02:17 -0800 Subject: [PATCH 63/64] Updated `MIDIKitUIExample` example project --- .../MIDIKitUIExample/ListsExampleView.swift | 6 ++++++ .../MIDIKitUIExample/PickersExampleView.swift | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/ListsExampleView.swift b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/ListsExampleView.swift index f12e998fb9..297def13d6 100644 --- a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/ListsExampleView.swift +++ b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/ListsExampleView.swift @@ -87,6 +87,9 @@ struct ListsExampleView: View { showIcons: showIcons, hideOwned: hideCreated ) + // note: supply a non-nil tag to auto-update an output connection in MIDIManager + .updatingOutputConnection(withTag: nil) + #if os(macOS) .listStyle(.bordered(alternatesRowBackgrounds: true)) #endif @@ -99,6 +102,9 @@ struct ListsExampleView: View { showIcons: showIcons, hideOwned: hideCreated ) + // note: supply a non-nil tag to auto-update an input connection in MIDIManager + .updatingInputConnection(withTag: nil) + #if os(macOS) .listStyle(.bordered(alternatesRowBackgrounds: true)) #endif diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/PickersExampleView.swift b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/PickersExampleView.swift index 460d1de3d2..a999b397dc 100644 --- a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/PickersExampleView.swift +++ b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/PickersExampleView.swift @@ -115,6 +115,8 @@ struct PickersExampleView: View { showIcons: showIcons, hideOwned: hideCreated ) + // note: supply a non-nil tag to auto-update an output connection in MIDIManager + .updatingOutputConnection(withTag: nil) .pickerStyle(selection: pickerStyle) } @@ -126,6 +128,8 @@ struct PickersExampleView: View { showIcons: showIcons, hideOwned: hideCreated ) + // note: supply a non-nil tag to auto-update an input connection in MIDIManager + .updatingInputConnection(withTag: nil) .pickerStyle(selection: pickerStyle) } } From 9f31f10b5e0c3378ad741716b45810def29fbad1 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 10 Nov 2023 08:06:21 -0800 Subject: [PATCH 64/64] Updated docs --- Sources/MIDIKitUI/MIDIEndpointsList.swift | 14 ++++++++++++++ Sources/MIDIKitUI/MIDIEndpointsPicker.swift | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/Sources/MIDIKitUI/MIDIEndpointsList.swift b/Sources/MIDIKitUI/MIDIEndpointsList.swift index 80d76b815b..7aa0b75215 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsList.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsList.swift @@ -145,6 +145,13 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, } /// 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, _MIDIInputsSelectable { @EnvironmentObject private var midiManager: ObservableMIDIManager @@ -197,6 +204,13 @@ public struct MIDIInputsList: View, _MIDIInputsSelectable { } /// 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, _MIDIOutputsSelectable { @EnvironmentObject private var midiManager: ObservableMIDIManager diff --git a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift index 5a1166da87..6ebbd3e1bb 100644 --- a/Sources/MIDIKitUI/MIDIEndpointsPicker.swift +++ b/Sources/MIDIKitUI/MIDIEndpointsPicker.swift @@ -155,6 +155,13 @@ where Endpoint: MIDIEndpoint & Hashable & Identifiable, } /// 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, _MIDIInputsSelectable { @EnvironmentObject private var midiManager: ObservableMIDIManager @@ -211,6 +218,13 @@ public struct MIDIInputsPicker: View, _MIDIInputsSelectable { } /// 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, _MIDIOutputsSelectable { @EnvironmentObject private var midiManager: ObservableMIDIManager