@@ -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