Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit 9d3034a

Browse files
authored
Enhance DownloadSignManager with progress tracking
Refactor download and signing process to include progress tracking for each stage and improve error handling.
1 parent 922acb2 commit 9d3034a

1 file changed

Lines changed: 133 additions & 44 deletions

File tree

‎Sources/prostore/signing/DownloadSignManager.swift‎

Lines changed: 133 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,45 @@ class DownloadSignManager: ObservableObject {
77
@Published var status: String = ""
88
@Published var isProcessing: Bool = false
99
@Published var showSuccess: Bool = false
10-
10+
1111
private var downloadTask: URLSessionDownloadTask?
12+
private var downloadProgressObservation: NSKeyValueObservation?
1213
private var installationStream: AsyncThrowingStream<(progress: Double, status: String), Error>?
1314
private var installationTask: Task<Void, Never>?
1415
private var cancellables = Set<AnyCancellable>()
15-
16+
17+
// Portion split (tweak if you want different ratios)
18+
private let downloadPortion: Double = 0.33
19+
private let signPortion: Double = 0.33
20+
private let installPortion: Double = 1.0 - (0.33 + 0.33) // ~0.34
21+
1622
func downloadAndSign(app: AltApp) {
1723
guard let downloadURL = app.downloadURL else {
1824
self.status = "No download URL available"
1925
return
2026
}
21-
27+
2228
guard let selectedCertFolder = UserDefaults.standard.string(forKey: "selectedCertificateFolder") else {
2329
self.status = "No certificate selected"
2430
return
2531
}
26-
32+
2733
self.isProcessing = true
2834
self.progress = 0.0
2935
self.status = "Starting download..."
3036
self.showSuccess = false
31-
37+
3238
DispatchQueue.global(qos: .userInitiated).async {
3339
self.performDownloadAndSign(downloadURL: downloadURL, appName: app.name, certFolder: selectedCertFolder)
3440
}
3541
}
36-
42+
3743
private func performDownloadAndSign(downloadURL: URL, appName: String, certFolder: String) {
3844
// Step 1: Setup directories
3945
let fm = FileManager.default
4046
let appFolder = self.getAppFolder()
4147
let tempDir = appFolder.appendingPathComponent("temp")
42-
48+
4349
do {
4450
if !fm.fileExists(atPath: tempDir.path) {
4551
try fm.createDirectory(at: tempDir, withIntermediateDirectories: true)
@@ -51,13 +57,16 @@ class DownloadSignManager: ObservableObject {
5157
}
5258
return
5359
}
54-
60+
5561
let tempIPAURL = tempDir.appendingPathComponent("\(UUID().uuidString).ipa")
56-
57-
// Step 2: Download the IPA
62+
63+
// Step 2: Download the IPA (with progress)
5864
self.downloadIPA(from: downloadURL, to: tempIPAURL) { [weak self] result in
5965
guard let self = self else { return }
60-
66+
67+
// ensure observer cleared
68+
self.downloadProgressObservation = nil
69+
6170
switch result {
6271
case .success:
6372
// Step 3: Get certificate files
@@ -68,80 +77,158 @@ class DownloadSignManager: ObservableObject {
6877
}
6978
return
7079
}
71-
80+
7281
// Step 4: Sign the IPA
7382
self.signIPA(ipaURL: tempIPAURL, p12URL: p12URL, provURL: provURL, password: password, appName: appName)
74-
83+
7584
case .failure(let error):
7685
DispatchQueue.main.async {
7786
self.status = "Download failed: \(error.localizedDescription)"
7887
self.isProcessing = false
7988
}
80-
89+
8190
// Clean up temp file if it exists
8291
try? FileManager.default.removeItem(at: tempIPAURL)
8392
}
8493
}
8594
}
86-
95+
96+
// Download with progress observer and final move to destination
97+
private func downloadIPA(from url: URL, to destinationURL: URL, completion: @escaping (Result<Void, Error>) -> Void) {
98+
let request = URLRequest(url: url)
99+
let session = URLSession(configuration: .default)
100+
101+
DispatchQueue.main.async {
102+
self.status = "Downloading... (0%)"
103+
self.progress = 0.0
104+
}
105+
106+
let task = session.downloadTask(with: request) { [weak self] tempLocalURL, response, error in
107+
guard let self = self else { return }
108+
109+
if let error = error as NSError?, error.domain == NSURLErrorDomain, error.code == NSURLErrorCancelled {
110+
DispatchQueue.main.async {
111+
self.status = "Cancelled"
112+
self.isProcessing = false
113+
self.progress = 0.0
114+
}
115+
completion(.failure(error))
116+
return
117+
}
118+
119+
if let error = error {
120+
completion(.failure(error))
121+
return
122+
}
123+
124+
guard let tempLocalURL = tempLocalURL else {
125+
let err = NSError(domain: "DownloadSignManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "No temp file URL"])
126+
completion(.failure(err))
127+
return
128+
}
129+
130+
do {
131+
// If destination exists, remove it
132+
if FileManager.default.fileExists(atPath: destinationURL.path) {
133+
try FileManager.default.removeItem(at: destinationURL)
134+
}
135+
try FileManager.default.moveItem(at: tempLocalURL, to: destinationURL)
136+
137+
DispatchQueue.main.async {
138+
// make sure progress reflects completed download within its portion
139+
self.progress = self.downloadPortion
140+
self.status = "Downloaded (100%)"
141+
}
142+
143+
completion(.success(()))
144+
} catch {
145+
completion(.failure(error))
146+
}
147+
}
148+
149+
// keep reference for cancel()
150+
self.downloadTask = task
151+
152+
// Observe the Progress
153+
self.downloadProgressObservation = task.progress.observe(\.fractionCompleted, options: [.initial, .new]) { [weak self] progressObj, _ in
154+
guard let self = self else { return }
155+
DispatchQueue.main.async {
156+
let fraction = progressObj.fractionCompleted // 0.0 .. 1.0
157+
// Map download fraction to [0 .. downloadPortion]
158+
let overall = fraction * self.downloadPortion
159+
self.progress = overall
160+
161+
let percent = Int(round(fraction * 100))
162+
self.status = "Downloading... (\(percent)%)"
163+
}
164+
}
165+
166+
task.resume()
167+
}
168+
87169
private func signIPA(ipaURL: URL, p12URL: URL, provURL: URL, password: String, appName: String) {
88170
DispatchQueue.main.async {
89171
self.status = "Starting signing process..."
90-
self.progress = 0.5
172+
// keep progress where download left off (downloadPortion)
91173
}
92-
174+
93175
signer.sign(
94176
ipaURL: ipaURL,
95177
p12URL: p12URL,
96178
provURL: provURL,
97179
p12Password: password,
98180
progressUpdate: { [weak self] status, progress in
99181
DispatchQueue.main.async {
100-
// Signing takes first 50% of progress
101-
let overallProgress = 0.0 + (progress * 0.5)
102-
self?.progress = overallProgress
103-
let percent = Int(overallProgress * 100)
104-
self?.status = "\(status) (\(percent)%)"
182+
guard let self = self else { return }
183+
// Signing maps to the middle portion (downloadPortion .. downloadPortion + signPortion)
184+
let overallProgress = self.downloadPortion + (progress * self.signPortion)
185+
self.progress = overallProgress
186+
let percentOfSign = Int(round(progress * 100))
187+
let overallPercent = Int(round(overallProgress * 100))
188+
self.status = "\(status) (\(overallPercent)%)"
189+
// If you want a "Signing... (xx%)" label specifically, you can use:
190+
// self.status = "Signing... (\(percentOfSign)%)"
105191
}
106192
},
107193
completion: { [weak self] result in
108194
DispatchQueue.main.async {
195+
guard let self = self else { return }
109196
switch result {
110197
case .success(let signedIPAURL):
111-
self?.progress = 0.5
112-
self?.status = "✅ Signed! Installing app..."
113-
198+
// After signing, set progress to end of signing portion
199+
self.progress = self.downloadPortion + self.signPortion
200+
self.status = "✅ Signed! Installing app..."
114201
// Start installation and track progress
115-
self?.startInstallation(signedIPAURL: signedIPAURL)
116-
202+
self.startInstallation(signedIPAURL: signedIPAURL)
203+
117204
// Clean up original downloaded IPA
118205
try? FileManager.default.removeItem(at: ipaURL)
119-
206+
120207
case .failure(let error):
121-
self?.status = "❌ Signing failed: \(error.localizedDescription)"
122-
self?.isProcessing = false
208+
self.status = "❌ Signing failed: \(error.localizedDescription)"
209+
self.isProcessing = false
123210
try? FileManager.default.removeItem(at: ipaURL)
124211
}
125212
}
126213
}
127214
)
128215
}
129-
216+
130217
private func startInstallation(signedIPAURL: URL) {
131218
self.installationTask = Task {
132219
do {
133220
// Get the installation progress stream
134221
let stream = try await installApp(from: signedIPAURL)
135222
self.installationStream = stream
136-
223+
137224
// Process installation progress updates
138225
for try await (installProgress, installStatus) in stream {
139226
await MainActor.run {
140-
// Installation takes second 50% of progress (0.5 to 1.0)
141-
let overallProgress = 0.5 + (installProgress * 0.5)
227+
// Installation maps to final portion:
228+
let overallProgress = self.downloadPortion + self.signPortion + (installProgress * self.installPortion)
142229
self.progress = overallProgress
143-
144-
let percent = Int(overallProgress * 100)
230+
231+
let percent = Int(round(overallProgress * 100))
145232
if installStatus.contains("Successfully") {
146233
self.status = installStatus
147234
self.showSuccess = true
@@ -150,12 +237,13 @@ class DownloadSignManager: ObservableObject {
150237
}
151238
}
152239
}
153-
240+
154241
// Installation completed successfully
155242
await MainActor.run {
243+
self.progress = 1.0
156244
self.status = "✅ Successfully installed app!"
157245
self.showSuccess = true
158-
246+
159247
// Hide progress bar after 5 seconds
160248
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
161249
self.isProcessing = false
@@ -166,7 +254,7 @@ class DownloadSignManager: ObservableObject {
166254
self.installationTask = nil
167255
}
168256
}
169-
257+
170258
} catch {
171259
await MainActor.run {
172260
self.status = "❌ Install failed: \(error.localizedDescription)"
@@ -177,20 +265,23 @@ class DownloadSignManager: ObservableObject {
177265
}
178266
}
179267
}
180-
268+
181269
func cancel() {
182270
downloadTask?.cancel()
183271
installationTask?.cancel()
184272
installationStream = nil
185273
installationTask = nil
186-
274+
275+
// Remove observer
276+
downloadProgressObservation = nil
277+
187278
DispatchQueue.main.async {
188279
self.isProcessing = false
189280
self.status = "Cancelled"
190281
self.progress = 0.0
191282
}
192283
}
193-
284+
194285
private func getAppFolder() -> URL {
195286
let fm = FileManager.default
196287
let documents = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
@@ -200,5 +291,3 @@ class DownloadSignManager: ObservableObject {
200291
}
201292
return appFolder
202293
}
203-
204-
}

0 commit comments

Comments
 (0)