← Back to list

Combine subscriptions and async iteration lifecycles in Swift

This detailed explanation compares Combine subscriptions and async iteration lifecycles in Swift, highlighting their strengths, weaknesses…

abdul ahad · 2025-04-07 10:51 · 2 claps · 2.4 min read paywalled
#combine #cancellable #asyncsequence #lifecycle #swift-concurrency
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

Combine subscriptions and async iteration lifecycles in Swift

jPhoto by Brett Jordan on Unsplash

jPhoto by Brett Jordan on Unsplash

This detailed explanation compares Combine subscriptions and async iteration lifecycles in Swift, highlighting their strengths, weaknesses, and how to handle them in various scenarios effectively.

Combine Subscription Lifecycle

In Combine, subscriptions are tied to the lifecycle of an AnyCancellable. Here's the main takeaway:

Automatic Cleanup:

  • A Combine subscription is automatically torn down when its associated AnyCancellable is deallocated. This ensures no lingering subscriptions exist once they are no longer needed.

“Safe by Default”:

  • Combine’s design prevents unnecessary resource usage. If you forget to retain the AnyCancellable, the subscription ends as soon as it is created.

Code Example:

In the first example, storing the AnyCancellable locally in a function results in the subscription being immediately deallocated:

func nonStoredCancellable() {  
   let cancellable = URLSession.shared.dataTaskPublisher(for: URL(string: "https://example.com")!)
         .sink(receiveCompletion: { _ in print("Completion") }    
          .receiveValue: { _ in print("Value Received") })
 }
  • No output is produced because the cancellable goes out of scope at the end of the function.
  • Storing the AnyCancellable as a property of a class ensures the subscription remains active:
class Example {    

 var cancellable: AnyCancellable?  

 func storedCancellable() {      
   cancellable = URLSession.shared.dataTaskPublisher(for: URL(string: "https://example.com")!)  
           .sink(receiveCompletion: { _ in print("Completion") }  
            .receiveValue: { _ in print("Value Received") })   
  } }

Manual Subscription Control:

  • Subscriptions tied to an AnyCancellable can be managed manually. Once the AnyCancellable is deallocated, Combine ensures resources are cleaned up.

Async Iteration Lifecycle

In AsyncSequence, the lifecycle of an iteration task is different:

No Automatic Cleanup:

  • Tasks created for iterating over an AsyncSequence are not automatically tied to an object lifecycle. This can result in leaks if the task is not explicitly cancelled.

Lifecycle Management:

  • Tasks are persistent and require manual cancellation when no longer needed. Without explicit cancellation, tasks can run indefinitely, especially when iterating over infinite sequences.

Code Example:

  • Using an async for loop to iterate over a subject's values:
class SequenceDrivenExample {
  let subject: CurrentValueSubject<Int, Never> 
   var task: Task<Void, Never>?   

   init(subject: CurrentValueSubject<Int, Never>) {      
   self.subject = subject    
 }    

  func subscribe() {    
     task = Task { [subject] in    
         for await value in subject.values {   
              print("Received: \(value)")       
      }      
   }    
 }     
 deinit {     
    task?.cancel()   
      print("Sequence Driven Example Deinitialized")  
   } 
}

Manual Task Cancellation:

  • If the task is not cancelled (e.g., via deinit), it will continue running even after the associated object is deallocated. This contrasts with Combine's automatic cleanup.

Key Differences

Improving Async Iteration Lifecycle

To manage async tasks more like Combine, you can store tasks in a property and ensure they are cancelled when no longer needed:

Store Tasks in AnyCancellable:

  • Extend Task to integrate with Combine's AnyCancellable mechanism:
extension Task {  
   func store(in cancellables: inout Set<AnyCancellable>) {   
      cancellables.insert(AnyCancellable { 
      self.cancel()
     })  
   } 
}

Example Usage:

class SequenceDrivenExample { 
    let subject: CurrentValueSubject<Int, Never> 
    var cancellables = Set<AnyCancellable>()   

   init(subject: CurrentValueSubject<Int, Never>) { 
        self.subject = subject   
  }     

  func subscribe() {  
       Task { [subject] in     
        for await value in subject.values { 
                print("Received: \(value)")   
          }       
        }.store(in: &cancellables)  
   }
 }

Conclusion

  • Combine provides a “safe by default” subscription model, automatically managing the subscription lifecycle.
  • Async Iteration requires manual lifecycle management but offers flexibility in handling asynchronous workflows.
  • Proper cleanup of tasks (manual cancellation or integrating with AnyCancellable) ensures robust and leak-free async code.

메타데이터
post_id
14d779df8219
slug
combine-subscriptions-and-async-iteration-lifecycles-in-swift-14d779df8219
url
https://medium.com/@abdulahd1996/combine-subscriptions-and-async-iteration-lifecycles-in-swift-14d779df8219
canonical_url
https://medium.com/@abdulahd1996/combine-subscriptions-and-async-iteration-lifecycles-in-swift-14d779df8219
author_url
https://medium.com/@abdulahd1996
status
ok
fetched_at
2026-06-21 19:25:17