Skip to content

Implement support for resource transfers, revamp sample code #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
16 changes: 11 additions & 5 deletions Sources/MultipeerKit/Internal API/MockMultipeerConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import MultipeerConnectivity.MCPeerID

final class MockMultipeerConnection: MultipeerProtocol {

let localPeer: Peer = {
let underlyingPeer = MCPeerID(displayName: "MockPeer")
return try! Peer(peer: underlyingPeer, discoveryInfo: nil)
}()
let localPeer = Peer.mock

var didReceiveData: ((Data, Peer) -> Void)?
var didFindPeer: ((Peer) -> Void)?
var didLosePeer: ((Peer) -> Void)?
var didConnectToPeer: ((Peer) -> Void)?
var didDisconnectFromPeer: ((Peer) -> Void)?

var didStartReceivingResource: ((_ sender: Peer, _ resourceName: String, _ progress: Progress) -> Void)?
var didFinishReceivingResource: ((_ sender: Peer, _ resourceName: String, _ result: Result<URL, Error>) -> Void)?

var isRunning = false

func resume() {
Expand All @@ -32,6 +31,13 @@ final class MockMultipeerConnection: MultipeerProtocol {

}

@available(iOS 13.0, macOS 10.15, *)
func send(_ resourceURL: URL, to peer: Peer) -> MultipeerTransceiver.ResourceUploadStream {
AsyncThrowingStream { continuation in
continuation.finish()
}
}

func invite(_ peer: Peer, with context: Data?, timeout: TimeInterval, completion: InvitationCompletionHandler?) {

}
Expand Down
70 changes: 67 additions & 3 deletions Sources/MultipeerKit/Internal API/MultipeerConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ final class MultipeerConnection: NSObject, MultipeerProtocol {
var didLosePeer: ((Peer) -> Void)?
var didConnectToPeer: ((Peer) -> Void)?
var didDisconnectFromPeer: ((Peer) -> Void)?
var didStartReceivingResource: ((_ sender: Peer, _ resourceName: String, _ progress: Progress) -> Void)?
var didFinishReceivingResource: ((_ sender: Peer, _ resourceName: String, _ result: Result<URL, Error>) -> Void)?

private var discoveredPeers: [MCPeerID: Peer] = [:]

Expand Down Expand Up @@ -109,6 +111,44 @@ final class MultipeerConnection: NSObject, MultipeerProtocol {
try session.send(data, toPeers: ids, with: .reliable)
}

@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
func send(_ resourceURL: URL, to peer: Peer) -> MultipeerTransceiver.ResourceUploadStream {
AsyncThrowingStream { [weak self] continuation in
guard let self else {
return continuation.finish(throwing: CancellationError())
}

guard resourceURL.isFileURL else {
return continuation.finish(throwing: MultipeerError(localizedDescription: "Resource must be a local file URL, remote URLs are not supported."))
}

let peerID = peer.underlyingPeer

/// Ensure resource names are unique even if the file name is the same, while keeping the file name to aid in debugging.
let resourceID = "\(UUID().uuidString)-\(resourceURL.lastPathComponent)"

let progress = session.sendResource(at: resourceURL, withName: resourceID, toPeer: peerID) { error in
if let error {
continuation.finish(throwing: error)
} else {
continuation.finish()
}
}

guard let progress else {
return continuation.finish(throwing: MultipeerError(localizedDescription: "Failed to start resource upload."))
}

let cancellable = progress.publisher(for: \.fractionCompleted).sink { progress in
continuation.yield(progress)
}

continuation.onTermination = { @Sendable _ in
cancellable.cancel()
}
}
}

private var invitationCompletionHandlers: [MCPeerID: InvitationCompletionHandler] = [:]

func invite(_ peer: Peer, with context: Data?, timeout: TimeInterval, completion: InvitationCompletionHandler?) {
Expand Down Expand Up @@ -155,10 +195,11 @@ extension MultipeerConnection: MCSessionDelegate {
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
os_log("%{public}@", log: log, type: .debug, #function)

if let peer = try? Peer(peer: peerID, discoveryInfo: nil) {
do {
let peer = try Peer(peer: peerID, discoveryInfo: nil)
didReceiveData?(data, peer)
} else {
os_log("Received data, but cannot create peer for %s", log: log, type: .error, #function, peerID.displayName)
} catch {
os_log("Received data, but cannot create peer for %@: %{public}@", log: log, type: .error, #function, peerID.displayName, String(describing: error))
}
}

Expand All @@ -168,10 +209,33 @@ extension MultipeerConnection: MCSessionDelegate {

func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
os_log("%{public}@", log: log, type: .debug, #function)

do {
let peer = try Peer(peer: peerID, discoveryInfo: nil)

didStartReceivingResource?(peer, resourceName, progress)
} catch {
os_log("Started receiving resource, but cannot create peer for %@: %{public}@", log: log, type: .error, #function, peerID.displayName, String(describing: error))
}
}

func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
os_log("%{public}@", log: log, type: .debug, #function)

do {
let peer = try Peer(peer: peerID, discoveryInfo: nil)

let result: Result<URL, Error>
if let localURL {
result = .success(localURL)
} else {
result = .failure(error ?? MultipeerError(localizedDescription: "Unknown MultipeerConnectivity error."))
}

didFinishReceivingResource?(peer, resourceName, result)
} catch {
os_log("Finished receiving resource, but cannot create peer for %@: %{public}@", log: log, type: .error, #function, peerID.displayName, String(describing: error))
}
}

}
Expand Down
7 changes: 6 additions & 1 deletion Sources/MultipeerKit/Internal API/MultipeerProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ protocol MultipeerProtocol: AnyObject {
var didLosePeer: ((Peer) -> Void)? { get set }
var didConnectToPeer: ((Peer) -> Void)? { get set }
var didDisconnectFromPeer: ((Peer) -> Void)? { get set }
var didStartReceivingResource: ((_ sender: Peer, _ resourceName: String, _ progress: Progress) -> Void)? { get set }
var didFinishReceivingResource: ((_ sender: Peer, _ resourceName: String, _ result: Result<URL, Error>) -> Void)? { get set }

func resume()
func stop()

func invite(_ peer: Peer, with context: Data?, timeout: TimeInterval, completion: InvitationCompletionHandler?)
func broadcast(_ data: Data) throws
func send(_ data: Data, to peers: [Peer]) throws


@available(iOS 13.0, macOS 10.15, *)
func send(_ resourceURL: URL, to peer: Peer) -> MultipeerTransceiver.ResourceUploadStream

func getLocalPeer() -> Peer?

}
8 changes: 8 additions & 0 deletions Sources/MultipeerKit/Public API/Models/Peer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,11 @@ fileprivate extension Data {
}

}

public extension Peer {
/// A mock peer that can be used for testing or SwiftUI previews.
static let mock: Peer = {
let underlyingPeer = MCPeerID(displayName: "MockPeer")
return try! Peer(peer: underlyingPeer, discoveryInfo: nil)
}()
}
93 changes: 90 additions & 3 deletions Sources/MultipeerKit/Public API/MultipeerDataSource.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import Foundation
import Combine

@available(iOS 14.0, tvOS 14.0, macOS 11.0, *)
public typealias ObservablePeer = MultipeerDataSource.ObservablePeer

/// An ObservableObject wrapper around ``MultipeerTransceiver``,
/// useful for use with `Combine` and SwiftUI apps.
@available(tvOS 13.0, *)
@available(OSX 10.15, *)
@available(iOS 13.0, *)
@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
public final class MultipeerDataSource: ObservableObject {

public let transceiver: MultipeerTransceiver

public static let isSwiftUIPreview: Bool = { ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" }()

/// Initializes a new data source.
/// - Parameter transceiver: The transceiver to be used by this data source.
/// Note that the data source will set ``MultipeerTransceiver/availablePeersDidChange`` on the
Expand All @@ -22,9 +26,92 @@ public final class MultipeerDataSource: ObservableObject {
}

availablePeers = transceiver.availablePeers

#if DEBUG
if Self.isSwiftUIPreview {
availablePeers.append(.mock)
}
#endif
}

/// Peers currently available for invitation, connection and data transmission.
@Published public private(set) var availablePeers: [Peer] = []

/// Manually invites a peer.
///
/// For more details, read the documentation for ``MultipeerTransceiver/invite(_:with:timeout:completion:)``.
public func invite(_ peer: Peer, with data: Data? = nil, timeout: TimeInterval = 30) async throws {
try await withCheckedThrowingContinuation { [weak self] (continuation: CheckedContinuation<Void, Error>) in
guard let self else { return continuation.resume(throwing: CancellationError()) }

self.transceiver.invite(peer, with: data, timeout: timeout) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}

/// Returns an ``ObservablePeer`` instance that automatically updates when the
/// specified peer is updated, such as when it connects or disconnects.
///
/// Use an observable peer as the input to a SwiftUI view if you'd like the view to be
/// updated when the state of the observed peer changes, such as when it connects/disconnects from the local peer.
@available(iOS 14.0, tvOS 14.0, macOS 11.0, *)
public func observablePeer(_ peer: Peer) -> ObservablePeer {
ObservablePeer(peer: peer, dataSource: self)
}

/// Convenient `ObservableObject` that tracks updates to the state of a specific ``Peer``.
@available(iOS 14.0, tvOS 14.0, macOS 11.0, *)
public final class ObservablePeer: ObservableObject {
/// This is updated from the transceiver.
/// If the peer goes away, this may be set to `nil`.
/// Doesn't affect availability of the original `Peer` model to API clients,
/// since that's stored on `init` and provided as fallback.
@Published fileprivate var dynamicPeer: Peer?

/// The peer that's being observed by the object.
///
/// - note: Data on the peer might be stale if the remote peer goes away
/// during the lifetime of the observable peer object. For connection state,
/// prefer reading directly from the ``isConnected`` property.
public var observedPeer: Peer { dynamicPeer ?? initialPeer }

/// The unique identifier for the peer.
public var id: String

/// The peer's display name.
public var name: String { dynamicPeer?.name ?? initialPeer.name }

/// Discovery info provided by the peer.
public var discoveryInfo: [String: String]? { dynamicPeer?.discoveryInfo ?? initialPeer.discoveryInfo }

/// `true` if we are currently connected to this peer.
public var isConnected: Bool { dynamicPeer?.isConnected ?? false }

private lazy var cancellables = Set<AnyCancellable>()

private let initialPeer: Peer

fileprivate init(peer: Peer, dataSource: MultipeerDataSource?) {
self.dynamicPeer = peer
self.initialPeer = peer
self.id = peer.id

guard let dataSource else { return }

dataSource
.$availablePeers
.map({ $0.first(where: { $0.id == peer.id }) })
.removeDuplicates()
.assign(to: &$dynamicPeer)
}

public static let mock = ObservablePeer(peer: .mock, dataSource: nil)
}

}
Loading