How combining Rust’s computational power with SwiftUI’s elegance creates a blazing-fast financial charting app

The Challenge: Performance Meets User Experience
When building financial applications, developers face a fundamental tension. On one hand, users expect buttery-smooth 60 FPS animations and real-time updates. On the other hand, calculating technical indicators like RSI, MACD, and moving averages across thousands of data points is computationally expensive. This dilemma is especially true when you need to recalculate these indicators on every price update from a WebSocket stream.
Traditional approaches force you to choose between performance and developer experience. You can write everything in a low-level language like C++ for maximum speed, sacrificing the elegant declarative UI patterns of modern frameworks. For an excellent developer experience, you can also use pure Swift/SwiftUI, albeit with the performance cost of a garbage-collected language.
What if you didn’t have to choose?
The Solution: Hybrid Architecture
The Financial-RustAndSwift project demonstrates a third path by combining the strengths of both worlds. Rust handles all computationally intensive calculations, while SwiftUI provides the beautiful, reactive user interface. The result is an application that delivers native performance where it matters most while maintaining the developer-friendly patterns that make iOS development enjoyable.
The architecture creates a clear separation of concerns. At the bottom layer, Rust implementations of technical indicators run with zero garbage collection overhead and benefit from SIMD autovectorization. Above that, a thin C FFI bridge connects the Rust code to Swift. Finally, SwiftUI manages the presentation layer with its reactive data flow and declarative syntax.
Real Performance Gains: The Numbers Don’t Lie
The performance improvements from this hybrid approach are substantial and measurable. When processing one thousand candlesticks, the Rust implementations consistently outperform pure Swift alternatives by factors of two to four and a half.
The Simple Moving Average calculation runs in just 0.8 milliseconds with Rust compared to 2.1 milliseconds in pure Swift, achieving a 2.6x speedup. The Exponential Moving Average completes in 0.6 milliseconds versus 1.8 milliseconds, a 3.0x improvement. The more complex calculations show even more dramatic gains. The Relative Strength Index drops from 5.4 milliseconds to just 1.2 milliseconds, achieving a 4.5x speedup. The MACD indicator, which combines multiple moving averages, sees a similar 4.5x improvement, running in 1.5 milliseconds compared to 6.8 milliseconds in pure Swift.
These improvements stem from several factors working together. Rust’s lack of garbage collection eliminates unpredictable pauses during calculations. The compiler’s aggressive optimisation, including SIMD vectorisation, allows calculations to process multiple data points simultaneously. Efficient memory layout and cache usage reduce the time spent waiting for data from RAM. Finally, Rust’s zero-cost abstractions mean that writing clean, maintainable code doesn’t sacrifice runtime performance.
Deep Dive: The Rust Implementation
At the heart of the performance story is the Rust implementation of technical indicators. The code lives in the indicators.rs file and demonstrates how systems-level programming can be both safe and fast.
Consider the implementation of the Simple Moving Average, one of the most fundamental technical indicators. The Rust version is remarkably straightforward yet highly optimised.
pub fn calculate_sma(data: &[f32], period: usize) -> Vec<f32> {
let mut result = Vec::with_capacity(data.len());
if data.len() < period {
return result;
}
// Fill initial values with NaN
for _ in 0..period - 1 {
result.push(f32::NAN);
}
// Calculate SMA for each window
for i in period - 1..data.len() {
let sum: f32 = data[i - period + 1..=i].iter().sum();
result.push(sum / period as f32);
}
result
}
This implementation makes several key choices that contribute to its performance. Pre-allocating the result vector with the exact capacity needed eliminates reallocation overhead. Using slicing operations leverages Rust’s optimised iterator implementations. The compiler can automatically vectorise the sum calculation and process multiple values per CPU instruction. Most importantly, everything happens without any heap allocations in the hot loop.
The Exponential Moving Average builds on this foundation with a more sophisticated algorithm that gives more weight to recent prices:
pub fn calculate_ema(data: &[f32], period: usize) -> Vec<f32> {
let mut result = Vec::with_capacity(data.len());
if data.len() < period {
return result;
}
let multiplier = 2.0 / (period + 1) as f32;
// Initialize with SMA
let initial_sma: f32 = data[..period].iter().sum::<f32>() / period as f32;
for _ in 0..period - 1 {
result.push(f32::NAN);
}
result.push(initial_sma);
// Calculate EMA recursively
for i in period..data.len() {
let ema = (data[i] - result[i - 1]) * multiplier + result[i - 1];
result.push(ema);
}
result
}
The EMA calculation demonstrates one of Rust’s key advantages: the recursive nature of the algorithm means each calculation depends on the previous result, which could be problematic in some languages. However, Rust’s ownership system ensures memory safety without runtime checks, and the tight loop with simple arithmetic operations allows the compiler to generate highly efficient machine code.
Bridging Two Worlds: The FFI Layer
The magic that connects Rust’s performance to Swift’s elegance happens through the Foreign Function Interface. The FFI implementation in lib.rs carefully manages the boundary between Rust’s memory model and Swift’s requirements.
The bridge uses a custom structure to safely pass arrays across the FFI boundary:
#[repr(C)]
pub struct FloatArray {
pub data: *mut f32,
pub len: usize,
pub capacity: usize,
}
The repr(C) annotation ensures that Rust uses the same memory layout as C, making the structure compatible with Swift. The structure contains a pointer to the data, along with length and capacity information that Swift needs to properly interpret the array.
Each indicator function gets wrapped with FFI-safe code that handles the marshalling between Swift and Rust:
#[no_mangle]
pub unsafe extern "C" fn calculate_sma_ffi(
data: *const f32,
len: usize,
period: usize,
) -> FloatArray {
if data.is_null() || len == 0 {
return FloatArray {
data: std::ptr::null_mut(),
len: 0,
capacity: 0,
};
}
let slice = std::slice::from_raw_parts(data, len);
let result = calculate_sma(slice, period);
let mut result_boxed = result.into_boxed_slice();
let ptr = result_boxed.as_mut_ptr();
let len = result_boxed.len();
let capacity = len;
std::mem::forget(result_boxed);
FloatArray {
data: ptr,
len,
capacity,
}
}
This FFI function demonstrates the careful dance required at language boundaries. The no_mangle attribute prevents Rust from changing the function name, ensuring Swift can find it. The unsafe extern “C” declaration marks this functionality as a C-compatible function that can be called across the FFI boundary. Inside, the code carefully converts between raw pointers and Rust’s safe slice types, performs the calculation, then hands ownership of the result back to Swift by forgetting the boxed slice after extracting its pointer.
The Swift side provides a clean, idiomatic interface that hides this complexity:
@_silgen_name("calculate_sma_ffi")
private static func calculate_sma_ffi(
_ data: UnsafePointer<Float>,
_ len: Int,
_ period: Int
) -> FloatArray
static func calculateSMA (data: [Float], period: Int) -> [Float] {
guard !data.isEmpty && period > 0 && period <= data.count else {
return []
}
let result = data.withUnsafeBufferPointer { bufferPtr in
calculate_sma_ffi(bufferPtr.baseAddress!, data.count, period)
}
guard result.data != nil else {
return []
}
let array = Array(UnsafeBufferPointer(
start: result.data,
count: result.len
))
free_float_array(result)
return array
}
This Swift wrapper handles all the unsafe pointer operations internally while exposing a completely safe interface to the rest of the application. The withUnsafeBufferPointer method temporarily provides direct access to the array’s memory, avoiding any copying. After retrieving the result, the code creates a new Swift array from the Rust-allocated memory, then explicitly frees the Rust memory to prevent leaks.
Real-Time Data: The WebSocket Integration
Performance calculations mean nothing without data to process. The application connects to Binance’s WebSocket API to receive real-time trade data for Bitcoin against USDT. The BinanceWebSocketService demonstrates how to integrate live financial data into a SwiftUI application.
The service maintains its state using published properties that automatically trigger UI updates:
class BinanceWebSocketService: ObservableObject {
@Published var connectionState: ConnectionState = .disconnected
@Published var latestTrade: TradeData?
private var webSocketTask: URLSessionWebSocketTask?
private let symbol: String
init(symbol: String = "btcusdt") {
self.symbol = symbol.lowercased()
}
func connect() {
let url = URL(string: "wss://stream.binance.com:9443/ws/\(symbol)@trade")!
webSocketTask = URLSession.shared.webSocketTask(with: url)
connectionState = .connecting
webSocketTask?.resume()
receiveMessage()
}
}
The connection process uses URLSession’s native WebSocket support, avoiding external dependencies. The service connects to Binance’s trade stream endpoint, which provides individual trade data as it happens on the exchange. Each trade includes the price, quantity, timestamp, and whether it was a buy or sell order.
The message-receiving loop demonstrates Swift’s modern async patterns:
private func receiveMessage() {
webSocketTask?.receive { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let message):
switch message {
case .string(let text):
self.handleMessage(text)
case .data(let data):
if let text = String(data: data, encoding: .utf8) {
self.handleMessage(text)
}
@unknown default:
break
}
// Keep receiving messages
self.receiveMessage()
case .failure(let error):
print("WebSocket error: \(error)")
self.connectionState = .disconnected
}
}
}
The recursive nature of receiveMessage creates a continuous loop that processes messages as they arrive. This pattern ensures the connection stays active and responsive without blocking the main thread. When a message arrives, the code decodes the JSON into a structured TradeData model that the rest of the application can use.
Orchestrating Everything: The Data Manager
The FinancialDataManager acts as the central coordinator, bringing together WebSocket data, Rust calculations, and UI state management. This class demonstrates how to structure a complex SwiftUI application with multiple data sources and heavy computations.
The manager maintains all the state needed for the UI:
class FinancialDataManager: ObservableObject {
@Published var chartData: ChartData?
@Published var currentPrice: String = "0.00"
@Published var priceChange: String = "0.00"
@Published var priceChangePercent: String = "0.00"
private let webSocketService: BinanceWebSocketService
private var cancellables = Set<AnyCancellable>()
init() {
self.webSocketService = BinanceWebSocketService(symbol: "btcusdt")
setupSubscriptions()
loadInitialData()
webSocketService.connect()
}
}
The published properties automatically trigger UI updates whenever they change, following SwiftUI’s reactive programming model. The Combine framework handles subscriptions to the WebSocket service, creating a clean data flow from network events to UI updates.
When new trade data arrives, the manager processes it and recalculates all indicators:
func calculateIndicators() {
guard let candles = chartData?.candles, !candles.isEmpty else { return }
let closePrices = candles.map { $0.close }
// All calculations use Rust for maximum performance
let sma20 = TechnicalIndicators.calculateSMA(data: closePrices, period: 20)
let sma50 = TechnicalIndicators.calculateSMA(data: closePrices, period: 50)
let ema12 = TechnicalIndicators.calculateEMA(data: closePrices, period: 12)
let ema26 = TechnicalIndicators.calculateEMA(data: closePrices, period: 26)
let rsi = TechnicalIndicators.calculateRSI(data: closePrices, period: 14)
let macd = TechnicalIndicators.calculateMACD(data: closePrices)
// Update chart data with calculated indicators
DispatchQueue.main.async {
self.chartData?.sma20 = sma20
self.chartData?.sma50 = sma50
self.chartData?.ema12 = ema12
self.chartData?.ema26 = ema26
self.chartData?.rsi = rsi
self.chartData?.macd = macd
}
}
The code extracts close prices from the candlestick data, then calls each Rust-powered indicator function. The calculations happen on a background thread to avoid blocking the UI, with results dispatched back to the main thread for display. Because Rust handles the heavy lifting, these recalculations complete in just a few milliseconds even with thousands of data points.
Rendering at 60 FPS: The Chart Implementation
The chart rendering demonstrates how to build high-performance visualisations in SwiftUI. Rather than using standard SwiftUI views, which can be slow for complex graphics, the implementation uses the Canvas API for direct, efficient drawing.
The candlestick chart shows the core rendering approach:
struct CandlestickChartView: View {
let chartData: ChartData
let width: CGFloat
let height: CGFloat
var body: some View {
Canvas { context, size in
drawCandles(context: context, size: size)
if chartData.showSMA20 {
drawIndicator(context: context, size: size,
data: chartData.sma20,
color: .blue)
}
// Draw other indicators...
}
.frame(width: width, height: height)
}
private func drawCandles(context: GraphicsContext, size: CGSize) {
let candleWidth = size.width / CGFloat(chartData.candles.count)
for (index, candle) in chartData.candles.enumerated() {
let x = CGFloat(index) * candleWidth
let color = candle.close >= candle.open ?
ChartColors.bullish : ChartColors.bearish
// Draw high-low line
let highY = mapPriceToY(price: candle.high, size: size)
let lowY = mapPriceToY(price: candle.low, size: size)
context.stroke(
Path { path in
path.move(to: CGPoint(x: x + candleWidth/2, y: highY))
path.addLine(to: CGPoint(x: x + candleWidth/2, y: lowY))
},
with: .color(color),
lineWidth: 1
)
// Draw open-close body
let openY = mapPriceToY(price: candle.open, size: size)
let closeY = mapPriceToY(price: candle.close, size: size)
let rect = CGRect(
x: x + candleWidth * 0.2,
y: min(openY, closeY),
width: candleWidth * 0.6,
height: abs(openY - closeY)
)
context.fill(Path(rect), with: .color(color))
}
}
}
The Canvas API provides direct access to the graphics context, allowing fine-grained control over rendering. Each candlestick consists of a vertical line showing the high and low prices, with a thicker body showing the open and close. The colour code makes bullish candles (where close is greater than open) visually distinct from bearish ones.
The coordinate mapping function transforms price values into screen coordinates:
private func mapPriceToY(price: Float, size: CGSize) -> CGFloat {
let priceRange = chartData.maxPrice - chartData.minPrice
let priceOffset = price - chartData.minPrice
let normalizedPosition = CGFloat(priceOffset / priceRange)
return size.height * (1 - normalizedPosition)
}
This function inverts the Y axis because screen coordinates increase downward while prices increase upward. Normalisation ensures that the chart automatically scales to show all the data, regardless of the price range.
Key Learnings and Insights
Building this hybrid application revealed several important insights about combining Rust and Swift. The FFI boundary requires careful attention to memory management, but once properly implemented, it becomes invisible to the rest of the codebase. The key is to encapsulate all unsafe operations in a thin layer that exposes safe, idiomatic Swift interfaces.
The performance benefits of Rust are not theoretical. In real-world usage with live data streaming and constant recalculations, the difference between smooth 60 FPS rendering and janky, stuttering animations comes down to these milliseconds. When you need to recalculate indicators on every price update, having calculations complete in under 2 milliseconds instead of 6–8 milliseconds is the difference between responsive and sluggish.
SwiftUI’s reactive programming model pairs beautifully with high-performance computations. The published properties and Combine framework create a clean separation between data processing and presentation. Heavy calculations can happen on background threads without complicating the view code.
This method of using a static library is different from Android’s dynamic library model, but it works well for iOS. The static libraries get linked directly into the application binary, eliminating the need to bundle separate library files. This simplifies deployment and reduces the risk of version mismatches, though it does increase the overall binary size.
Beyond Performance: Architecture Benefits
The hybrid architecture delivers benefits beyond raw speed. The clear separation between calculation logic (Rust) and presentation logic (Swift) makes the codebase more maintainable. Changes to indicator algorithms don’t require touching any UI code. Similarly, UI redesigns don’t risk breaking calculation logic.
Testing becomes more straightforward with this separation. The Rust code can be tested independently with comprehensive unit tests that run quickly and reliably. The Swift side can use preview providers and UI tests without needing to mock complex calculations.
The architecture also provides flexibility for future enhancements. Adding new indicators means implementing them in Rust, where performance matters most, then exposing them through the FFI layer. The pattern established by existing indicators makes this process straightforward.
The Best of Both Worlds
This project demonstrates that native performance and modern UI frameworks are not mutually exclusive. By leveraging Rust for computational work and Swift for user interfaces, you can build applications that are both blazingly fast and delightful to use.
The approach scales well beyond financial applications. Any iOS app with significant computational requirements, from image processing to machine learning to scientific calculations, could benefit from this hybrid architecture. The key is identifying the performance-critical paths and implementing them in Rust while keeping the user-facing code in Swift.
The tooling and ecosystem support for Rust-Swift interop continue to improve. Building for multiple architectures and platforms becomes more streamlined with each release. For developers willing to work across languages, the reward is applications that deliver exceptional performance without sacrificing developer experience.
The complete source code and build instructions are available in the GitHub repository https://github.com/davthecodercom/Financial-RustAndswift, making it easy to explore this architecture pattern in depth or use it as a foundation for your projects. Whether you’re building financial applications or any other performance-sensitive iOS app, this hybrid approach offers a proven path to achieving both speed and elegance.
Happy coding!
David Cruz
davthecoder.com
Loading comments…