From ba33d0beb3bab294cd0f80f6c777e650c281c5c9 Mon Sep 17 00:00:00 2001 From: Gui Rambo Date: Sun, 31 Oct 2021 16:45:28 -0300 Subject: [PATCH 1/2] Implemented AsyncSequence peerEvents property that can be used to observe peer discovery and connection events with the new Swift Concurrency API --- .../MockMultipeerConnection.swift | 26 +++++++++ .../Public API/MultipeerTransceiver.swift | 54 +++++++++++++++++++ .../MultipeerKitTests/MultipeerKitTests.swift | 48 +++++++++++++++-- 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/Sources/MultipeerKit/Internal API/MockMultipeerConnection.swift b/Sources/MultipeerKit/Internal API/MockMultipeerConnection.swift index 7e58550..61c0d89 100644 --- a/Sources/MultipeerKit/Internal API/MockMultipeerConnection.swift +++ b/Sources/MultipeerKit/Internal API/MockMultipeerConnection.swift @@ -39,5 +39,31 @@ final class MockMultipeerConnection: MultipeerProtocol { func getLocalPeerId() -> String? { return localPeer.id } + + @discardableResult func findFakePeer(with id: String) -> Peer { + let fakePeer = Peer( + underlyingPeer: MCPeerID(displayName: id), + id: id, + name: id, + discoveryInfo: nil, + isConnected: false + ) + + didFindPeer?(fakePeer) + + return fakePeer + } + + func loseFakePeer(_ fakePeer: Peer) { + didLosePeer?(fakePeer) + } + + func connectFakePeer(_ fakePeer: Peer) { + didConnectToPeer?(fakePeer) + } + + func disconnectFakePeer(_ fakePeer: Peer) { + didDisconnectFromPeer?(fakePeer) + } } diff --git a/Sources/MultipeerKit/Public API/MultipeerTransceiver.swift b/Sources/MultipeerKit/Public API/MultipeerTransceiver.swift index b85fdcb..52bb504 100644 --- a/Sources/MultipeerKit/Public API/MultipeerTransceiver.swift +++ b/Sources/MultipeerKit/Public API/MultipeerTransceiver.swift @@ -163,12 +163,21 @@ public final class MultipeerTransceiver { public func invite(_ peer: Peer, with context: Data?, timeout: TimeInterval, completion: InvitationCompletionHandler?) { connection.invite(peer, with: context, timeout: timeout, completion: completion) } + + public enum PeerEvent: Hashable { + case found(Peer) + case lost(Peer) + case connected(Peer) + case disconnected(Peer) + } private func handlePeerAdded(_ peer: Peer) { guard !availablePeers.contains(peer) else { return } availablePeers.append(peer) peerAdded(peer) + + peerEventOccurred(.found(peer)) } private func handlePeerRemoved(_ peer: Peer) { @@ -176,18 +185,24 @@ public final class MultipeerTransceiver { availablePeers.remove(at: idx) peerRemoved(peer) + + peerEventOccurred(.lost(peer)) } private func handlePeerConnected(_ peer: Peer) { setConnected(true, on: peer) peerConnected(peer) + + peerEventOccurred(.connected(peer)) } private func handlePeerDisconnected(_ peer: Peer) { setConnected(false, on: peer) peerDisconnected(peer) + + peerEventOccurred(.disconnected(peer)) } private func setConnected(_ connected: Bool, on peer: Peer) { @@ -197,5 +212,44 @@ public final class MultipeerTransceiver { mutablePeer.isConnected = connected availablePeers[idx] = mutablePeer } + + // MARK: - Swift Concurrency Support + + private typealias PeerEventCallback = (PeerEvent) -> Void + + private var internalPeerEventCallbacks: [UUID: PeerEventCallback] = [:] + + private func addInternalPeerEventsCallback(with block: @escaping PeerEventCallback) -> UUID { + let id = UUID() + internalPeerEventCallbacks[id] = block + return id + } + + private func peerEventOccurred(_ event: PeerEvent) { + DispatchQueue.main.async { + self.internalPeerEventCallbacks.values.forEach { $0(event) } + } + } } + +@available(tvOS 13.0, *) +@available(iOS 13.0, *) +@available(macOS 10.15, *) +public extension MultipeerTransceiver { + + var peerEvents: AsyncStream { + AsyncStream { [weak self] continuation in + guard let self = self else { return } + + let id = self.addInternalPeerEventsCallback { event in + continuation.yield(event) + } + + continuation.onTermination = { @Sendable _ in + self.internalPeerEventCallbacks[id] = nil + } + } + } + +} diff --git a/Tests/MultipeerKitTests/MultipeerKitTests.swift b/Tests/MultipeerKitTests/MultipeerKitTests.swift index 4cdbc8d..a053697 100644 --- a/Tests/MultipeerKitTests/MultipeerKitTests.swift +++ b/Tests/MultipeerKitTests/MultipeerKitTests.swift @@ -47,10 +47,48 @@ final class MultipeerKitTests: XCTestCase { wait(for: [expect], timeout: 2) } + + @available(tvOS 13.0, *) + @available(iOS 13.0, *) + @available(macOS 10.15, *) + func testAsyncEventsStreamContinuesWithEachPeerEvent() throws { + let transceiver = makeMockTransceiver() + transceiver.resume() + + let exp = expectation(description: "Async Peer") + exp.expectedFulfillmentCount = 4 + + Task.detached { + // Used to ensure that events are yielded in the right order. + var currentEvent = 0 + + for await event in transceiver.peerEvents { + switch event { + case .found(let peer): + XCTAssertEqual(peer.id, "A") + XCTAssertEqual(currentEvent, 0) + case .connected(let peer): + XCTAssertEqual(peer.id, "A") + XCTAssertEqual(currentEvent, 1) + case .disconnected(let peer): + XCTAssertEqual(peer.id, "A") + XCTAssertEqual(currentEvent, 2) + case .lost(let peer): + XCTAssertEqual(peer.id, "A") + XCTAssertEqual(currentEvent, 3) + } + + currentEvent += 1 + exp.fulfill() + } + } + + let peer = transceiver.mockConnection.findFakePeer(with: "A") + transceiver.mockConnection.connectFakePeer(peer) + transceiver.mockConnection.disconnectFakePeer(peer) + transceiver.mockConnection.loseFakePeer(peer) + + wait(for: [exp], timeout: 2) + } - static var allTests = [ - ("testCallingResumeResumesConnection", testCallingResumeResumesConnection), - ("testCallingStopStopsConnection", testCallingStopStopsConnection), - ("testReceivingCustomPayload", testReceivingCustomPayload), - ] } From 07ba426dcde491a6448d53463177521a91f4379b Mon Sep 17 00:00:00 2001 From: Gui Rambo Date: Sun, 31 Oct 2021 16:53:33 -0300 Subject: [PATCH 2/2] Documented new async API --- .../Public API/MultipeerTransceiver.swift | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/Sources/MultipeerKit/Public API/MultipeerTransceiver.swift b/Sources/MultipeerKit/Public API/MultipeerTransceiver.swift index 52bb504..786acba 100644 --- a/Sources/MultipeerKit/Public API/MultipeerTransceiver.swift +++ b/Sources/MultipeerKit/Public API/MultipeerTransceiver.swift @@ -12,10 +12,10 @@ public final class MultipeerTransceiver { /// Called on the main queue when available peers have changed (new peers discovered or peers removed). public var availablePeersDidChange: ([Peer]) -> Void = { _ in } - /// Called on the main queue when a new peer discovered. + /// Called on the main queue when a new peer is discovered. public var peerAdded: (Peer) -> Void = { _ in } - /// Called on the main queue when a peer removed. + /// Called on the main queue when a previously discovered peer is no longer seen nearby. public var peerRemoved: (Peer) -> Void = { _ in } /// Called on the main queue when a connection is established with a peer. @@ -164,10 +164,16 @@ public final class MultipeerTransceiver { connection.invite(peer, with: context, timeout: timeout, completion: completion) } + /// Describes an event that has occurred with a remote peer. + /// You can receive a stream of events by `await`ing on the ``peerEvents`` async sequence property. public enum PeerEvent: Hashable { + /// A new peer has been discovered. case found(Peer) + /// A previously discovered peer is no longer detected nearby. case lost(Peer) + /// A peer is now connected. case connected(Peer) + /// A peer is now disconnected. case disconnected(Peer) } @@ -238,6 +244,24 @@ public final class MultipeerTransceiver { @available(macOS 10.15, *) public extension MultipeerTransceiver { + /// An `AsyncStream` that you can `await` on in order to receive peer discovery and connection events as they occur. + /// + /// ## Example: + /// + /// ```swift + /// for await event in transceiver.peerEvents { + /// switch event { + /// case .found(let peer): + /// print("Peer \(peer.name) was found.") + /// case .lost(let peer): + /// print("Peer \(peer.name) was lost.") + /// case .connected(let peer): + /// print("Peer \(peer.name) is now connected.") + /// case .disconnected(let peer): + /// print("Peer \(peer.name) is now disconnected.") + /// } + /// } + /// ``` var peerEvents: AsyncStream { AsyncStream { [weak self] continuation in guard let self = self else { return }