Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Sources/MultipeerKit/Internal API/MockMultipeerConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

}
82 changes: 80 additions & 2 deletions Sources/MultipeerKit/Public API/MultipeerTransceiver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -163,31 +163,52 @@ 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)
}

/// 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)
}

private func handlePeerAdded(_ peer: Peer) {
guard !availablePeers.contains(peer) else { return }

availablePeers.append(peer)
peerAdded(peer)

peerEventOccurred(.found(peer))
}

private func handlePeerRemoved(_ peer: Peer) {
guard let idx = availablePeers.firstIndex(where: { $0.underlyingPeer == peer.underlyingPeer }) else { return }

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) {
Expand All @@ -197,5 +218,62 @@ 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 {

/// 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<PeerEvent> {
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
}
}
}

}
48 changes: 43 additions & 5 deletions Tests/MultipeerKitTests/MultipeerKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
]
}