Skip to content
iOSiOS

Building High-Performance iOS Financial Charts with Rust and SwiftUIGráficos financieros de alto rendimiento en iOS con Rust y SwiftUI

ByPor David Cruz Anaya, Senior Mobile & Applied AI Engineer (Android, Kotlin Multiplatform, product AI)Ingeniero sénior de móvil e IA aplicada (Android, Kotlin Multiplatform, IA de producto)

Updated 11 July 2026Actualizado el 11 de julio de 20266 min read6 min de lectura
AndroidMobile DevelopmentSwiftiOSRust
Building High-Performance iOS Financial Charts with Rust and SwiftUI
Share:Compartir:

Rust and SwiftUI architecture diagram for iOS financial charting app

You can get native performance and a modern declarative UI at the same time by splitting the work: Rust computes the technical indicators (SMA, EMA, RSI, MACD) and SwiftUI renders the chart. The two sides talk over a thin C FFI bridge, so the heavy math runs with no garbage collection and SwiftUI keeps its reactive data flow. In this post I walk through the hybrid architecture behind the Financial-RustAndSwift project, the measured speedups over pure Swift, and how the pieces fit together to keep the chart at 60 FPS.

Architecture

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.

How Much Faster Is Rust Than Pure Swift?

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 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 Relative Strength Index drops from 5.4 milliseconds to 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.

The Rust indicators

The indicators live in indicators.rs. The simple moving average:

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
}

Pre-allocating the result vector with the exact capacity needed eliminates reallocation overhead. The slice iterators are what the compiler can vectorise, so the sum processes multiple values per instruction. The hot loop does not allocate.

The exponential moving average 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
}

Each EMA step depends on the previous result. The loop is simple arithmetic, so the compiler can emit tight machine code. The ownership checks stay at compile time.

The FFI bridge

The bridge is a C FFI in lib.rs.

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,
    }
}

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

On the Swift side:

@_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
}

The Swift wrapper keeps the unsafe pointer work inside the function. 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

The application connects to Binance’s WebSocket API to receive real-time trade data for Bitcoin against USDT.

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.

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.

The data manager

FinancialDataManager joins the WebSocket feed, the Rust calculations, and the UI state.

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. The recalculations finish in a few milliseconds, even with thousands of points.

How Do You Render Charts at 60 FPS in SwiftUI?

The chart draws with the Canvas API.

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.

Static linking

iOS links these Rust libraries statically, unlike Android’s dynamic library model. The static libraries are linked into the application binary, so you do not ship a separate library file. That removes a version-mismatch risk and increases the binary size.

The Rust indicators can be unit-tested on their own. The Swift side can use preview providers and UI tests without mocking the math.

The source and the build instructions are in the Financial-RustAndSwift repository.

Happy coding!

David Cruz
davthecoder.com


Diagrama de arquitectura de Rust y SwiftUI para una aplicación de gráficos financieros en iOS

Puedes tener rendimiento nativo y una interfaz declarativa moderna a la vez si partes el trabajo: Rust calcula los indicadores técnicos (SMA, EMA, RSI, MACD) y SwiftUI pinta el gráfico. Los dos lados hablan a través de un puente C FFI fino, así que la matemática pesada corre sin recolección de basura y SwiftUI conserva su flujo de datos reactivo. En este artículo recorro la arquitectura híbrida que hay detrás del proyecto Financial-RustAndSwift, las aceleraciones medidas frente a Swift puro y cómo encajan las piezas para mantener el gráfico a 60 FPS.

Arquitectura

La arquitectura crea una separación clara de responsabilidades. En la capa de abajo, las implementaciones en Rust de los indicadores técnicos corren sin sobrecoste de recolección de basura y se benefician de la autovectorización SIMD. Por encima, un puente C FFI fino conecta el código de Rust con Swift. Por último, SwiftUI gestiona la capa de presentación con su flujo de datos reactivo y su sintaxis declarativa.

¿Cuánto más rápido es Rust que Swift puro?

Al procesar mil velas, las implementaciones en Rust superan de forma constante a las alternativas en Swift puro por factores de entre dos y cuatro y medio.

El cálculo de la media móvil simple tarda 0,8 milisegundos con Rust, frente a 2,1 milisegundos en Swift puro, y consigue una aceleración de 2,6x. La media móvil exponencial termina en 0,6 milisegundos frente a 1,8 milisegundos, una mejora de 3,0x. El índice de fuerza relativa baja de 5,4 milisegundos a 1,2 milisegundos, y consigue una aceleración de 4,5x. El indicador MACD, que combina varias medias móviles, ve una mejora similar de 4,5x, y tarda 1,5 milisegundos frente a 6,8 milisegundos en Swift puro.

Estas mejoras salen de varios factores que trabajan juntos. La ausencia de recolección de basura en Rust elimina las pausas impredecibles durante los cálculos. La optimización agresiva del compilador, incluida la vectorización SIMD, permite que los cálculos procesen varios puntos de datos a la vez. Una disposición eficiente de la memoria y un buen uso de la caché reducen el tiempo esperando datos de la RAM.

Los indicadores en Rust

Los indicadores viven en indicators.rs. La media móvil simple:

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
}

Reservar por adelantado el vector de resultado con la capacidad exacta que hace falta elimina el sobrecoste de reasignar. Los iteradores de los slices son lo que el compilador puede vectorizar, así que la suma procesa varios valores por instrucción. El bucle caliente no asigna.

La media móvil exponencial da más peso a los precios recientes:

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
}

Cada paso de la EMA depende del resultado anterior. El bucle es aritmética simple, así que el compilador puede emitir código máquina ajustado. Las comprobaciones de propiedad se quedan en tiempo de compilación.

El puente FFI

El puente es una FFI de C en lib.rs.

El puente usa una estructura propia para pasar arrays con seguridad a través de la frontera FFI:

#[repr(C)]
pub struct FloatArray {
    pub data: *mut f32,
    pub len: usize,
    pub capacity: usize,
}

La anotación repr(C) garantiza que Rust usa la misma disposición de memoria que C, y así la estructura es compatible con Swift. La estructura contiene un puntero a los datos, junto con la longitud y la capacidad que Swift necesita para interpretar bien el array.

Cada función de indicador se envuelve con código seguro para FFI que se encarga del marshalling entre Swift y 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,
    }
}

El atributo no_mangle impide que Rust cambie el nombre de la función, y así Swift puede encontrarla. La declaración unsafe extern "C" marca esta función como compatible con C y llamable a través de la frontera FFI. Dentro, el código convierte entre punteros crudos y los tipos de slice seguros de Rust, hace el cálculo y luego devuelve la propiedad del resultado a Swift olvidando el slice en caja después de extraer su puntero.

En el lado Swift:

@_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
}

El wrapper de Swift deja el trabajo inseguro con punteros dentro de la función. El método withUnsafeBufferPointer da acceso directo y temporal a la memoria del array, y evita cualquier copia. Después de recuperar el resultado, el código crea un array nuevo de Swift a partir de la memoria reservada por Rust y luego libera de forma explícita la memoria de Rust para evitar fugas.

Datos en tiempo real: la integración con WebSocket

La aplicación se conecta a la API WebSocket de Binance para recibir datos de operaciones en tiempo real de Bitcoin contra USDT.

El servicio mantiene su estado con propiedades publicadas que disparan solas las actualizaciones de la interfaz:

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()
    }
}

El proceso de conexión usa el soporte nativo de WebSocket de URLSession, y evita dependencias externas. El servicio se conecta al endpoint de flujo de operaciones de Binance, que entrega los datos de cada operación en el momento en que ocurre en el mercado. Cada operación incluye el precio, la cantidad, la marca de tiempo y si fue una orden de compra o de venta.

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
        }
    }
}

La naturaleza recursiva de receiveMessage crea un bucle continuo que procesa los mensajes según llegan. Este patrón garantiza que la conexión sigue activa y responde, sin bloquear el hilo principal. Cuando llega un mensaje, el código decodifica el JSON en un modelo TradeData estructurado que el resto de la aplicación puede usar.

El gestor de datos

FinancialDataManager junta el flujo de WebSocket, los cálculos de Rust y el estado de la interfaz.

El gestor mantiene todo el estado que necesita la interfaz:

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()
    }
}

Las propiedades publicadas disparan solas las actualizaciones de la interfaz cada vez que cambian, siguiendo el modelo de programación reactiva de SwiftUI. El framework Combine se encarga de las suscripciones al servicio de WebSocket, y crea un flujo de datos limpio desde los eventos de red hasta las actualizaciones de la interfaz.

Cuando llegan datos nuevos de operaciones, el gestor los procesa y recalcula todos los indicadores:

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
    }
}

El código extrae los precios de cierre de los datos de velas y luego llama a cada función de indicador impulsada por Rust. Los cálculos ocurren en un hilo en segundo plano para no bloquear la interfaz, y los resultados vuelven al hilo principal para mostrarlos. Los recálculos terminan en unos milisegundos, incluso con miles de puntos.

¿Cómo renderizas gráficos a 60 FPS en SwiftUI?

El gráfico se dibuja con la API Canvas.

El gráfico de velas muestra el enfoque central de renderizado:

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))
        }
    }
}

La API Canvas da acceso directo al contexto gráfico, y permite un control fino del renderizado. Cada vela consiste en una línea vertical que muestra el máximo y el mínimo, con un cuerpo más grueso que muestra la apertura y el cierre. El código de color hace que las velas alcistas (donde el cierre es mayor que la apertura) se distingan a simple vista de las bajistas.

La función de mapeo de coordenadas transforma los valores de precio en coordenadas de pantalla:

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)
}

Esta función invierte el eje Y porque las coordenadas de pantalla crecen hacia abajo mientras que los precios crecen hacia arriba. La normalización garantiza que el gráfico escala solo para mostrar todos los datos, sea cual sea el rango de precios.

Enlazado estático

En iOS estas bibliotecas de Rust se enlazan de forma estática, a diferencia del modelo de biblioteca dinámica de Android. Las bibliotecas estáticas quedan dentro del binario de la aplicación, así que no distribuyes un archivo de biblioteca aparte. Eso quita un riesgo de desajuste de versión y aumenta el tamaño del binario.

Los indicadores de Rust se pueden probar con tests unitarios por su cuenta. El lado Swift puede usar proveedores de previews y tests de interfaz sin simular las matemáticas.

El código y las instrucciones de compilación están en el repositorio Financial-RustAndSwift.

¡Feliz programación!

David Cruz
davthecoder.com

Share:Compartir:

CommentsComentarios

Loading comments…Cargando comentarios…