Swift concurrency has become the standard approach to asynchronous programming on Apple platforms. However, adopting it in an SDK is very different from using it in an application.
During SDK projects involving Swift concurrency, your team might face unexpected constraints that can slow integration, increase maintenance costs, and make failures harder for businesses to diagnose and resolve.
In this article, we share Apriorit’s experience migrating a production-grade customer support iOS SDK (used in mobile banking apps) from ReactiveSwift to native Swift concurrency.
In the text below, we explain:
- The architectural challenges we encountered.
- The migration strategy we chose.
- How Swift concurrency helped us improve maintainability, reliability, and debugging without disrupting existing integrations.
This article will be particularly useful for product and team leaders who are building and maintaining client-facing SDKs for FinTech, telecom, or enterprise software projects.
If you’re evaluating Swift modernization initiatives, looking to reduce dependency-related integration issues, or planning a low-risk SDK migration, this article is for you.
Contents:
- Why should businesses care about concurrency modernization in their SDKs?
- Migrating from ReactiveSwift to Swift concurrency: A practical example for an iOS SDK
- 1. Defining the reasons for migration
- 2. Evaluating approaches
- 3. Analyzing the original architecture: event-driven state repositories
- 4. Preparing a migration strategy
- 5. Migrating to Swift concurrency using AsyncStream
- What we achieved
- Lessons learned: What your team should know about SDK migration to Swift concurrency
- How can Apriorit help you empower iOS, macOS, and SDK projects?
Why should businesses care about concurrency modernization in their SDKs?
As SDKs mature, the cost of architectural decisions becomes increasingly visible. Concurrency models influence everything from dependency management and debugging to API design and long-term maintainability.
Unlike applications, SDKs run inside environments they do not control. The thing is that even a single dependency conflict, lifecycle issue, or compatibility problem can affect dozens or hundreds of client applications. You must keep in mind that decisions about concurrency architecture directly influence integration reliability, maintenance costs, and release timelines.
Yehor, Software Designer at Apriorit
Many long-lived SDKs still rely on a combination of reactive frameworks, callbacks, and Grand Central Dispatch (GCD). While such solutions may remain stable in production, they often introduce challenges that become more visible as products grow and evolve:
- Dependency conflicts between SDKs and host applications
- Increasing maintenance costs for legacy concurrency frameworks
- Harder debugging due to callback chains and reactive pipelines
- Pressure to support modern async/await-based APIs
- High migration risk when modernizing business-critical systems
Swift’s ecosystem is now evolving around Swift concurrency. New platform APIs are designed with async/await in mind, and many development teams expect native asynchronous interfaces when integrating third-party SDKs.
For teams maintaining commercial SDKs, modernization moves from Should we? to How will we do it?
In the sections below, we show how we modernized a production customer support iOS SDK used in mobile banking applications. Rather than performing a large-scale rewrite, we focused on removing a third-party dependency, improving maintainability, and preparing the SDK for the future while preserving its existing architecture.
Need safe SDK upgrades without rewrites?
Work with our macOS and iOS experts to modernize concurrency models, improve maintainability, and prepare your SDK for future platform changes without disruption.
Migrating from ReactiveSwift to Swift concurrency: A practical example for an iOS SDK
For our example of SDK modernization, we chose an SDK used in mobile banking applications.
This SDK relies on WebSocket-based communication to coordinate multiple concurrent workflows, including interaction management, messaging, audio and video streaming, and user interaction events.
Let’s explore the entire process step by step, from analyzing the issues in the SDK and choosing the approach to the migration itself and the results of our analysis.
1. Defining the reasons for migration
The SDK in question was functioning reliably and posed no immediate challenges. However, the Apriorit team identified several long-term risks that were worth addressing proactively.
While working with the SDK on a project, our team uncovered two major concerns:
1. Risks of third-party dependencies. A core part of the SDK relies on ReactiveSwift to propagate state changes from socket-driven repositories to other SDK components.
While this approach proves stable in production, it introduces a significant third-party dependency into the SDK’s public dependency graph, creating several challenges for the project:
- Client applications may already depend on a different version of the same framework.
- Additional dependencies increase both SDK and application binary size.
- As projects grow, dependency version conflicts become more likely and harder to resolve.
2. Readability and debuggability issues. Other parts of the SDK rely on a combination of GCD and callback-based asynchronous code.
While functional, this approach introduces several challenges:
- Deep callback chains that are difficult to reason about.
- Execution paths that are hard to trace during debugging.
- Error handling is scattered across multiple layers of asynchronous logic.
2. Evaluating approaches
Once we decided to remove ReactiveSwift from the SDK, our primary requirement was clear: replace it with a platform-native solution without redesigning the existing architecture.
There are two realistic approaches that could satisfy our most important constraint:
1. Combine seems like a natural candidate for replacing ReactiveSwift from a technical perspective, as it:
- Provides publisher–subscriber abstractions similar to ReactiveSwift’s event streams.
- Integrates well with existing callback-based and GCD-based code.
- Allows many SignalProducer-based APIs to be replaced with AnyPublisher, preserving existing patterns.
- Requires minimal architectural changes and allows the SDK to retain its existing reactive programming model.
2. Swift concurrency seems a fitting choice because:
- async/await is becoming the standard way to express asynchronous operations in Swift, making Swift concurrency a future-proof option.
- Almost all modern Apple frameworks expose async-first APIs.
- async APIs are often easier to read, compose, and maintain than callback-based alternatives.
- Structured concurrency and actors are built directly into the language and runtime.
Both provide native alternatives to reactive streams and allow us to preserve the SDK’s high-level architecture, including state repositories, event streams, and observer-based state propagation.
For our project, we chose the second option since:
- Migrating to Swift concurrency can eliminate an entire class of issues by replacing external framework dependencies with platform-native concurrency primitives.
- Swift concurrency’s async/await model can replace nested execution flows with a more linear and structured approach. As a result, the code will be easier to read, maintain, and debug.
- While Combine remains fully supported, it is no longer Apple’s primary abstraction for general-purpose concurrency.
In this case, iOS Swift concurrency appears to be the more future-proof choice. It can address our immediate goals of removing ReactiveSwift, supporting modern async APIs, and aligning the SDK with the direction of the Swift ecosystem for years to come.
Watch webinar
Migration from macOS Kexts to System Extensions
Discover proven approaches to modernizing macOS software while reducing migration risks and maintaining compatibility with evolving Apple platform requirements.
3. Analyzing the original architecture: event-driven state repositories
The SDK is built around WebSocket-driven events. To keep responsibilities isolated, different state repositories manage specific domains, including interaction lifecycle, typing indicators, and media streaming.
One of the central components is InteractionStateRepository, which tracks interaction started, interaction failed, and interaction ended. Before migration, its flow looked like this:
- A message arrives to the WebSocket client.
- The message’s raw payload is forwarded to the
InteractionStateRepository. - The raw payload is parsed into
InteractionStateby theInteractionStateParser. - The
InteractionStateRepositoryupdates itsMutableProperty<InteractionState?>with the new interaction state. - Event consumers (subscribers) subscribe using
SignalProducer<InteractionState?, Never>and receive updates.
This iOS SDK architecture provides a clear separation of concerns and predictable event propagation, but it also introduces a third-party dependency to the SDK:
final class StateRepository {
private let parser: InteractionStateParser
private var activeState: MutableProperty<InteractionState?> = MutableProperty(nil)
init(parser: InteractionStateParser) {
self.parser = parser
}
/// Use to subscribe to `InteractionState` changes.
func interactionState() -> SignalProducer<InteractionState?, Never> {
return activeState.producer
}
/// Use to reset current `InteractionState`.
func reset() {
activeState.modify { $0 = nil }
}
/// Use to handle InteractionState-related socket events.
func handle(_ json: [String: Any]) {
let state = parser.parse(json: json)
activeState.value = state
}
}4. Preparing a migration strategy
To minimize risk and avoid disrupting a production iOS SDK, we adopted an incremental, architecture-preserving migration strategy:
- Migrate state repositories one at a time.
- Isolate development in a dedicated feature branch to simplify reviews and validation.
- Don’t introduce compatibility or bridging layers between the old and new concurrency models.
Such an approach should keep each change small and easy to verify while preserving the existing architecture throughout the migration.
Related project
Cross-Platform Data Backup Solution Development: Windows, Android, macOS, iOS
Explore how Apriorit developed and launched backup software across four operating systems. As a result, our client managed to double their user base and speed up market entry.
5. Migrating to Swift concurrency using AsyncStream
Introducing AsyncStream-based event delivery
We started by replacing SignalProducer with AsyncStream:
func interactionState() -> AsyncStream<InteractionState?>Each call returns a new AsyncStream instance. This keeps consumers isolated from one another and simplifies stream lifecycle management.
Internally, the repository maintains a registry of continuations, with one continuation per active subscriber, and protects it with a lock:
private var continuations: [UUID: AsyncStream<InteractionState?>.Continuation] = [:]
private let lock = NSLock()Registering a new subscriber
When a consumer requests a stream, the repository creates a new AsyncStream and stores its continuation:
func interactionState() -> AsyncStream<InteractionState?> {
let id = UUID()
return AsyncStream { [weak self] continuation in
guard let self else {
continuation.finish()
return
}
lock.withLock {
continuations[id] = continuation
}To avoid leaks and keep the registry consistent, we ensured that continuation is removed when the stream terminates:
continuation.onTermination = { [weak self] _ in
guard let self else { return }
self.lock.withLock {
self.continuations[id] = nil
}
}
}
}Delivering new states
When a new socket event is parsed into an InteractionState, the repository makes a copy of the active continuations under the lock and then yields the value outside of the critical section:
func handle(_ json: [String: Any]) {
let state = parser.parse(json: json)
let sinks = lock.withLock {
Array(continuations.values)
}
for continuation in sinks {
continuation.yield(state)
}
}Finishing streams on the interaction end
Finally, when the interaction lifecycle ends, all active streams are completed explicitly:
func reset() {
let sinks = lock.withLock {
let values = Array(continuations.values)
continuations.removeAll()
return values
}
sinks.forEach { $0.finish() }
}This approach separates the implementation into clear responsibilities thanks to:
- Stream creation
- Lifecycle tracking
- Event emission
- Deterministic completion
Event consumption
Consumers observe state changes using a simple sequential loop:
@MainActor
func observeinteractionState() async {
for await state in repository.interactionState() {
updateUI(with: state)
}
}Here’s the full ReactiveSwift implementation:
class InteractionStateRepository {
private let parser: InteractionStateParser
var activeState: MutableProperty< InteractionState?> = MutableProperty(nil)
func interactionState() -> SignalProducer< InteractionState?, Never> {
return activeState.producer
}
func handle(json: [String: Any]) {
let state = parser.parse(json: json)
activeState.modify { $0 = state }
}
func reset() {
activeState.modify { $0 = nil }
}
}
class InteractionStateConsumer {
private let interactionStateRepository: InteractionStateRepository
func observeInteraction State() {
interactionStateRepository
.interactionState()
.observe(on: UIScheduler())
.startWithValues { state in
updateUI(with: state)
}
}
}And here’s the full Swift concurrency implementation:
class InteractionStateRepository {
private let parser: InteractionStateParser
private var continuations: [UUID: AsyncStream<InteractionState?>.Continuation] = [:]
private let lock = NSLock()
init(parser: InteractionStateParser) {
self.parser = parser
}
func interactionState() -> AsyncStream<InteractionState?> {
let id = UUID()
return AsyncStream { continuation in
lock.withLock {
continuations[id] = continuation
continuation.onTermination = { _ in
self.lock.withLock {
self.continuations[id] = nil
}
}
}
}
}
func handle(_ json: [String: Any]) {
let state = parser.parse(json: json)
let sinks = lock.withLock {
Array(continuations.values)
}
for continuation in sinks {
continuation.yield(state)
}
}
func reset() {
let sinks = lock.withLock {
let values = Array(continuations.values)
continuations.removeAll()
return values
}
sinks.forEach { $0.finish() }
}
}
class InteractionStateConsumer {
private let interactionStateRepository: InteractionStateRepository
@MainActor
func observeInteractionState() async {
let task = Task {
for await state in interactionStateRepository.interactionState() {
updateUI(with: state)
}
}
return TaskDisposable(task)
}
}At this point, the migration is effectively complete.
The repository still exposes an event stream, and consumers still react to state changes, but the implementation now relies entirely on Swift concurrency primitives. The architectural concepts remain the same, while the dependency on ReactiveSwift disappears.
What we achieved
The migration achieved its primary goals without requiring a large-scale architectural redesign.
By successfully introducing Swift concurrency to a production socket-driven iOS SDK, we’ve proven that modern concurrency can be pragmatically adopted without architectural rewrites. The SDK is now easier to reason about and safer to integrate, and it aligns with Apple’s long-term concurrency direction.
Yehor, Software Designer at Apriorit
Swift concurrency is more than just a feature of applications; thoughtfully applied, it is a powerful tool to build and evolve complex SDKs.
Here’s a brief summary of what the Apriorit team achieved:
- Simpler dependency management. Removing ReactiveSwift eliminated SDK integration issues caused by conflicts between ReactiveSwift versions used in our SDK and integrator apps.
- Preserved the existing architecture. The migration didn’t require a redesign of the repository-based event architecture. State repositories, event propagation, and lifecycle management remained conceptually unchanged. This allowed the migration to proceed incrementally while minimizing review effort and regression risk.
- Improved readability and debuggability. Swift concurrency replaced a combination of ReactiveSwift pipelines, callbacks, and GCD-based coordination with a more explicit concurrency model. As a result, asynchronous flows became easier to follow, stack traces became more readable, and debugging execution paths now requires less framework-specific knowledge.
- Aligned the SDK with the future of Swift. The migration positioned the SDK around Apple’s primary concurrency model.
Read also
How to Reverse Engineer an Undocumented macOS API to Use It in a Swift Project
Improve the stability and performance of your macOS solutions. See how to analyze undocumented APIs, identify hidden implementation details, and safely evaluate integration opportunities before they affect your product roadmap.
Lessons learned: What your team should know about SDK migration to Swift concurrency
Looking back, the migration reinforced several principles that may be useful for teams modernizing long-lived SDKs:
1. Preserve behavior before redesigning architecture. For mature SDKs, incremental migrations often carry less risk than large-scale rewrites. We focused on replacing the event–delivery mechanism while keeping existing repositories, event streams, and lifecycle rules intact.
2. Actors aren’t always the right first step. Actors are powerful, but introducing them can force broader API changes by adding asynchronous boundaries. The lesson is not to avoid actors but to introduce them when they solve a specific problem rather than as a mandatory part of every Swift concurrency migration.
3. Treat stream lifetime as a domain concern. We tied stream completion to the interaction lifecycle rather than UI lifecycle events. This made state observation deterministic and easier to reason, which is particularly important for SDKs operating inside applications they don’t control.
4. Isolate subscribers whenever possible. Providing each consumer with its own AsyncStream reduced coupling and simplified lifecycle management, cancellation, and memory ownership.
5. Swift concurrency doesn’t eliminate synchronization. Concurrency primitives improve code structure, but a shared mutable state still requires protection when it exists outside actor isolation.
How can Apriorit help you empower iOS, macOS, and SDK projects?
Apriorit is ready to assist you with projects of any complexity. From FinTech and telecom to automotive and healthcare, our experts know how to tie business goals and technology requirements to industry standards.

Whatever project you’re working on, Apriorit will help you eliminate dependency conflicts, migrate SDKs to newer technologies, and keep your product up to date with:
- Custom macOS development. Design and modernize macOS applications, frameworks, and SDKs while maintaining compatibility with evolving platform requirements.
- Professional iOS development. Create and enhance iOS applications and SDKs using modern architectures and Apple-native technologies such as Swift concurrency.
- Enterprise MDM and MAM projects. Build enterprise mobility solutions that securely manage devices, applications, and user access across large-scale deployments.
- API development and integration. Connect products with internal and external services through scalable APIs while reducing integration complexity and dependency-related risks.
- Software modernization services. Safely upgrade applications and SDKs with platform-native technologies, reduce dependency-related risks, and keep solutions maintainable.
Struggling with complex SDK modernization?
Partner with engineers who can migrate your iOS or macOS SDK to modern Swift technologies while preserving compatibility, reducing dependencies, and minimizing integration risks.
FAQ
Why should I migrate from Combine to Swift concurrency?
Swift concurrency is becoming the primary model for asynchronous programming across the Apple ecosystem. Because features such as async/await, structured concurrency, and actors are built directly into the Swift language, they offer a more consistent and future-oriented foundation than framework-based solutions. Migrating also helps align your SDK or application with modern Apple APIs, simplifies asynchronous code, and reduces the need for additional abstractions when exposing public async interfaces.
Can Swift concurrency fully replace reactive frameworks in an SDK?
<p>The answer depends on your architecture and requirements. Some advanced reactive patterns may require additional implementation, so the best approach is to evaluate which reactive features your SDK actually uses before planning a migration.</p>
<p>However, in many SDK scenarios, the answer is yes. Swift concurrency provides native building blocks such as AsyncSequence, AsyncStream, tasks, and actors that can cover common reactive use cases, including event streams, asynchronous pipelines, and state propagation.</p>
How does Swift concurrency reduce integration issues?
Unlike third-party reactive frameworks, Swift concurrency is part of the Swift language and Apple platforms. This eliminates a dependency from your SDK’s public dependency graph, reducing the risk of version conflicts, transitive dependency issues, and compatibility problems in client applications.
Does adopting Swift concurrency require rewriting my entire architecture?
Not necessarily. Many teams successfully adopt Swift concurrency incrementally while preserving their existing architecture. Repositories, services, event-driven workflows, and state management components can often remain conceptually unchanged even when the underlying concurrency mechanisms evolve.
What is the best strategy to migrate a large SDK to Swift concurrency incrementally?
<p>A low-risk migration usually starts with modernizing the most isolated asynchronous components rather than attempting a full rewrite:</p>
<ul class=apriorit-list-markers-green>
<li>Replace existing event delivery mechanisms and <a href=third-party-api-integration-best-practices>asynchronous APIs</a> one area at a time, keeping architectural changes to a minimum.</li>
<li>Avoid introducing unnecessary compatibility layers unless they provide clear value.</li>
<li>Validate each migration step independently.</li>
<li>Adopt stricter concurrency checks gradually.</li>
</ul>
<p>This approach allows teams to modernize large codebases while keeping reviews manageable, reducing regression risk, and maintaining compatibility with existing customers.</p>


