TLDR: If you do not want to read the long explanation and get straight to the code, all the ideas below are brought together and can be tested by downloading my public repository which has comments to explain all of this.
So here are my suggestions on how you can achieve this
After reviewing your code, I see that you are using the following function to send data
func send(_ data: Data, toPeers peerIDs: [MCPeerID], with mode: MCSessionSendDataMode)
There is nothing wrong with this and you can indeed convert your UIImage to a Data object and send it this way, it will work.
However I don't think you can track progress and MultiPeer does not give you any delegates to track progress using this method.
Instead you are left with two other options. You could use
func session(_ session: MCSession,
didFinishReceivingResourceWithName resourceName: String,
fromPeer peerID: MCPeerID,
at localURL: URL?,
withError error: Error?)
Or you could use
func startStream(withName streamName: String,
toPeer peerID: MCPeerID) throws -> OutputStream
I am going to use the first option as that is more straightforward however I think the stream option will give you better results. You can read up on both the options here:
Send Resource (we will implement this one)
Streaming
Step 1
I made some UI updates to your original code by adding a UIImageView to show the transferred image to the advertiser(guest) and a UIButton to start the file transfer from the browser(host)
The UIImageView has an outlet named @IBOutlet weak var imageView: UIImageView!
and an action for the UIButton @IBAction func sendImageAsResource(_ sender: Any)
I have also added an image called image2.jpg to the project which we will send from the host to the guest.
Step 2
I also declared few additional variables
// Progress variable that needs to store the progress of the file transfer
var fileTransferProgress: Progress?
// Timer that will be used to check the file transfer progress
var checkProgressTimer: Timer?
// Used by the host to track bytes to receive
var bytesExpectedToExchange = 0
// Used to track the time taken in transfer, this is for testing purposes.
// You might get more reliable results using Date to track time
var transferTimeElapsed = 0.0
Step 3
Set up the host and guest as normal by tapping Guest and Host buttons respectively. After that, tap the Send image as resource
button on the host and the action is implemented as follows for the host:
// A new action added to send the image stored in the bundle
@IBAction func sendImageAsResource(_ sender: Any)
{
// Call local function created
sendImageAsResource()
}
func sendImageAsResource()
{
// 1. Get the url of the image in the project bundle.
// Change this if your image is hosted in your documents directory
// or elsewhere.
//
// 2. Get all the connected peers. For testing purposes I am only
// getting the first peer, you might need to loop through all your
// connected peers and send the files individually.
guard let imageURL = Bundle.main.url(forResource: "image2",
withExtension: "jpg"),
let guestPeerID = mcSession.connectedPeers.first else {
return
}
// Retrieve the file size of the image
if let fileSizeToTransfer = getFileSize(atURL: imageURL)
{
bytesExpectedToExchange = fileSizeToTransfer
// Put the file size in a dictionary
let fileTransferMeta = ["fileSize": bytesExpectedToExchange]
// Convert the dictionary to a data object in order to send it via
// MultiPeer
let encoder = JSONEncoder()
if let JSONData = try? encoder.encode(fileTransferMeta)
{
// Send the file size to the guest users
try? mcSession.send(JSONData, toPeers: mcSession.connectedPeers,
with: .reliable)
}
}
// Ideally for best reliability, you will want to develop some logic
// for the guest to respond that it has received the file size and then
// you should initiate the transfer to that peer only after you receive
// this confirmation. For now, I just add a delay so that I am highly
// certain the guest has received this data for testing purposes
DispatchQueue.main.asyncAfter(deadline: .now() + 1)
{ [weak self] in
self?.initiateFileTransfer(ofImage: imageURL, to: guestPeerID)
}
}
func initiateFileTransfer(ofImage imageURL: URL, to guestPeerID: MCPeerID)
{
// Initialize and fire a timer to check the status of the file
// transfer every 0.1 second
checkProgressTimer = Timer.scheduledTimer(timeInterval: 0.1,
target: self,
selector: #selector(updateProgressStatus),
userInfo: nil,
repeats: true)
// Call the sendResource function and send the image from the bundle
// keeping hold of the returned progress object which we need to keep checking
// using the timer
fileTransferProgress = mcSession.sendResource(at: imageURL,
withName: "image2.jpg",
toPeer: guestPeerID,
withCompletionHandler: { (error) in
// Handle errors
if let error = error as NSError?
{
print("Error: \(error.userInfo)")
print("Error: \(error.localizedDescription)")
}
})
}
func getFileSize(atURL url: URL) -> Int?
{
let urlResourceValue = try? url.resourceValues(forKeys: [.fileSizeKey])
return urlResourceValue?.fileSize
}
Step 4
This next function is used by the host and the guest. The guest will make side of things will make sense later on, however for the host, in step 3, you have stored a progress object after initiating the file transfer and you have launched a timer to fire every 0.1 seconds, so now implement the timer to query this progress object to display the progress and data transfer status on the host side in the UILabel
/// Function fired by the local checkProgressTimer object used to track the progress of the file transfer
/// Function fired by the local checkProgressTimer object used to track the progress of the file transfer
@objc
func updateProgressStatus()
{
// Update the time elapsed. As mentioned earlier, a more reliable approach
// might be to compare the time of a Date object from when the
// transfer started to the time of a current Date object
transferTimeElapsed += 0.1
// Verify the progress variable is valid
if let progress = fileTransferProgress
{
// Convert the progress into a percentage
let percentCompleted = 100 * progress.fractionCompleted
// Calculate the data exchanged sent in MegaBytes
let dataExchangedInMB = (Double(bytesExpectedToExchange)
* progress.fractionCompleted) / 1000000
// We have exchanged 'dataExchangedInMB' MB of data in 'transferTimeElapsed'
// seconds. So we have to calculate how much data will be exchanged in 1 second
// using cross multiplication
// For example:
// 2 MB in 0.5s
// ? in 1s
// MB/s = (1 x 2) / 0.5 = 4 MB/s
let megabytesPerSecond = (1 * dataExchangedInMB) / transferTimeElapsed
// Convert dataExchangedInMB into a string rounded to 2 decimal places
let dataExchangedInMBString = String(format: "%.2f", dataExchangedInMB)
// Convert megabytesPerSecond into a string rounded to 2 decimal places
let megabytesPerSecondString = String(format: "%.2f", megabytesPerSecond)
// Update the progress an data exchanged on the UI
numberLabel.text = "\(percentCompleted.rounded())% - \(dataExchangedInMBString) MB @ \(megabytesPerSecondString) MB/s"
// This is mostly useful on the browser side to check if the file transfer
// is complete so that we can safely deinit the timer, reset vars and update the UI
if percentCompleted >= 100
{
numberLabel.text = "Transfer complete!"
checkProgressTimer?.invalidate()
checkProgressTimer = nil
transferTimeElapsed = 0.0
}
}
}
Step 5
Handle the receiving of the file on the receiver (guest) side by implementing the following delegate methods
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID)
{
// Check if the guest has received file transfer data
if let fileTransferMeta = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Int],
let fileSizeToReceive = fileTransferMeta["fileSize"]
{
// Store the bytes to be received in a variable
bytesExpectedToExchange = fileSizeToReceive
print("Bytes expected to receive: \(fileSizeToReceive)")
return
}
}
func session(_ session: MCSession,
didStartReceivingResourceWithName resourceName: String,
fromPeer peerID: MCPeerID,
with progress: Progress)
{
// Store the progress object so that we can query it using the timer
fileTransferProgress = progress
// Launch the main thread
DispatchQueue.main.async { [unowned self] in
// Fire the timer to check the file transfer progress every 0.1 second
self.checkProgressTimer = Timer.scheduledTimer(timeInterval: 0.1,
target: self,
selector: #selector(updateProgressStatus),
userInfo: nil,
repeats: true)
}
}
func session(_ session: MCSession,
didFinishReceivingResourceWithName resourceName: String,
fromPeer peerID: MCPeerID,
at localURL: URL?,
withError error: Error?)
{
// Verify that we have a valid url. You should get a url to the file in
// the tmp directory
if let url = localURL
{
// Launch the main thread
DispatchQueue.main.async { [weak self] in
// Call a function to handle download completion
self?.handleDownloadCompletion(withImageURL: url)
}
}
}
/// Handles the file transfer completion process on the advertiser/client side
/// - Parameter url: URL of a file in the documents directory
func handleDownloadCompletion(withImageURL url: URL)
{
// Debugging data
print("Full URL: \(url.absoluteString)")
// Invalidate the timer
checkProgressTimer?.invalidate()
checkProgressTimer = nil
// Set the UIImageView with the downloaded image
imageView.image = UIImage(contentsOfFile: url.path)
}
Step 6
Run the code and this is the end result (uploaded to youtube) on the guest side which shows the progress and the file once the transfer is complete and the same progress is shown on the host side as well.
Step 7
I did not implement this but I believe this bit is straight forward:
The file size can be calculated as from the host and it can be sent as a message to the guest on the size to expect
You can can compute the approximate % of a file that has been
downloaded by multiplying the progress % by the file size
The speed can be calculated based on the amount of data downloaded / time elapsed so far since the transfer started
I can try to add this code if you feel these calculations are not straightforward.
Update
I have updated the above code samples, github repo and the video to include the final 3 steps as well which gives the final result as follows: