@@ -3,88 +3,57 @@ import Foundation
33import IDeviceSwift
44import Combine
55
6- /// Phase-aware install progress reporting.
7- public enum InstallPhase : String {
8- case uploading = " 📤 Uploading "
9- case installing = " 📲 Installing "
10- case finalizing = " ⏳ Finalizing "
11- case completed = " ✅ Completed "
12- }
13-
146/// Installs a signed IPA on the device using InstallationProxy
15- /// - Returns: an AsyncThrowingStream that yields `(phase: InstallPhase, progress: Double, status: String)`
16- /// where `progress` is phase-relative (0.0...1.0).
17- public func installApp( from ipaURL: URL ) async throws -> AsyncThrowingStream < ( phase: InstallPhase , progress: Double , status: String ) , Error > {
7+ public func installApp( from ipaURL: URL ) async throws -> AsyncThrowingStream < ( progress: Double , status: String ) , Error > {
188 print ( " Installing app from: \( ipaURL. path) " )
199
2010 return AsyncThrowingStream { continuation in
21- // Keep cancellables outside of the Task so termination handler can access them.
22- var cancellables = Set < AnyCancellable > ( )
23-
24- // The actual installation work runs on a Task so we can cancel it if the stream is terminated.
25- let installTask = Task {
11+ Task {
2612 // Start heartbeat to keep connection alive during long install
2713 HeartbeatManager . shared. start ( )
2814
2915 // Create view model to receive installation status updates
3016 let viewModel = InstallerStatusViewModel ( )
31-
32- // Observe progress updates (phase-aware)
33- // uploadProgress and installProgress are assumed to be 0.0...1.0 published Doubles.
17+
18+ // Observe progress updates
19+ var cancellables = Set < AnyCancellable > ( )
20+
21+ // Combine progress updates into a single stream
3422 viewModel. $uploadProgress
3523 . combineLatest ( viewModel. $installProgress)
3624 . sink { uploadProgress, installProgress in
37- // Decide which phase is active and yield phase-relative progress
25+ let overallProgress = ( uploadProgress + installProgress) / 2.0
26+ let currentStage : String
27+
3828 if uploadProgress < 1.0 {
39- // Uploading phase
40- let status = " \( InstallPhase . uploading. rawValue) ... ( \( Int ( uploadProgress * 100 ) ) %) "
41- DispatchQueue . main. async {
42- continuation. yield ( ( phase: . uploading, progress: uploadProgress, status: status) )
43- }
29+ currentStage = " Uploading... "
4430 } else if installProgress < 1.0 {
45- // Installing phase
46- let status = " \( InstallPhase . installing. rawValue) ... ( \( Int ( installProgress * 100 ) ) %) "
47- DispatchQueue . main. async {
48- continuation. yield ( ( phase: . installing, progress: installProgress, status: status) )
49- }
31+ currentStage = " Installing... "
5032 } else {
51- // Finalizing phase — both reported progress are 1.0, so show finalizing
52- let status = " \( InstallPhase . finalizing. rawValue) ... "
53- DispatchQueue . main. async {
54- continuation. yield ( ( phase: . finalizing, progress: 1.0 , status: status) )
55- }
33+ currentStage = " Finalizing... "
5634 }
35+
36+ continuation. yield ( ( progress: overallProgress, status: currentStage) )
5737 }
5838 . store ( in: & cancellables)
59-
60- // Observe installer status for completion/failure
39+
40+ // Handle completion
6141 viewModel. $status
6242 . sink { installerStatus in
6343 switch installerStatus {
6444 case . completed( . success) :
65- // Report completed (phase-relative = 1.0)
66- let status = " \( InstallPhase . completed. rawValue) Successfully installed app! "
67- DispatchQueue . main. async {
68- continuation. yield ( ( phase: . completed, progress: 1.0 , status: status) )
69- continuation. finish ( )
70- }
71- // cleanup
45+ continuation. yield ( ( progress: 1.0 , status: " Successfully installed app! " ) )
46+ continuation. finish ( )
7247 cancellables. removeAll ( )
73-
48+
7449 case . completed( . failure( let error) ) :
75- // Forward the error and finish the stream
76- DispatchQueue . main. async {
77- continuation. finish ( throwing: error)
78- }
50+ continuation. finish ( throwing: error)
7951 cancellables. removeAll ( )
80-
52+
8153 case . broken( let error) :
82- // Broken connection or other fatal error
83- DispatchQueue . main. async {
84- continuation. finish ( throwing: error)
85- }
54+ continuation. finish ( throwing: error)
8655 cancellables. removeAll ( )
87-
56+
8857 default :
8958 break
9059 }
@@ -94,40 +63,18 @@ public func installApp(from ipaURL: URL) async throws -> AsyncThrowingStream<(ph
9463 do {
9564 // Create the installation proxy
9665 let installer = await InstallationProxy ( viewModel: viewModel)
97-
66+
9867 // Perform the actual installation
9968 try await installer. install ( at: ipaURL)
100-
101- // Allow a short grace period for final signals to arrive
102- try await Task . sleep ( nanoseconds: 500_000_000 ) // 0.5s
103-
104- // NOTE: viewModel.$status should drive the final .completed event;
105- // if for some reason it didn't, ensure we finish here.
106- // (If status already finished the continuation, these calls are no-ops.)
107- DispatchQueue . main. async {
108- // If not yet finished, mark completed.
109- continuation. yield ( ( phase: . completed, progress: 1.0 , status: " \( InstallPhase . completed. rawValue) " ) )
110- continuation. finish ( )
111- }
112-
113- cancellables. removeAll ( )
69+
70+ // Wait a moment for completion
71+ try await Task . sleep ( nanoseconds: 1_000_000_000 ) // 1 second
72+
73+ print ( " Installation completed successfully! " )
11474 } catch {
115- // On error, forward and cleanup
116- DispatchQueue . main. async {
117- continuation. finish ( throwing: error)
118- }
75+ continuation. finish ( throwing: error)
11976 cancellables. removeAll ( )
12077 }
121- } // End Task
122-
123- // If the consumer cancels/terminates the stream, cancel the install task and cleanup.
124- continuation. onTermination = { @Sendable _ in
125- installTask. cancel ( )
126- cancellables. removeAll ( )
12778 }
12879 }
12980}
130-
131-
132-
133-
0 commit comments