From 25c0714d5b704948497254e8832070706b079698 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 19 Apr 2022 19:04:00 -0700 Subject: [PATCH 1/5] Endpoints: added `displayName` cached property --- .../MIDIEventLogger/ContentView SubViews.swift | 2 +- Examples/MIDIEventLogger/MIDIEventLogger/ContentView.swift | 2 +- .../IO/Objects/Endpoint/AnyEndpoint/AnyEndpoint.swift | 3 +++ .../IO/Objects/Endpoint/InputEndpoint/InputEndpoint.swift | 7 +++++-- .../IO/Objects/Endpoint/MIDIIOEndpointProtocol.swift | 4 ++++ .../Objects/Endpoint/OutputEndpoint/OutputEndpoint.swift | 7 +++++-- 6 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Examples/MIDIEventLogger/MIDIEventLogger/ContentView SubViews.swift b/Examples/MIDIEventLogger/MIDIEventLogger/ContentView SubViews.swift index c6cdf6db27..22d8b89c2c 100644 --- a/Examples/MIDIEventLogger/MIDIEventLogger/ContentView SubViews.swift +++ b/Examples/MIDIEventLogger/MIDIEventLogger/ContentView SubViews.swift @@ -614,7 +614,7 @@ extension ContentView { VStack { Divider().padding(.leading) } ForEach(midiManager.endpoints.outputs) { - Text("🎹 " + ($0.getDisplayName() ?? $0.name)) + Text("🎹 " + ($0.displayName)) .tag(MIDI.IO.OutputEndpoint?.some($0)) } } diff --git a/Examples/MIDIEventLogger/MIDIEventLogger/ContentView.swift b/Examples/MIDIEventLogger/MIDIEventLogger/ContentView.swift index b36abc29a8..c0fc7125de 100644 --- a/Examples/MIDIEventLogger/MIDIEventLogger/ContentView.swift +++ b/Examples/MIDIEventLogger/MIDIEventLogger/ContentView.swift @@ -149,7 +149,7 @@ struct ContentView: View { guard let endpoint = midiInputConnectionEndpoint else { return } - let endpointName = (endpoint.getDisplayName() ?? endpoint.name).quoted + let endpointName = endpoint.displayName.quoted logger.debug("Setting up new input connection to \(endpointName).") do { diff --git a/Sources/MIDIKit/IO/Objects/Endpoint/AnyEndpoint/AnyEndpoint.swift b/Sources/MIDIKit/IO/Objects/Endpoint/AnyEndpoint/AnyEndpoint.swift index f0c942e0bb..d99e9a1e74 100644 --- a/Sources/MIDIKit/IO/Objects/Endpoint/AnyEndpoint/AnyEndpoint.swift +++ b/Sources/MIDIKit/IO/Objects/Endpoint/AnyEndpoint/AnyEndpoint.swift @@ -14,6 +14,8 @@ extension MIDI.IO { public let name: String + public let displayName: String + public typealias UniqueID = MIDI.IO.AnyUniqueID public let uniqueID: UniqueID @@ -40,6 +42,7 @@ extension MIDI.IO { self.coreMIDIObjectRef = base.coreMIDIObjectRef self.name = base.name + self.displayName = base.displayName self.uniqueID = .init(base.uniqueID.coreMIDIUniqueID) } diff --git a/Sources/MIDIKit/IO/Objects/Endpoint/InputEndpoint/InputEndpoint.swift b/Sources/MIDIKit/IO/Objects/Endpoint/InputEndpoint/InputEndpoint.swift index 257973596b..4f2c98a066 100644 --- a/Sources/MIDIKit/IO/Objects/Endpoint/InputEndpoint/InputEndpoint.swift +++ b/Sources/MIDIKit/IO/Objects/Endpoint/InputEndpoint/InputEndpoint.swift @@ -34,13 +34,16 @@ extension MIDI.IO { public internal(set) var name: String = "" + public internal(set) var displayName: String = "" + public internal(set) var uniqueID: UniqueID = 0 /// Update the cached properties internal mutating func update() { - self.name = (try? MIDI.IO.getName(of: coreMIDIObjectRef)) ?? "" - self.uniqueID = UniqueID(MIDI.IO.getUniqueID(of: coreMIDIObjectRef)) + self.name = getName() ?? "" + self.displayName = getDisplayName() ?? "" + self.uniqueID = getUniqueID() } diff --git a/Sources/MIDIKit/IO/Objects/Endpoint/MIDIIOEndpointProtocol.swift b/Sources/MIDIKit/IO/Objects/Endpoint/MIDIIOEndpointProtocol.swift index d006eb1f82..93a9a86af5 100644 --- a/Sources/MIDIKit/IO/Objects/Endpoint/MIDIIOEndpointProtocol.swift +++ b/Sources/MIDIKit/IO/Objects/Endpoint/MIDIIOEndpointProtocol.swift @@ -7,6 +7,10 @@ public protocol MIDIIOEndpointProtocol: MIDIIOObjectProtocol { + /// Display name of the endpoint. + /// This typically includes the model number and endpoint name. + var displayName: String { get } + // implemented in extension _MIDIIOEndpointProtocol /// Returns the entity the endpoint originates from. For virtual endpoints, this will return `nil`. diff --git a/Sources/MIDIKit/IO/Objects/Endpoint/OutputEndpoint/OutputEndpoint.swift b/Sources/MIDIKit/IO/Objects/Endpoint/OutputEndpoint/OutputEndpoint.swift index 38381bf076..4b207f4ad1 100644 --- a/Sources/MIDIKit/IO/Objects/Endpoint/OutputEndpoint/OutputEndpoint.swift +++ b/Sources/MIDIKit/IO/Objects/Endpoint/OutputEndpoint/OutputEndpoint.swift @@ -34,13 +34,16 @@ extension MIDI.IO { public internal(set) var name: String = "" + public internal(set) var displayName: String = "" + public internal(set) var uniqueID: UniqueID = 0 /// Update the cached properties internal mutating func update() { - self.name = (try? MIDI.IO.getName(of: coreMIDIObjectRef)) ?? "" - self.uniqueID = .init(MIDI.IO.getUniqueID(of: coreMIDIObjectRef)) + self.name = getName() ?? "" + self.displayName = getDisplayName() ?? "" + self.uniqueID = getUniqueID() } From 34912e7007444efa309fb9cd084bb4fdd6d4639f Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 19 Apr 2022 19:30:56 -0700 Subject: [PATCH 2/5] `EndpointIDCriteria`: added `uniqueIDWithFallback` case --- .../Core MIDI/Core MIDI Properties Get.swift | 4 +- .../Objects/Endpoint/EndpointIDCriteria.swift | 54 ++++++++++++++++--- .../Endpoint/Endpoint IDCriteria Tests.swift | 15 ++++++ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/Sources/MIDIKit/IO/Core MIDI/Core MIDI Properties Get.swift b/Sources/MIDIKit/IO/Core MIDI/Core MIDI Properties Get.swift index 5ff91358d2..ef4aa67f49 100644 --- a/Sources/MIDIKit/IO/Core MIDI/Core MIDI Properties Get.swift +++ b/Sources/MIDIKit/IO/Core MIDI/Core MIDI Properties Get.swift @@ -255,7 +255,7 @@ extension MIDI.IO { /// (Apple-recommended user-visible name) /// (`kMIDIPropertyDisplayName`) /// - /// For objects other than endpoints, the display name is the same as its `kMIDIPropertyName` value. + /// For objects other than endpoints, the display name is (sometimes) the same as its `kMIDIPropertyName` value. /// /// - Throws: `MIDI.IO.MIDIError` internal static func getDisplayName(of ref: MIDIObjectRef) throws -> String { @@ -553,7 +553,7 @@ extension MIDI.IO { // -------------------------------------------------------------- // | | | | Presentation: // *|•| | | kMIDIPropertyImage string (POSIX path to image file) - // *|•|•|•| kMIDIPropertyDisplayName string + // *| |?|•| kMIDIPropertyDisplayName string // -------------------------------------------------------------- // | | | | Audio: // *|•|•| | kMIDIPropertyPanDisruptsStereo int32 0/1 diff --git a/Sources/MIDIKit/IO/Objects/Endpoint/EndpointIDCriteria.swift b/Sources/MIDIKit/IO/Objects/Endpoint/EndpointIDCriteria.swift index a78493dc49..f0626f4539 100644 --- a/Sources/MIDIKit/IO/Objects/Endpoint/EndpointIDCriteria.swift +++ b/Sources/MIDIKit/IO/Objects/Endpoint/EndpointIDCriteria.swift @@ -8,6 +8,8 @@ extension MIDI.IO { /// Enum describing the criteria with which to identify endpoints. + /// + /// It is recommended to use `uniqueID` primarily. For added resiliency, it is also possible to use `uniqueID` with fallback criteria in the event the endpoint provider does not correctly restore its unique identifier number. public enum EndpointIDCriteria { /// Utilizes first endpoint matching the endpoint name. @@ -15,12 +17,19 @@ extension MIDI.IO { case name(String) /// Utilizes first endpoint matching the display name. - /// Use of this is discouraged outside of debugging, since multiple endpoints can potentially share the same name in the system. + /// Use of this is discouraged outside of debugging, since multiple endpoints can potentially share the same display name in the system. case displayName(String) - /// Endpoint matching the unique ID. + /// Endpoint matching the unique ID. (Recommended) + /// This is typically the primary piece of criteria that should be used to persistently identify a unique endpoint in the system. case uniqueID(T.UniqueID) + /// Priority is given to the endpoint matching the given unique ID. If an endpoint is not found with that ID, the first endpoint matching the display name is used. + /// This may be useful in the event an endpoint vendor does not correctly maintain its own unique identifier number persistently. + /// However it is still recommended to use the `uniqueID` exclusive case where possible and not rely on falling back to fuzzy criteria such as display name. + case uniqueIDWithFallback(id: T.UniqueID, + fallbackDisplayName: String) + } } @@ -52,6 +61,14 @@ extension MIDI.IO.EndpointIDCriteria: Equatable where T : MIDIIOObjectProtocol { guard case .uniqueID(let rhsUniqueID) = rhs else { return false } return lhsUniqueID.isEqual(to: rhsUniqueID) + case .uniqueIDWithFallback(id: let lhsUniqueID, + fallbackDisplayName: let lhsFallbackDisplayName): + guard case .uniqueIDWithFallback(id: let rhsUniqueID, + fallbackDisplayName: let rhsFallbackDisplayName) = rhs + else { return false } + + return lhsUniqueID.isEqual(to: rhsUniqueID) + && lhsFallbackDisplayName == rhsFallbackDisplayName } } @@ -75,6 +92,12 @@ extension MIDI.IO.EndpointIDCriteria: Hashable where T : MIDIIOObjectProtocol { hasher.combine("uniqueID") uniqueID.hash(into: &hasher) + case .uniqueIDWithFallback(id: let uniqueID, + fallbackDisplayName: let fallbackDisplayName): + hasher.combine("uniqueIDWithFallback") + uniqueID.hash(into: &hasher) + fallbackDisplayName.hash(into: &hasher) + } } @@ -92,9 +115,12 @@ extension MIDI.IO.EndpointIDCriteria: CustomStringConvertible { case .displayName(let displayName): return "EndpointDisplayName: \(displayName.quoted))" - case .uniqueID(let uID): - return "UniqueID: \(uID)" + case .uniqueID(let uniqueID): + return "UniqueID: \(uniqueID)" + case .uniqueIDWithFallback(id: let uniqueID, + fallbackDisplayName: let fallbackDisplayName): + return "UniqueID: \(uniqueID) with fallback EndpointDisplayName: \(fallbackDisplayName.quoted)" } } @@ -106,13 +132,18 @@ extension MIDI.IO.EndpointIDCriteria where T : MIDIIOEndpointProtocol { /// A MIDI endpoint. public static func endpoint(_ endpoint: T) -> Self { - .uniqueID(endpoint.uniqueID) + if !endpoint.displayName.isEmpty { + return .uniqueIDWithFallback(id: endpoint.uniqueID, + fallbackDisplayName: endpoint.displayName) + } else { + return .uniqueID(endpoint.uniqueID) + } } } -extension MIDI.IO.EndpointIDCriteria where T : MIDIIOObjectProtocol { +extension MIDI.IO.EndpointIDCriteria where T : MIDIIOEndpointProtocol { /// Uses the criteria to find the first match and returns it if found. internal func locate(in endpoints: [T]) -> T? { @@ -128,9 +159,16 @@ extension MIDI.IO.EndpointIDCriteria where T : MIDIIOObjectProtocol { .filter(displayName: endpointName) .first - case .uniqueID(let uID): + case .uniqueID(let uniqueID): + return endpoints + .first(whereUniqueID: uniqueID) + + case .uniqueIDWithFallback(id: let uniqueID, + fallbackDisplayName: let fallbackDisplayName): return endpoints - .first(whereUniqueID: uID) + .first(whereUniqueID: uniqueID, + fallbackDisplayName: fallbackDisplayName, + ignoringEmpty: true) } diff --git a/Tests/MIDIKitTests/IO/Objects/Endpoint/Endpoint IDCriteria Tests.swift b/Tests/MIDIKitTests/IO/Objects/Endpoint/Endpoint IDCriteria Tests.swift index 00a1bee0bb..59eb38dae2 100644 --- a/Tests/MIDIKitTests/IO/Objects/Endpoint/Endpoint IDCriteria Tests.swift +++ b/Tests/MIDIKitTests/IO/Objects/Endpoint/Endpoint IDCriteria Tests.swift @@ -20,6 +20,11 @@ final class EndpointIDCriteriaTests: XCTestCase { switch criteria { case .uniqueID(let uID): XCTAssertEqual(uID, 10000001) + + case .uniqueIDWithFallback(id: let uID, + fallbackDisplayName: _): + XCTAssertEqual(uID, 10000001) + default: XCTFail() } @@ -36,6 +41,11 @@ final class EndpointIDCriteriaTests: XCTestCase { switch criteria { case .uniqueID(let uID): XCTAssertEqual(uID, 10000001) + + case .uniqueIDWithFallback(id: let uID, + fallbackDisplayName: _): + XCTAssertEqual(uID, 10000001) + default: XCTFail() } @@ -53,6 +63,11 @@ final class EndpointIDCriteriaTests: XCTestCase { switch criteria { case .uniqueID(let uID): XCTAssertEqual(uID, 10000001) + + case .uniqueIDWithFallback(id: let uID, + fallbackDisplayName: _): + XCTAssertEqual(uID, 10000001) + default: XCTFail() } From 37b719b8182444eb14cb0b051fc97fce02b411de Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 19 Apr 2022 19:31:52 -0700 Subject: [PATCH 3/5] Simplified property cache getters --- Sources/MIDIKit/IO/Objects/Device/Device.swift | 7 ++++--- Sources/MIDIKit/IO/Objects/Entity/Entity.swift | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/MIDIKit/IO/Objects/Device/Device.swift b/Sources/MIDIKit/IO/Objects/Device/Device.swift index 5faf2f1ced..c8d3594618 100644 --- a/Sources/MIDIKit/IO/Objects/Device/Device.swift +++ b/Sources/MIDIKit/IO/Objects/Device/Device.swift @@ -33,7 +33,7 @@ extension MIDI.IO { // MARK: - Properties (Cached) - /// User-visible endpoint name. + /// User-visible device name. /// (`kMIDIPropertyName`) public internal(set) var name: String = "" @@ -44,8 +44,9 @@ extension MIDI.IO { /// Update the cached properties internal mutating func update() { - self.name = (try? MIDI.IO.getName(of: coreMIDIObjectRef)) ?? "" - self.uniqueID = .init(MIDI.IO.getUniqueID(of: coreMIDIObjectRef)) + self.name = getName() ?? "" + self.name = getDisplayName() ?? "" + self.uniqueID = getUniqueID() } diff --git a/Sources/MIDIKit/IO/Objects/Entity/Entity.swift b/Sources/MIDIKit/IO/Objects/Entity/Entity.swift index 7890351b35..b4f7116e73 100644 --- a/Sources/MIDIKit/IO/Objects/Entity/Entity.swift +++ b/Sources/MIDIKit/IO/Objects/Entity/Entity.swift @@ -42,8 +42,8 @@ extension MIDI.IO { /// Update the cached properties internal mutating func update() { - self.name = (try? MIDI.IO.getName(of: coreMIDIObjectRef)) ?? "" - self.uniqueID = .init(MIDI.IO.getUniqueID(of: coreMIDIObjectRef)) + self.name = getName() ?? "" + self.uniqueID = getUniqueID() } From 1bf9a7749af2aa21eff3d6d72073e8a8fd583a07 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 19 Apr 2022 19:33:11 -0700 Subject: [PATCH 4/5] Expanded custom sorted, first, and filter methods on object & endpoint collections --- .../MIDIIOObjectProtocol Collection.swift | 87 ++++- ...IDIIOObjectProtocol Collection Tests.swift | 367 ++++++++++++++++++ 2 files changed, 447 insertions(+), 7 deletions(-) create mode 100644 Tests/MIDIKitTests/IO/Objects/MIDIIOObjectProtocol/MIDIIOObjectProtocol Collection Tests.swift diff --git a/Sources/MIDIKit/IO/Objects/MIDIIOObjectProtocol/MIDIIOObjectProtocol Collection.swift b/Sources/MIDIKit/IO/Objects/MIDIIOObjectProtocol/MIDIIOObjectProtocol Collection.swift index c48ea9ac0b..39235d1a26 100644 --- a/Sources/MIDIKit/IO/Objects/MIDIIOObjectProtocol/MIDIIOObjectProtocol Collection.swift +++ b/Sources/MIDIKit/IO/Objects/MIDIIOObjectProtocol/MIDIIOObjectProtocol Collection.swift @@ -3,6 +3,8 @@ // MIDIKit • https://github.com/orchetect/MIDIKit // +// MARK: - sorted + extension Collection where Element : MIDIIOObjectProtocol { /// Returns the array sorted alphabetically by MIDI object name. @@ -17,30 +19,101 @@ extension Collection where Element : MIDIIOObjectProtocol { } +extension Collection where Element : MIDIIOEndpointProtocol { + + /// Returns the array sorted alphabetically by MIDI endpoint display name. + public func sortedByDisplayName() -> [Element] { + + self.sorted(by: { + $0.displayName + .localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending + }) + + } + +} + +// MARK: - first + +extension Collection where Element : MIDIIOObjectProtocol { + + /// Returns the first element matching the given name. + public func first(withName name: String, + ignoringEmpty: Bool = false) -> Element? { + + ignoringEmpty + ? first(where: { $0.name == name && !$0.name.isEmpty }) + : first(where: { $0.name == name }) + + } + +} + +extension Collection where Element : MIDIIOEndpointProtocol { + + /// Returns the first element matching the given name. + public func first(withDisplayName displayName: String, + ignoringEmpty: Bool = false) -> Element? { + + ignoringEmpty + ? first(where: { + guard let elementDisplayName = $0.getDisplayName(), + !elementDisplayName.isEmpty else { return false } + return elementDisplayName == displayName + }) + : first(where: { $0.displayName == displayName }) + + } + + /// Returns the element with matching unique ID. + /// If not found, the first element matching the given display name is returned. + public func first(whereUniqueID: Element.UniqueID, + fallbackDisplayName: String, + ignoringEmpty: Bool = false) -> Element? { + + first(whereUniqueID: whereUniqueID) ?? + first(withDisplayName: fallbackDisplayName, ignoringEmpty: ignoringEmpty) + + } + +} + extension Collection where Element : MIDIIOObjectProtocol { - /// Returns the element where uniqueID matches if found. + /// Returns the element with matching unique ID. public func first(whereUniqueID: Element.UniqueID) -> Element? { first(where: { $0.uniqueID.isEqual(to: whereUniqueID) }) } +} + +// MARK: - filter + +extension Collection where Element : MIDIIOObjectProtocol { + /// Returns all elements matching the given name. - public func filter(name: String) -> [Element] { + public func filter(name: String, + ignoringEmpty: Bool = false) -> [Element] { - filter { $0.name == name } + ignoringEmpty + ? filter { $0.name == name && !$0.name.isEmpty } + : filter { $0.name == name } } } -extension Collection where Element : MIDIIOObjectProtocol { +extension Collection where Element : MIDIIOEndpointProtocol { - /// Returns all elements matching all supplied parameters. - public func filter(displayName: String) -> [Element] { + /// Returns all elements matching the given display name. + public func filter(displayName: String, + ignoringEmpty: Bool = false) -> [Element] { - filter { $0.getDisplayName() == displayName } + ignoringEmpty + ? filter { (displayName == $0.displayName) && !$0.displayName.isEmpty } + : filter { $0.displayName == displayName } } diff --git a/Tests/MIDIKitTests/IO/Objects/MIDIIOObjectProtocol/MIDIIOObjectProtocol Collection Tests.swift b/Tests/MIDIKitTests/IO/Objects/MIDIIOObjectProtocol/MIDIIOObjectProtocol Collection Tests.swift new file mode 100644 index 0000000000..ed2e7ebf62 --- /dev/null +++ b/Tests/MIDIKitTests/IO/Objects/MIDIIOObjectProtocol/MIDIIOObjectProtocol Collection Tests.swift @@ -0,0 +1,367 @@ +// +// MIDIIOObjectProtocol Collection Tests.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// + +#if shouldTestCurrentPlatform + +import XCTest +@testable import MIDIKit + +final class MIDIIOObjectProtocolCollectionTests: XCTestCase { + + // MARK: - sorted + + func testSortedByName() { + + let elements: [MIDI.IO.InputEndpoint] = [ + .init(ref: 1000, name: "B", displayName: "C", uniqueID: -1000), + .init(ref: 1001, name: "C", displayName: "A", uniqueID: -1001), + .init(ref: 1002, name: "A", displayName: "B", uniqueID: -1002) + ] + + XCTAssertEqual( + elements.sortedByName(), + [ + .init(ref: 1002, name: "A", displayName: "B", uniqueID: -1002), + .init(ref: 1000, name: "B", displayName: "C", uniqueID: -1000), + .init(ref: 1001, name: "C", displayName: "A", uniqueID: -1001) + ]) + + } + + func testSortedByDisplayName() { + + let elements: [MIDI.IO.InputEndpoint] = [ + .init(ref: 1000, name: "B", displayName: "C", uniqueID: -1000), + .init(ref: 1001, name: "C", displayName: "A", uniqueID: -1001), + .init(ref: 1002, name: "A", displayName: "B", uniqueID: -1002) + ] + + XCTAssertEqual( + elements.sortedByDisplayName(), + [ + .init(ref: 1001, name: "C", displayName: "A", uniqueID: -1001), + .init(ref: 1002, name: "A", displayName: "B", uniqueID: -1002), + .init(ref: 1000, name: "B", displayName: "C", uniqueID: -1000) + ]) + + } + + // MARK: - first + + func testFirstWithName() { + + let elements: [MIDI.IO.InputEndpoint] = [ + .init(ref: 1000, name: "Port B", displayName: "C", uniqueID: -1000), + .init(ref: 1003, name: "Port A", displayName: "A", uniqueID: -1003), + .init(ref: 1001, name: "Port C", displayName: "A", uniqueID: -1001), + .init(ref: 1002, name: "Port A", displayName: "B", uniqueID: -1002), + .init(ref: 1004, name: "", displayName: "", uniqueID: -1004) + ] + + XCTAssertEqual( + elements.first(withName: "Port A"), + .init(ref: 1003, name: "Port A", displayName: "A", uniqueID: -1003) + ) + + XCTAssertNil( + elements.first(withName: "Port E") + ) + + } + + func testFirstWithNameIgnoringEmpty() { + + let elements: [MIDI.IO.InputEndpoint] = [ + .init(ref: 1000, name: "Port B", displayName: "C", uniqueID: -1000), + .init(ref: 1003, name: "Port A", displayName: "A", uniqueID: -1003), + .init(ref: 1001, name: "Port C", displayName: "A", uniqueID: -1001), + .init(ref: 1002, name: "Port A", displayName: "B", uniqueID: -1002), + .init(ref: 1004, name: "", displayName: "", uniqueID: -1004) + ] + + XCTAssertEqual( + elements.first(withName: "", ignoringEmpty: false), + .init(ref: 1004, name: "", displayName: "", uniqueID: -1004) + ) + + XCTAssertNil( + elements.first(withName: "", ignoringEmpty: true) + ) + + } + + func testFirstWithDisplayName() { + + let elements: [MIDI.IO.InputEndpoint] = [ + .init(ref: 1000, name: "Port B", displayName: "C", uniqueID: -1000), + .init(ref: 1003, name: "Port A", displayName: "A", uniqueID: -1003), + .init(ref: 1001, name: "Port C", displayName: "A", uniqueID: -1001), + .init(ref: 1002, name: "Port A", displayName: "B", uniqueID: -1002), + .init(ref: 1004, name: "Port D", displayName: "", uniqueID: -1004) + ] + + XCTAssertEqual( + elements.first(withDisplayName: "A"), + .init(ref: 1003, name: "Port A", displayName: "A", uniqueID: -1003) + ) + + XCTAssertEqual( + elements.first(withDisplayName: ""), + .init(ref: 1004, name: "Port D", displayName: "", uniqueID: -1004) + ) + + XCTAssertNil( + elements.first(withDisplayName: "E") + ) + + } + + func testFirstWithDisplayNameIgnoringEmpty() { + + let elements: [MIDI.IO.InputEndpoint] = [ + .init(ref: 1000, name: "Port B", displayName: "C", uniqueID: -1000), + .init(ref: 1003, name: "Port A", displayName: "A", uniqueID: -1003), + .init(ref: 1001, name: "Port C", displayName: "A", uniqueID: -1001), + .init(ref: 1002, name: "Port A", displayName: "B", uniqueID: -1002), + .init(ref: 1004, name: "Port D", displayName: "", uniqueID: -1004) + ] + + XCTAssertEqual( + elements.first(withDisplayName: "", ignoringEmpty: false), + .init(ref: 1004, name: "Port D", displayName: "", uniqueID: -1004) + ) + + XCTAssertNil( + elements.first(withDisplayName: "", ignoringEmpty: true) + ) + + } + + func testFirstWhereUniqueID() { + + let elements: [MIDI.IO.InputEndpoint] = [ + .init(ref: 1000, name: "Port B", displayName: "C", uniqueID: -1000), + .init(ref: 1003, name: "Port A", displayName: "A", uniqueID: -1003), + .init(ref: 1001, name: "Port C", displayName: "A", uniqueID: -1001), + .init(ref: 1002, name: "Port A", displayName: "B", uniqueID: -1002), + .init(ref: 1004, name: "", displayName: "", uniqueID: -1004) + ] + + XCTAssertEqual( + elements.first(whereUniqueID: -1002), + .init(ref: 1002, name: "Port A", displayName: "B", uniqueID: -1002) + ) + + XCTAssertNil( + elements.first(whereUniqueID: -2000) + ) + + } + + func testFirstWhereUniqueID_fallbackDisplayName() { + + let elements: [MIDI.IO.InputEndpoint] = [ + .init(ref: 1000, name: "Port B", displayName: "1", uniqueID: -1000), + .init(ref: 1003, name: "Port A", displayName: "2", uniqueID: -1003), + .init(ref: 1001, name: "Port C", displayName: "3", uniqueID: -1001), + .init(ref: 1002, name: "Port A", displayName: "4", uniqueID: -1002), + .init(ref: 1004, name: "", displayName: "", uniqueID: -1004) + ] + + // prioritize unique ID + XCTAssertEqual( + elements.first(whereUniqueID: -1002, fallbackDisplayName: "2"), + .init(ref: 1002, name: "Port A", displayName: "B", uniqueID: -1002) + ) + + XCTAssertEqual( + elements.first(whereUniqueID: -1002, fallbackDisplayName: "4"), + .init(ref: 1002, name: "Port A", displayName: "B", uniqueID: -1002) + ) + + XCTAssertEqual( + elements.first(whereUniqueID: -1002, fallbackDisplayName: ""), + .init(ref: 1002, name: "Port A", displayName: "B", uniqueID: -1002) + ) + + // ID not found, fall back to display name + XCTAssertEqual( + elements.first(whereUniqueID: -2000, fallbackDisplayName: "4"), + .init(ref: 1002, name: "Port A", displayName: "B", uniqueID: -1002) + ) + + // ID not found, fall back to display name + XCTAssertEqual( + elements.first(whereUniqueID: -2000, fallbackDisplayName: ""), + .init(ref: 1004, name: "", displayName: "", uniqueID: -1004) + ) + + // ID not found, display name not found + XCTAssertNil( + elements.first(whereUniqueID: -2000, fallbackDisplayName: "6") + ) + + // ID not found, fall back to display name. but ignore empty strings. + XCTAssertNil( + elements.first(whereUniqueID: -2000, + fallbackDisplayName: "", + ignoringEmpty: true) + ) + + } + + // MARK: - filter + + func testFilterName() { + + let elements: [MIDI.IO.InputEndpoint] = [ + .init(ref: 1000, name: "Port B", displayName: "1", uniqueID: -1000), + .init(ref: 1003, name: "Port A", displayName: "2", uniqueID: -1003), + .init(ref: 1001, name: "Port C", displayName: "3", uniqueID: -1001), + .init(ref: 1002, name: "Port A", displayName: "4", uniqueID: -1002), + .init(ref: 1004, name: "", displayName: "5", uniqueID: -1004) + ] + + XCTAssertEqual( + elements.filter(name: "Port A"), + [ + .init(ref: 1003, name: "Port A", displayName: "2", uniqueID: -1003), + .init(ref: 1002, name: "Port A", displayName: "4", uniqueID: -1002) + ] + ) + + XCTAssertEqual( + elements.filter(name: ""), + [ + .init(ref: 1004, name: "", displayName: "5", uniqueID: -1004) + ] + ) + + XCTAssertEqual( + elements.filter(name: "Port D"), + [] + ) + + } + + func testFilterNameIgnoringEmpty() { + + let elements: [MIDI.IO.InputEndpoint] = [ + .init(ref: 1000, name: "Port B", displayName: "1", uniqueID: -1000), + .init(ref: 1003, name: "Port A", displayName: "2", uniqueID: -1003), + .init(ref: 1001, name: "Port C", displayName: "3", uniqueID: -1001), + .init(ref: 1002, name: "Port A", displayName: "4", uniqueID: -1002), + .init(ref: 1004, name: "", displayName: "5", uniqueID: -1004) + ] + + XCTAssertEqual( + elements.filter(name: "Port A", ignoringEmpty: true), + [ + .init(ref: 1003, name: "Port A", displayName: "2", uniqueID: -1003), + .init(ref: 1002, name: "Port A", displayName: "4", uniqueID: -1002) + ] + ) + + XCTAssertEqual( + elements.filter(name: "", ignoringEmpty: false), + [ + .init(ref: 1004, name: "", displayName: "5", uniqueID: -1004) + ] + ) + + XCTAssertEqual( + elements.filter(name: "", ignoringEmpty: true), + [] + ) + + } + + func testFilterDisplayName() { + + let elements: [MIDI.IO.InputEndpoint] = [ + .init(ref: 1000, name: "Port B", displayName: "1", uniqueID: -1000), + .init(ref: 1003, name: "Port A", displayName: "2", uniqueID: -1003), + .init(ref: 1001, name: "Port C", displayName: "3", uniqueID: -1001), + .init(ref: 1002, name: "Port A", displayName: "2", uniqueID: -1002), + .init(ref: 1004, name: "Port D", displayName: "", uniqueID: -1004) + ] + + XCTAssertEqual( + elements.filter(displayName: "2"), + [ + .init(ref: 1003, name: "Port A", displayName: "2", uniqueID: -1003), + .init(ref: 1002, name: "Port A", displayName: "2", uniqueID: -1002) + ] + ) + + XCTAssertEqual( + elements.filter(displayName: ""), + [ + .init(ref: 1004, name: "Port D", displayName: "", uniqueID: -1004) + ] + ) + + XCTAssertEqual( + elements.filter(displayName: "5"), + [] + ) + + } + + func testFilterDisplayNameIgnoringEmpty() { + + let elements: [MIDI.IO.InputEndpoint] = [ + .init(ref: 1000, name: "Port B", displayName: "1", uniqueID: -1000), + .init(ref: 1003, name: "Port A", displayName: "2", uniqueID: -1003), + .init(ref: 1001, name: "Port C", displayName: "3", uniqueID: -1001), + .init(ref: 1002, name: "Port A", displayName: "2", uniqueID: -1002), + .init(ref: 1004, name: "Port D", displayName: "", uniqueID: -1004) + ] + + XCTAssertEqual( + elements.filter(displayName: "2", ignoringEmpty: true), + [ + .init(ref: 1003, name: "Port A", displayName: "2", uniqueID: -1003), + .init(ref: 1002, name: "Port A", displayName: "2", uniqueID: -1002) + ] + ) + + XCTAssertEqual( + elements.filter(displayName: "", ignoringEmpty: false), + [ + .init(ref: 1004, name: "Port D", displayName: "", uniqueID: -1004) + ] + ) + + XCTAssertEqual( + elements.filter(displayName: "", ignoringEmpty: true), + [] + ) + + } + +} + + +// MARK: - Utility + +extension MIDI.IO.InputEndpoint { + + /// Unit testing only: manually mock an endpoint with custom name, display name, and unique ID. + internal init(ref: MIDI.IO.CoreMIDIEndpointRef, + name: String, + displayName: String, + uniqueID: UniqueID) { + + self.init(ref) + self.name = name + self.displayName = displayName + self.uniqueID = uniqueID + + } + +} + +#endif From 1e6f60c1f98a8c1327be49131d7cd1e92e36e514 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 19 Apr 2022 23:00:59 -0700 Subject: [PATCH 5/5] `MIDI.IO.Manager.endpoints`: Added `inputsUnowned` and `unownedOutputs` --- Sources/MIDIKit/IO/Endpoints/Endpoints.swift | 51 +++++++++++++++++--- Sources/MIDIKit/IO/Manager/Manager.swift | 2 + 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/Sources/MIDIKit/IO/Endpoints/Endpoints.swift b/Sources/MIDIKit/IO/Endpoints/Endpoints.swift index 46a7093df0..7296f0a121 100644 --- a/Sources/MIDIKit/IO/Endpoints/Endpoints.swift +++ b/Sources/MIDIKit/IO/Endpoints/Endpoints.swift @@ -8,14 +8,20 @@ import Foundation // this protocol may not be necessary, it was experimental so that the `MIDI.IO.Manager.endpoints` property could be swapped out with a different Endpoints class with Combine support public protocol MIDIIOEndpointsProtocol { - /// List of MIDI output endpoints in the system + /// List of MIDI input endpoints in the system. + var inputs: [MIDI.IO.InputEndpoint] { get } + + /// List of MIDI input endpoints in the system omitting virtual endpoints owned by this `Manager` instance. + var inputsUnowned: [MIDI.IO.InputEndpoint] { get } + + /// List of MIDI output endpoints in the system. var outputs: [MIDI.IO.OutputEndpoint] { get } - /// List of MIDI input endpoints in the system - var inputs: [MIDI.IO.InputEndpoint] { get } + /// List of MIDI output endpoints in the system omitting virtual endpoints owned by this `Manager` instance. + var outputsUnowned: [MIDI.IO.OutputEndpoint] { get } /// 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. + /// This method does not need to be manually invoked, as it is called automatically by the `Manager` when MIDI system endpoints change. mutating func update() } @@ -25,9 +31,14 @@ extension MIDI.IO { /// Manages system MIDI endpoints information cache. public class Endpoints: NSObject, MIDIIOEndpointsProtocol { - public internal(set) dynamic var outputs: [OutputEndpoint] = [] + /// Weak reference to `Manager`. + internal weak var manager: MIDI.IO.Manager? = nil public internal(set) dynamic var inputs: [InputEndpoint] = [] + public internal(set) dynamic var inputsUnowned: [InputEndpoint] = [] + + public internal(set) dynamic var outputs: [OutputEndpoint] = [] + public internal(set) dynamic var outputsUnowned: [OutputEndpoint] = [] internal override init() { @@ -35,11 +46,39 @@ extension MIDI.IO { } + internal init(manager: Manager) { + + self.manager = manager + super.init() + + } + public func update() { - outputs = MIDI.IO.getSystemSourceEndpoints inputs = MIDI.IO.getSystemDestinationEndpoints + if let manager = manager { + inputsUnowned = inputs.filter { systemEndpoint in + !manager.managedInputs.contains(where: { + $0.value.uniqueID == systemEndpoint.uniqueID + }) + } + } else { + inputsUnowned = inputs + } + + outputs = MIDI.IO.getSystemSourceEndpoints + + if let manager = manager { + outputsUnowned = outputs.filter { systemEndpoint in + !manager.managedOutputs.contains(where: { + $0.value.uniqueID == systemEndpoint.uniqueID + }) + } + } else { + outputsUnowned = outputs + } + } } diff --git a/Sources/MIDIKit/IO/Manager/Manager.swift b/Sources/MIDIKit/IO/Manager/Manager.swift index c52b525a96..e361d3b7a5 100644 --- a/Sources/MIDIKit/IO/Manager/Manager.swift +++ b/Sources/MIDIKit/IO/Manager/Manager.swift @@ -125,6 +125,8 @@ extension MIDI.IO { super.init() + self.endpoints = Endpoints(manager: self) + addNetworkSessionObservers() }