Logo
Skip to main content

How Swift Concurrency Helped Us Evolve a Production iOS SDK

iOS

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.

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.

Watch now

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:

  1. A message arrives to the WebSocket client.
  2. The message’s raw payload is forwarded to the InteractionStateRepository.
  3. The raw payload is parsed into InteractionState by the InteractionStateParser.
  4. The InteractionStateRepository updates its MutableProperty<InteractionState?> with the new interaction state.
  5. 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:

Swift
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.

Project details
Cross-Platform Data Backup Solution Development

5. Migrating to Swift concurrency using AsyncStream

Introducing AsyncStream-based event delivery

We started by replacing SignalProducer with AsyncStream:

Swift
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:

Swift
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:

Swift
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:

Swift
 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:

Swift
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:

Swift
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:

Swift
@MainActor
func observeinteractionState() async {
    for await state in repository.interactionState() {
        updateUI(with: state)
    }
}

Here’s the full ReactiveSwift implementation:

Swift
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:

Swift
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.

Learn more
Reverse Engineering an Undocumented macOS API

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.

Choose Apriorit for software development projects

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>

Have a question?

Ask our expert!

Tell us about
your project

...And our team will:

  • Process your request within 1-2 business days.
  • Get back to you with an offer based on your project's scope and requirements.
  • Set a call to discuss your future project in detail and finalize the offer.
  • Sign a contract with you to start working on your project.

Do not have any specific task for us in mind but our skills seem interesting? Get a quick Apriorit intro to better understand our team capabilities.

* By sending us your request you confirm that you read and accepted our Terms & Conditions and Privacy Policy.