Add progress view for incoming transfer on iOS

This commit is contained in:
Leon Böttger committed 2025-08-17 13:22:44 +02:00
1 parent 62f5a13fd6
commit 288625cbd3
8 files changed
+1786 -1553

No files matched your search

@@ -25,6 +25,7 @@ class InboundNearbyConnection: NearbyConnection {
private var currentState: State = .initial
private var cipherCommitment: Data?
private var textPayloadID: Int64 = 0
private var bytesToBeTransferred: Int64 = 0
var isPlainTextTransfer = false
var wasUserRejected = false
@@ -154,6 +155,7 @@ class InboundNearbyConnection: NearbyConnection {
// only for logging
self.bytesTransferred += Int64(frame.payloadChunk.body.count)
NearbyConnectionManager.shared.inboundConnection(connection: self, transferProgress: Double(self.bytesTransferred) / Double(self.bytesToBeTransferred))
} catch {
log("[InboundNearbyConnection \(self.id)] Error occurred during writing file: \(error.localizedDescription)")
@@ -172,6 +174,7 @@ class InboundNearbyConnection: NearbyConnection {
if filesToBeReceived.isEmpty {
log("[InboundNearbyConnection \(self.id)] All files received, sending disconnection frame and disconnecting.")
NearbyConnectionManager.shared.inboundConnection(connection: self, transferProgress: 1)
try sendDisconnectionAndDisconnect()
}
}
@@ -218,6 +221,7 @@ class InboundNearbyConnection: NearbyConnection {
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
log("[InboundNearbyConnection \(self.id)] Received file payload. Disconnecting...")
NearbyConnectionManager.shared.inboundConnection(connection: self, transferProgress: 1)
try sendDisconnectionAndDisconnect()
return true
}
@@ -426,6 +430,7 @@ class InboundNearbyConnection: NearbyConnection {
payloadID: file.payloadID,
destinationURL: dest)
filesToBeReceived[file.payloadID] = info
bytesToBeTransferred += file.size
}
let metadata = TransferMetadata(files: filesToBeReceived.map { $0.value.meta }, id: id, pinCode: pinCode)
DispatchQueue.main.async {
@@ -134,6 +134,11 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
func inboundConnection(connection: InboundNearbyConnection, transferProgress: Double) {
mainAppDelegate?.transferProgress(progress: transferProgress)
}
public func submitUserConsent(transferID: String, accept: Bool, storeInTemp: Bool = false) {
guard let conn = incomingConnections[transferID] else { return }
+2
View File
@@ -13,6 +13,7 @@ import SwiftECC
protocol InboundNearbyConnectionDelegate {
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo, connection: InboundNearbyConnection)
func inboundConnection(connection: InboundNearbyConnection, transferProgress: Double)
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?)
}
@@ -29,6 +30,7 @@ protocol OutboundNearbyConnectionDelegate {
public protocol InboundAppDelegate: AnyObject {
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo)
func connectionWasTerminated(from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: Error?)
func transferProgress(progress: Double)
}
+2
View File
@@ -9,6 +9,8 @@ import Foundation
import ImageIO
#if os(macOS)
import AppKit
#else
import UIKit
#endif
public class SaveFilesManager {
+4
View File
@@ -113,6 +113,7 @@
EB97A0132E51D6B2008B8C5C /* RNDeviceName in Frameworks */ = {isa = PBXBuildFile; productRef = EB97A0122E51D6B2008B8C5C /* RNDeviceName */; };
EB97A0142E51DD00008B8C5C /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
EB97A0152E51DD00008B8C5C /* timeIsNow.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = EBC05C8A2E33B59E00EECC18 /* timeIsNow.mp3 */; };
EB97A01A2E51EAA2008B8C5C /* UIAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB97A0192E51EA9C008B8C5C /* UIAlert.swift */; };
EB9925802E45D53E00357055 /* sharing_enums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */; };
EB9925812E45D53E00357055 /* sharing_enums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */; };
EB9925822E45D53E00357055 /* sharing_enums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */; };
@@ -249,6 +250,7 @@
EB979F992E50CAA6008B8C5C /* PieProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PieProgressView.swift; sourceTree = "<group>"; };
EB97A00E2E511120008B8C5C /* ReceiveModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveModel.swift; sourceTree = "<group>"; };
EB97A0162E51DF4F008B8C5C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
EB97A0192E51EA9C008B8C5C /* UIAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlert.swift; sourceTree = "<group>"; };
EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = sharing_enums.pb.swift; sourceTree = "<group>"; };
EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlertHandler.swift; sourceTree = "<group>"; };
EBC05C6D2E33B57D00EECC18 /* Converter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Converter.swift; sourceTree = "<group>"; };
@@ -447,6 +449,7 @@
EB979F8F2E50CA23008B8C5C /* FilePicker.swift */,
EB979F902E50CA23008B8C5C /* SendModel.swift */,
EB97A00E2E511120008B8C5C /* ReceiveModel.swift */,
EB97A0192E51EA9C008B8C5C /* UIAlert.swift */,
EB979F992E50CAA6008B8C5C /* PieProgressView.swift */,
EB979F912E50CA23008B8C5C /* QuickDropiOS.entitlements */,
EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */,
@@ -813,6 +816,7 @@
EB5C383D2E38C2D90052EFD7 /* AudioManager.swift in Sources */,
EB979F942E50CA23008B8C5C /* Configuration.swift in Sources */,
EB979F952E50CA23008B8C5C /* ContentView.swift in Sources */,
EB97A01A2E51EAA2008B8C5C /* UIAlert.swift in Sources */,
EB979F962E50CA23008B8C5C /* FilePicker.swift in Sources */,
EB979F9A2E50CAA6008B8C5C /* PieProgressView.swift in Sources */,
EB979F972E50CA23008B8C5C /* SendModel.swift in Sources */,
File diff suppressed because it is too large. Load diff
+41 -26
View File
@@ -16,7 +16,7 @@ import LUI
#endif
class ReceiveModel: ObservableObject, InboundAppDelegate {
let controlPlusScreen: (Bool) -> Void
init(controlPlusScreen: @escaping (Bool) -> Void = { _ in }) {
@@ -91,7 +91,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
let primaryButtonTitle = "Accept".localized()
let primaryButtonAction = { self.pressAcceptButton(transferID: transferID) }
let secondaryButtonTitle = "Decline".localized()
let secondaryButtonAction = { self.continueTransmission(accept: false, transferID: transferID) }
let secondaryButtonAction = { NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: false, storeInTemp: false) }
#if os(macOS)
let alert = NSAlert()
@@ -111,31 +111,51 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
}
#else
// iOS
showAlert(title: title, message: mainMessage, primaryButton: LUIAlertButton(title: secondaryButtonTitle, action: secondaryButtonAction), secondaryButton: LUIAlertButton(title: primaryButtonTitle, action: primaryButtonAction))
ProgressAlert.shared.askForUserPermission(title: title, message: mainMessage, acceptLabel: primaryButtonTitle, rejectLabel: secondaryButtonTitle) { accepted in
accepted ? primaryButtonAction() : secondaryButtonAction()
}
#endif
}
}
func transferProgress(progress: Double) {
#if os(iOS)
ProgressAlert.shared.updateProgress(progress)
#endif
}
func connectionWasTerminated(from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: (any Error)?) {
if let error = error {
controlPlusScreen(false)
ErrorAlertHandler.shared.showErrorAlert(for: device.name ?? "Android", error: error)
} else {
if wasPlainTextTransfer {
showCopiedToClipboardAlert()
}
let currentCount = incomingTransmissionCount()
if currentCount == 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
SKStoreReviewController.requestReview()
#if os(macOS)
finish()
#else
ProgressAlert.shared.updateProgress(nil) {
finish()
}
#endif
func finish() {
if let error = error {
controlPlusScreen(false)
ErrorAlertHandler.shared.showErrorAlert(for: device.name ?? "Android", error: error)
} else {
if wasPlainTextTransfer {
showCopiedToClipboardAlert()
}
let currentCount = incomingTransmissionCount()
if currentCount == 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
SKStoreReviewController.requestReview()
}
}
}
UserDefaults.standard.set(currentCount + 1, forKey: UserDefaultsKeys.transmissionCount.rawValue)
log("[ReceiveModel] Successful transmission. Current count: \(currentCount)")
UserDefaults.standard.set(currentCount + 1, forKey: UserDefaultsKeys.transmissionCount.rawValue)
log("[ReceiveModel] Successful transmission. Current count: \(currentCount)")
}
}
}
@@ -154,16 +174,11 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
private func pressAcceptButton(transferID: String) {
if isFileTransferRestricted() {
continueTransmission(accept: true, transferID: transferID, storeInTemp: true)
log("[ReceiveModel] Showing plus screen...")
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, storeInTemp: true)
controlPlusScreen(true)
} else {
continueTransmission(accept: true, transferID: transferID)
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, storeInTemp: false)
}
}
private func continueTransmission(accept: Bool, transferID: String, storeInTemp: Bool = false) {
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: accept, storeInTemp: storeInTemp)
}
}
+93
View File
@@ -0,0 +1,93 @@
//
// UIAlert.swift
// QuickDrop
//
// Created by Leon Böttger on 17.08.25.
//
import SwiftUI
import UIKit
// MARK: - UIKit Alert Presenter
final class ProgressAlert {
static let shared = ProgressAlert()
private init() {}
private weak var progressAlert: UIAlertController?
private weak var progressView: UIProgressView?
// MARK: - Initial Accept / Decline
func askForUserPermission(title: String, message: String, acceptLabel: String, rejectLabel: String, onAccept: @escaping (Bool) -> Void) {
guard let vc = topMostViewController() else { return }
let alert = UIAlertController(title: title,
message: message,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: rejectLabel, style: .cancel, handler: { _ in
onAccept(false)
}))
alert.addAction(UIAlertAction(title: acceptLabel, style: .default, handler: { _ in
onAccept(true)
self.showProgressAlert(on: vc)
}))
vc.present(alert, animated: true)
}
// MARK: - Progress Alert
private func showProgressAlert(on vc: UIViewController) {
let alert = UIAlertController(title: "Receiving".localized(),
message: "\n\n",
preferredStyle: .alert)
let pv = UIProgressView(progressViewStyle: .default)
pv.translatesAutoresizingMaskIntoConstraints = false
alert.view.addSubview(pv)
NSLayoutConstraint.activate([
pv.leadingAnchor.constraint(equalTo: alert.view.leadingAnchor, constant: 20),
pv.trailingAnchor.constraint(equalTo: alert.view.trailingAnchor, constant: -20),
pv.centerYAnchor.constraint(equalTo: alert.view.centerYAnchor, constant: 8)
])
let cancel = UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: { _ in
print("Cancel during progress")
})
alert.addAction(cancel)
self.progressAlert = alert
self.progressView = pv
vc.present(alert, animated: true)
}
// MARK: - Update Progress
// progress: 0…1 → update progress, nil → finished
func updateProgress(_ progress: Double?, completion: @escaping () -> Void = {}) {
DispatchQueue.main.async {
if let p = progress {
self.progressView?.setProgress(Float(p), animated: true)
completion()
} else {
// Completed: dismiss progress and show final alert
self.progressAlert?.dismiss(animated: true) {
completion()
}
self.progressAlert = nil
self.progressView = nil
}
}
}
// MARK: - Helpers
private func topMostViewController(base: UIViewController? = UIApplication.shared.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow?.rootViewController }.first) -> UIViewController? {
if let nav = base as? UINavigationController { return topMostViewController(base: nav.visibleViewController) }
if let tab = base as? UITabBarController { return topMostViewController(base: tab.selectedViewController) }
if let presented = base?.presentedViewController { return topMostViewController(base: presented) }
return base
}
}