NeoCharts
Quick Start

iOS Integration

Embed NeoCharts into a native iOS app via platform channels.

Embed NeoCharts into an existing native iOS app (Swift / ObjC).

The SDK is distributed as pre-built binary frameworks (.xcframework bundles) — your native app only implements the platform channel handlers that supply data and receive trading callbacks.

This is the complete integration — the full channel set, alerts, OI analysis, and scalper mode. Coming from Quick Start? Steps 1–3 below are the same ones you just ran.

iOS integration architecture


Requirements

RequirementMinimum
iOS deployment target13.0
Xcode15.0

1. Embed the frameworks in Xcode

Access

NeoCharts is distributed directly to licensed partners, not published to CocoaPods/SPM. Your IOURING contact hands you the .xcframework bundles directly.

  1. Open your project in Xcode
  2. Select the app target → GeneralFrameworks, Libraries, and Embedded Content
  3. Add all provided .xcframework bundles
  4. Set each to Embed & Sign

Simulator builds

If you encounter architecture errors on simulator builds, add to your target's build settings:

EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64

2. Register channel handlers

Register all handlers in AppDelegate before flutterEngine.run().

import UIKit
import Flutter
import FlutterPluginRegistrant

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    // EventChannel sinks — whichever view is active holds the live reference
    var marketDataSink: FlutterEventSink?
    var ordersSink:     FlutterEventSink?
    var ocoOrdersSink:  FlutterEventSink?
    var positionsSink:  FlutterEventSink?
    var tradeEventSink: FlutterEventSink?
    var alertsSink:     FlutterEventSink?

    private let engineGroup = FlutterEngineGroup(name: "nxt_chart", project: nil)
    lazy var chartEngine   = engineGroup.makeEngine(withEntrypoint: nil,           libraryURI: nil)
    lazy var scalperEngine = engineGroup.makeEngine(withEntrypoint: "scalperMain", libraryURI: nil)

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {

        chartEngine.run()
        scalperEngine.run()
        GeneratedPluginRegistrant.register(with: chartEngine)
        GeneratedPluginRegistrant.register(with: scalperEngine)
        registerChannels(messenger: chartEngine.binaryMessenger)
        registerChannels(messenger: scalperEngine.binaryMessenger)
        return true
    }

    private func registerChannels(messenger: FlutterBinaryMessenger) {

        // ── nxtchart/data ─────────────────────────────────────────────────────
        FlutterMethodChannel(name: "nxtchart/data", binaryMessenger: messenger)
            .setMethodCallHandler { [weak self] call, result in
                guard let self else { return }
                switch call.method {

                // ── Init metadata (called once at startup) ───────────────────
                case "symbolInfo":           result(self.symbolInfoJson())
                case "underlyingSymbolInfo": result(self.underlyingSymbolInfoJson())
                case "optionSymbols":        result(self.optionSymbolsJson())
                case "futureSymbols":        result(self.futureSymbolsJson())
                // Populating this hides the Volume indicator for symbols in
                // the list (spot indices have no traded volume).
                case "indexSymbols":         result(self.indexSymbolsJson())
                case "marketTiming":         result(self.marketTimingJson())
                case "hasOCO":               result(true)
                case "storageKey":           result("user_\(self.userId())")
                // Called each time the scalper right panel initializes, not
                // cached at startup — safe to delay until background data
                // (e.g. option chain) finishes loading.
                case "atmSymbols":           result(self.atmSymbolsJson())
                case "chartTopOptions":      self.fetchTopOptions { json in result(json) }

                // ── Historical data ──────────────────────────────────────────
                case "loadData":
                    let args            = call.arguments as! [String: Any]
                    let from            = args["from"] as! Int
                    let to              = args["to"] as! Int
                    let intervalSeconds = args["intervalSeconds"] as! Int
                    let requiredBars    = args["requiredBars"] as! Int
                    self.fetchOHLCV(from: from, to: to,
                                    intervalSeconds: intervalSeconds,
                                    requiredBars: requiredBars) { json in
                        result(json)
                    }

                // ── Option chain & OI analysis ────────────────────────────────
                case "fetchOptionDetails":
                    let underlyingSymbolId = (call.arguments as! [String: Any])["underlyingSymbolId"] as! String
                    self.fetchOptionDetails(underlyingSymbolId: underlyingSymbolId) { json in result(json) }

                case "fetchOIAnalysis":
                    let args = call.arguments as! [String: Any]
                    self.fetchOIAnalysis(underlyingSymbolId: args["underlyingSymbolId"] as! String,
                                          expiry: args["expiry"] as! String,
                                          timeFrom: args["timeFrom"] as! Int,
                                          timeTo: args["timeTo"] as! Int) { json in result(json) }

                case "fetchOIChange":
                    let args = call.arguments as! [String: Any]
                    self.fetchOIChange(underlyingSymbolId: args["underlyingSymbolId"] as! String,
                                        expiries: args["expiries"] as! [String],
                                        timeFrom: args["timeFrom"] as! Int,
                                        timeTo: args["timeTo"] as! Int) { json in result(json) }

                case "fetchOI":
                    let args = call.arguments as! [String: Any]
                    self.fetchOI(underlyingSymbolId: args["underlyingSymbolId"] as! String,
                                 expiries: args["expiries"] as! [String]) { json in result(json) }

                case "fetchPcrIntraday":         self.fetchPcrIntraday { json in result(json) }
                case "fetchAtmStraddleIntraday": self.fetchAtmStraddleIntraday { json in result(json) }
                case "fetchAtmIvIntraday":       self.fetchAtmIvIntraday { json in result(json) }

                // ── Alerts ─────────────────────────────────────────────────────
                case "modifyAlert":
                    self.yourBroker.modifyAlert(params: call.arguments as! String)
                    result(nil)

                case "createAlert":
                    self.yourBroker.createAlert(params: call.arguments as! String)
                    result(nil)

                case "deleteAlert":
                    let alertId = (call.arguments as! [String: Any])["alertId"] as! String
                    self.yourBroker.deleteAlert(alertId: alertId)
                    result(nil)

                // ── Order actions (params arrive as a raw JSON string —
                //    see Reference for each method's exact shape) ────────────────
                case "placeOrder":
                    let p = self.jsonObject(from: call.arguments)
                    self.yourBroker.placeOrder(
                        symID: p["symID"] as! String, price: p["price"] as! Double,
                        orderType: p["orderType"] as! String,
                        orderAction: p["orderAction"] as! String, qty: p["qty"] as! Int)
                    result(nil)

                case "modifyOrder":
                    let p = self.jsonObject(from: call.arguments)
                    self.yourBroker.modifyOrder(
                        orderID: p["orderID"] as! String, symID: p["symID"] as! String,
                        price: p["price"] as! Double, orderType: p["orderType"] as! String,
                        orderAction: p["orderAction"] as! String, qty: p["qty"] as! Int)
                    result(nil)

                case "cancelOrder":
                    let orderID = (call.arguments as! [String: Any])["orderID"] as! String
                    self.yourBroker.cancelOrder(orderID: orderID)
                    result(nil)

                case "placeOCOOrder":
                    self.yourBroker.placeOCOOrder(params: call.arguments as! String)
                    result(nil)

                case "modifyOCOOrder":
                    self.yourBroker.modifyOCOOrder(params: call.arguments as! String)
                    result(nil)

                case "cancelOCOOrder":
                    let groupId = (call.arguments as! [String: Any])["groupId"] as! String
                    self.yourBroker.cancelOCOOrder(groupId: groupId)
                    result(nil)

                case "groupAdjustOrders":
                    self.yourBroker.groupAdjustOrders(params: call.arguments as! String)
                    result(nil)

                default:
                    result(FlutterMethodNotImplemented)
                }
            }

        // ── nxtchart/marketData ───────────────────────────────────────────────
        FlutterEventChannel(name: "nxtchart/marketData", binaryMessenger: messenger)
            .setStreamHandler(SinkHandler { [weak self] args, sink in
                self?.marketDataSink = sink
                if let sink, let symbols = args as? String {
                    self?.yourTickSource.subscribe(symbols) { tickJson in sink(tickJson) }
                }
            })

        // ── nxtchart/symbolSearch ─────────────────────────────────────────────
        FlutterEventChannel(name: "nxtchart/symbolSearch", binaryMessenger: messenger)
            .setStreamHandler(SinkHandler { [weak self] args, sink in
                // The chart takes only the first emission per query — no
                // need to keep pushing after the initial result.
                guard let sink, let query = args as? String else { return }
                self?.yourApi.searchSymbols(query) { json in sink(json) }
            })

        // ── nxtchart/orders ───────────────────────────────────────────────────
        FlutterEventChannel(name: "nxtchart/orders", binaryMessenger: messenger)
            .setStreamHandler(SinkHandler { [weak self] _, sink in self?.ordersSink = sink })

        // ── nxtchart/ocoOrders ────────────────────────────────────────────────
        FlutterEventChannel(name: "nxtchart/ocoOrders", binaryMessenger: messenger)
            .setStreamHandler(SinkHandler { [weak self] _, sink in self?.ocoOrdersSink = sink })

        // ── nxtchart/positions ────────────────────────────────────────────────
        FlutterEventChannel(name: "nxtchart/positions", binaryMessenger: messenger)
            .setStreamHandler(SinkHandler { [weak self] _, sink in self?.positionsSink = sink })

        // ── nxtchart/tradeEvents ──────────────────────────────────────────────
        FlutterEventChannel(name: "nxtchart/tradeEvents", binaryMessenger: messenger)
            .setStreamHandler(SinkHandler { [weak self] _, sink in self?.tradeEventSink = sink })

        // ── nxtchart/alerts ───────────────────────────────────────────────────
        FlutterEventChannel(name: "nxtchart/alerts", binaryMessenger: messenger)
            .setStreamHandler(SinkHandler { [weak self] _, sink in self?.alertsSink = sink })
    }

    // Order/modify params arrive as a raw JSON string, not a dictionary.
    private func jsonObject(from arguments: Any?) -> [String: Any] {
        guard let json = arguments as? String,
              let data = json.data(using: .utf8),
              let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
        else { return [:] }
        return obj
    }
}

// Minimal reusable stream handler
class SinkHandler: NSObject, FlutterStreamHandler {
    private let handler: (Any?, FlutterEventSink?) -> Void
    init(_ handler: @escaping (Any?, FlutterEventSink?) -> Void) { self.handler = handler }
    func onListen(withArguments args: Any?, eventSink sink: @escaping FlutterEventSink) -> FlutterError? {
        handler(args, sink); return nil
    }
    func onCancel(withArguments args: Any?) -> FlutterError? {
        handler(args, nil); return nil
    }
}

3. Launching the chart

import SwiftUI
import Flutter

private func appDelegate() -> AppDelegate {
    UIApplication.shared.delegate as! AppDelegate
}

struct ChartView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> FlutterViewController {
        FlutterViewController(engine: appDelegate().chartEngine, nibName: nil, bundle: nil)
    }
    func updateUIViewController(_ vc: FlutterViewController, context: Context) {}
}

struct ScalperView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> FlutterViewController {
        FlutterViewController(engine: appDelegate().scalperEngine, nibName: nil, bundle: nil)
    }
    func updateUIViewController(_ vc: FlutterViewController, context: Context) {}
}

Run the app — you should see a live chart with real-time data, indicators, drawing tools, and working order/position/alert actions, all driven by the handlers you registered above.


4. Pushing live data

Once the EventChannel sinks are active, push data from wherever your streams emit:

let app = UIApplication.shared.delegate as! AppDelegate

// Live ticks
app.marketDataSink?(
    """[{"symbolId":"RELIANCE","ltp":2411.5,"ltq":100,"chng":11.5,"chngPer":0.48,"ltt":1700001234}]"""
)

// Updated orders list — see Reference for the full Order JSON schema
app.ordersSink?(
    """[{"orderID":"ORD123","type":"limit","orderAction":"buy","productType":"normal","avgPrice":2400.0,"netQty":10,"fillQty":0,"ordTime":"2025-11-20T09:15:00","orderStatus":"open","symbol":{"id":"RELIANCE","...":"see symbolInfo"}}]"""
)

// Updated OCO orders list (pending SL+TP pairs) — see Reference for the full schema
app.ocoOrdersSink?(
    """[{"groupId":"OCO001","symID":"RELIANCE","name":"Reliance Industries","exchange":"NSE","side":"buy","productType":"normal","stopLoss":{"type":"stopLoss","side":"sell","triggerPrice":2350.0,"qty":10,"price":2340.0,"fillQty":0},"target":{"type":"limit","side":"sell","triggerPrice":2450.0,"qty":10,"price":2450.0,"fillQty":0}}]"""
)

// Updated positions list — see Reference for the full Position JSON schema
app.positionsSink?(
    """[{"symID":"RELIANCE","netQty":10,"avgPrice":2400.0,"pnl":115.0,"productType":"normal"}]"""
)

// Action feedback (order/alert outcome notification)
app.tradeEventSink?(
    """{"type":"positive","message":"Order placed successfully"}"""
)

// Updated alerts list — see Reference for the full alert JSON schema
app.alertsSink?(
    """[{"alertId":"357807025062912","symbolInfo":{"id":"RELIANCE","...":"see symbolInfo"},"triggerPrice":"2450","enabled":true,"triggered":false,"createdAt":1769509922000}]"""
)

Tip

Push ordersSink and positionsSink before or during loadData so the chart has order/position state ready on first render.


5. Info.plist

No NeoCharts-specific keys required. For scalper mode, enable landscape orientations:

<key>UISupportedInterfaceOrientations</key>
<array>
  <string>UIInterfaceOrientationPortrait</string>
  <string>UIInterfaceOrientationLandscapeLeft</string>
  <string>UIInterfaceOrientationLandscapeRight</string>
</array>

Channel Reference

See the Native Channel Reference for the full channel list and nxtchart/data method table — identical on Android and iOS, only the calling syntax differs (shown above).


Troubleshooting

SymptomFix
No such module 'Flutter'Ensure all .xcframework bundles are added and set to Embed & Sign
Blank chart — init never completesEnsure all 9 init methods on nxtchart/data return a value (no FlutterMethodNotImplemented)
Blank chart — no candlesVerify loadData returns valid OHLCV JSON ordered oldest-first
Orders/positions not shownPush to ordersSink/positionsSink before or during loadData
Alerts not shownPush to alertsSink whenever your alert list changes
MissingPluginExceptionRegister channels before flutterEngine.run() in AppDelegate
Landscape not workingAdd landscape orientations to UISupportedInterfaceOrientations
Simulator arm64 errorAdd EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64 to build settings

See the API Reference for all channel method details and JSON Schemas for wire formats. See How It Works for the data and trade flow.

On this page