Skip to content

Commit ba33d0b

Browse files
committed
Implemented AsyncSequence peerEvents property that can be used to observe peer discovery and connection events with the new Swift Concurrency API
1 parent 844cfda commit ba33d0b

3 files changed

Lines changed: 123 additions & 5 deletions

File tree

Sources/MultipeerKit/Internal API/MockMultipeerConnection.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,31 @@ final class MockMultipeerConnection: MultipeerProtocol {
3939
func getLocalPeerId() -> String? {
4040
return localPeer.id
4141
}
42+
43+
@discardableResult func findFakePeer(with id: String) -> Peer {
44+
let fakePeer = Peer(
45+
underlyingPeer: MCPeerID(displayName: id),
46+
id: id,
47+
name: id,
48+
discoveryInfo: nil,
49+
isConnected: false
50+
)
51+
52+
didFindPeer?(fakePeer)
53+
54+
return fakePeer
55+
}
56+
57+
func loseFakePeer(_ fakePeer: Peer) {
58+
didLosePeer?(fakePeer)
59+
}
60+
61+
func connectFakePeer(_ fakePeer: Peer) {
62+
didConnectToPeer?(fakePeer)
63+
}
64+
65+
func disconnectFakePeer(_ fakePeer: Peer) {
66+
didDisconnectFromPeer?(fakePeer)
67+
}
4268

4369
}

Sources/MultipeerKit/Public API/MultipeerTransceiver.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,31 +163,46 @@ public final class MultipeerTransceiver {
163163
public func invite(_ peer: Peer, with context: Data?, timeout: TimeInterval, completion: InvitationCompletionHandler?) {
164164
connection.invite(peer, with: context, timeout: timeout, completion: completion)
165165
}
166+
167+
public enum PeerEvent: Hashable {
168+
case found(Peer)
169+
case lost(Peer)
170+
case connected(Peer)
171+
case disconnected(Peer)
172+
}
166173

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

170177
availablePeers.append(peer)
171178
peerAdded(peer)
179+
180+
peerEventOccurred(.found(peer))
172181
}
173182

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

177186
availablePeers.remove(at: idx)
178187
peerRemoved(peer)
188+
189+
peerEventOccurred(.lost(peer))
179190
}
180191

181192
private func handlePeerConnected(_ peer: Peer) {
182193
setConnected(true, on: peer)
183194

184195
peerConnected(peer)
196+
197+
peerEventOccurred(.connected(peer))
185198
}
186199

187200
private func handlePeerDisconnected(_ peer: Peer) {
188201
setConnected(false, on: peer)
189202

190203
peerDisconnected(peer)
204+
205+
peerEventOccurred(.disconnected(peer))
191206
}
192207

193208
private func setConnected(_ connected: Bool, on peer: Peer) {
@@ -197,5 +212,44 @@ public final class MultipeerTransceiver {
197212
mutablePeer.isConnected = connected
198213
availablePeers[idx] = mutablePeer
199214
}
215+
216+
// MARK: - Swift Concurrency Support
217+
218+
private typealias PeerEventCallback = (PeerEvent) -> Void
219+
220+
private var internalPeerEventCallbacks: [UUID: PeerEventCallback] = [:]
221+
222+
private func addInternalPeerEventsCallback(with block: @escaping PeerEventCallback) -> UUID {
223+
let id = UUID()
224+
internalPeerEventCallbacks[id] = block
225+
return id
226+
}
227+
228+
private func peerEventOccurred(_ event: PeerEvent) {
229+
DispatchQueue.main.async {
230+
self.internalPeerEventCallbacks.values.forEach { $0(event) }
231+
}
232+
}
200233

201234
}
235+
236+
@available(tvOS 13.0, *)
237+
@available(iOS 13.0, *)
238+
@available(macOS 10.15, *)
239+
public extension MultipeerTransceiver {
240+
241+
var peerEvents: AsyncStream<PeerEvent> {
242+
AsyncStream { [weak self] continuation in
243+
guard let self = self else { return }
244+
245+
let id = self.addInternalPeerEventsCallback { event in
246+
continuation.yield(event)
247+
}
248+
249+
continuation.onTermination = { @Sendable _ in
250+
self.internalPeerEventCallbacks[id] = nil
251+
}
252+
}
253+
}
254+
255+
}

Tests/MultipeerKitTests/MultipeerKitTests.swift

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,48 @@ final class MultipeerKitTests: XCTestCase {
4747

4848
wait(for: [expect], timeout: 2)
4949
}
50+
51+
@available(tvOS 13.0, *)
52+
@available(iOS 13.0, *)
53+
@available(macOS 10.15, *)
54+
func testAsyncEventsStreamContinuesWithEachPeerEvent() throws {
55+
let transceiver = makeMockTransceiver()
56+
transceiver.resume()
57+
58+
let exp = expectation(description: "Async Peer")
59+
exp.expectedFulfillmentCount = 4
60+
61+
Task.detached {
62+
// Used to ensure that events are yielded in the right order.
63+
var currentEvent = 0
64+
65+
for await event in transceiver.peerEvents {
66+
switch event {
67+
case .found(let peer):
68+
XCTAssertEqual(peer.id, "A")
69+
XCTAssertEqual(currentEvent, 0)
70+
case .connected(let peer):
71+
XCTAssertEqual(peer.id, "A")
72+
XCTAssertEqual(currentEvent, 1)
73+
case .disconnected(let peer):
74+
XCTAssertEqual(peer.id, "A")
75+
XCTAssertEqual(currentEvent, 2)
76+
case .lost(let peer):
77+
XCTAssertEqual(peer.id, "A")
78+
XCTAssertEqual(currentEvent, 3)
79+
}
80+
81+
currentEvent += 1
82+
exp.fulfill()
83+
}
84+
}
85+
86+
let peer = transceiver.mockConnection.findFakePeer(with: "A")
87+
transceiver.mockConnection.connectFakePeer(peer)
88+
transceiver.mockConnection.disconnectFakePeer(peer)
89+
transceiver.mockConnection.loseFakePeer(peer)
90+
91+
wait(for: [exp], timeout: 2)
92+
}
5093

51-
static var allTests = [
52-
("testCallingResumeResumesConnection", testCallingResumeResumesConnection),
53-
("testCallingStopStopsConnection", testCallingStopStopsConnection),
54-
("testReceivingCustomPayload", testReceivingCustomPayload),
55-
]
5694
}

0 commit comments

Comments
 (0)