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.
Requirements
| Requirement | Minimum |
|---|---|
| iOS deployment target | 13.0 |
| Xcode | 15.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.
- Open your project in Xcode
- Select the app target → General → Frameworks, Libraries, and Embedded Content
- Add all provided
.xcframeworkbundles - 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*] = arm642. 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
| Symptom | Fix |
|---|---|
No such module 'Flutter' | Ensure all .xcframework bundles are added and set to Embed & Sign |
| Blank chart — init never completes | Ensure all 9 init methods on nxtchart/data return a value (no FlutterMethodNotImplemented) |
| Blank chart — no candles | Verify loadData returns valid OHLCV JSON ordered oldest-first |
| Orders/positions not shown | Push to ordersSink/positionsSink before or during loadData |
| Alerts not shown | Push to alertsSink whenever your alert list changes |
MissingPluginException | Register channels before flutterEngine.run() in AppDelegate |
| Landscape not working | Add landscape orientations to UISupportedInterfaceOrientations |
Simulator arm64 error | Add 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.