iOS / Apple (Flutter)
Status: beta (0.1.0-beta.1). Flutter and React Native on iOS work end to end for Objective-C APIs from Foundation and UIKit (see below for what has been tested). React Native details: docs/react-native/README.md.
How it works#
Xcode SDK headers ──libclang (Xcode toolchain, dart:ffi)──▶ Native IR (apple) ──▶ Dart over package:objective_c
- Discovery:
xcode-select -p,xcrun --sdk iphonesimulator|iphoneos --show-sdk-path/--show-sdk-version,xcodebuild -version. Nothing is downloaded or copied. - Extraction (
packages/native_api_ios): framework umbrella headers are parsed with libclang's stable C API, loaded fromXcodeDefault.xctoolchain/usr/lib/libclang.dylib. Extracted: classes, protocols, categories (merged into their class), methods and selectors, properties (readonly, class, custom getters such asisHidden), designatedinitfamilies,NS_ENUM/NS_OPTIONSvalues (exact, including 64-bit unsigned), structs, nullability (includingNS_ASSUME_NONNULLand macro-qualified types), and per-platform availability (introduced/deprecated/obsoleted/unavailable). - Planning (
packages/native_api_flutter_ios): decides per member what the target can express; everything skipped carries a diagnostic code (see below). - Emission: one Dart library per framework (
apple/uikit.dart, …), typedobjc_msgSendtrampolines (apple/_msgsend.dart, one per native signature, with the x86_64stret/fpretvariants), runtime helpers (apple/_runtime.dart) and an umbrellaapple.dart. Classes and protocols become Dart extension types overobjc.ObjCObject; types thatpackage:objective_calready provides (NSString,NSArray,NSObject,NSError, …) are referenced, not regenerated.
Configuration#
platform:
ios:
sdk: auto # iphonesimulator (default) or iphoneos
minVersion: "15.0" # the app's deployment target; newer APIs are guarded
frameworks: [Foundation, UIKit] # headers to parse
include: [] # frameworks to generate entirely
classes: [UIDevice, UIView, UIViewController]
entries: [] # dependency-aware roots
depth: 1
output:
dir: lib/src/generated
native-api-bindgen generate ios # from configuration
native-api-bindgen generate ios --class UIKit.UIDevice --depth 0
native-api-bindgen generate ios --framework UIKit # a whole framework
native-api-bindgen inspect ios # SDK version + frameworks with public headers
native-api-bindgen inspect UIDevice#systemName # IR of a generated symbol
native-api-bindgen coverage --target ios # android | ios | ios-rn | ios-swift | all
native-api-bindgen why-skipped UIKit.UIView#-… # reason codes for a skipped member
native-api-bindgen diff ios --from ir-ios-26.4.json --to current # snapshots are written by generate ios
native-api-bindgen update # regenerates every configured Android and iOS target
The app needs objective_c (^9.5) and ffi (^2.1) as dependencies. Android and iOS output can share output.dir: each target keeps its own manifest (.native_api_bindgen_manifest, .native_api_bindgen_manifest_ios), so neither run deletes the other's files.
Mapping#
| Objective-C | Dart |
|---|---|
| class / protocol | extension type X._(objc.ObjCObject) with X.as, X.fromPointer, X.isA (classes) / X.conformsTo (protocols) |
+alloc, +new, -init… |
X.alloc(), X.new$(), x.initWith…(); ARC ownership follows the method family (alloc/new/copy/mutableCopy/init return owned references) |
@property |
getter / setter (class properties are static) |
| selector keywords after the first | named parameters (addItem(item, atIndex: 2)) |
nullable |
T? (nil ⇄ null) |
NS_ENUM / NS_OPTIONS |
abstract final class of int constants; values are int |
struct (CGRect, …) |
ffi.Struct subclass, passed and returned by value |
NSError ** (last parameter) |
hidden; a NO/nil result with an error set throws NativeObjCError(domain, code, description) |
API newer than minVersion |
runtime guard: throws objc.OsVersionError on older iOS |
| deprecated API | @Deprecated with the iOS version |
Threading (main actor)#
The extractor reads Swift concurrency annotations from the headers, the way Swift's importer does (-D__SWIFT_ATTR_SUPPORTS_SENDABLE_DECLS): NS_SWIFT_UI_ACTOR / NS_SWIFT_MAIN_ACTOR on a class, protocol, category or member marks it mainThread; NS_SWIFT_NONISOLATED opts a member out (anyThread). libclang does not expose swift_attr, so the attribute's expansion site in the header is read.
- Flutter: main-actor members document it and check it with
assert(rt.checkMainThread(...)), which throwsNativeThreadingError(E013) off the main thread in debug builds and costs nothing in release. On iOS the root isolate runs on the main thread; background isolates may call nonisolated APIs (e.g. Foundation). - React Native: main-actor members (and
+alloc/+newof main-actor classes) are invoked on the main thread; everything else runs on the calling JS thread. This replaces the earlier rule "all of UIKit on the main thread" (RnObjCOptions.mainThreadModulesremains as an override). - Measured (iOS 27.0 SDK, Foundation + UIKit): 559 of 1,527 types and 6,708 of 12,886 methods are main-actor isolated, 39 methods are nonisolated; React Native dispatches 6,789 of 12,706 member rows to the main thread.
Blocks#
Block parameters are generated as JavaScript/Dart functions; NS_NOESCAPE is read from the headers.
- Flutter (pure Dart, no native code):
NS_NOESCAPEblocks are synchronous closure blocks (any argument and result types; they run while Dart is inside the call). Escaping blocks that returnvoidand take only primitives areNativeCallable.listenerblocks: callable from any thread, delivered asynchronously to the isolate (a completion handler's callable is closed after its first call). Escaping blocks with object arguments or a result would need a native trampoline (they may run later or on another thread) and stayE004. Note: escaping blocks that UIKit calls during the call (e.g.animations:) are delivered after the call returns. Avoidmethod whose last parameter is a completion block also getsfooAsync(...)returning aFuture(a trailingNSErrorargument fails it withNativeObjCError). - React Native: the generated
cpp/generated/NabBlocksObjC.mmhas one block factory per native signature (B<key>;conversion codes). A block invoked on the JS thread calls the function synchronously; elsewhere avoidblock posts the call to the JS thread with its arguments retained, so object arguments are supported. Blocks returning a value are bound only when they areNS_NOESCAPEon members that run on the JS thread (no Promise variant). After regenerating, runpod installwhen files were added (tools/run_rn_ios_tests.shdoes this). - Measured (iOS 27.0 SDK, Foundation + UIKit): Flutter binds 141 block parameters (11 synchronous and 5 listener signatures),
E004exclusions 584 → 394, generated methods 10,900 → 11,092; React Native binds 270 block parameters (16 signatures), bindings 15,401 → 15,653. Both full outputs analyze / type-check and compile cleanly.
Implementing protocols#
- React Native:
Proto.implement({...})creates an object adopting the protocol (a runtime subclass ofNabJSProtocolObjectper protocol set,class_addProtocol); calls arrive throughforwardInvocation:using the protocol's method signatures, so no per-signature code is generated. Methods are optional;respondsToSelector:reflects which ones were given. On the JS thread a method runs synchronously; elsewherevoidmethods are posted to the JS thread, and methods returning a value make the calling thread wait for JS (unless JS is itself waiting for the main thread, then nil/0 is returned and logged). Methods with block orNSError **parameters are not implementable. iOS 27.0 SDK, Foundation + UIKit: 192 protocols, 1,393 methods. - Flutter: not supported yet (
E004).package:objective_c's protocol builder needs per-signature native trampolines, and delegate methods are called later from the run loop, where a pure-Dart FFI callback is not valid.
Swift-only APIs (Layer 2: generated @objc adapters)#
Swift APIs that are not visible to Objective-C are discovered with the official toolchain and wrapped:
xcrun swiftc -emit-module(for a module built from sources) andxcrun swift-symbolgraph-extractproduce the module's public symbol graph (kept in a temporary directory, never written to the project).- For the Objective-C-representable subset (classes and structs;
Int,UInt,Double,Float,Bool,String,Date,Data,URL, other adapted types, optionals of object types),<Module>Adapters.swiftdefines@objc(<Module>_<Type>)classes with explicit selectors that wrap the Swift value (wrapped), plusNativeApiSwiftAdapters.podspec. - A matching Objective-C interface is fed through the same libclang pipeline as the SDK headers, so the Dart bindings (
apple/swiftadapters.dart) come from the regular emitter.
platform:
ios:
swift:
adaptersDir: ios/NativeApiSwiftAdapters # add: pod 'NativeApiSwiftAdapters', :path => 'NativeApiSwiftAdapters'
modules:
- name: NABSwiftFixtures # a pod of the same name provides the module
sources: [../../../fixtures/swift/basic/NABSwiftFixtures.swift]
- name: WeatherKit # SDK module: no sources
types: [Weather]
Selectors: the first argument label is appended to the base name (increment(by:) → incrementBy:, init(start:label:) → initWithStart:label:). APIs newer than minVersion carry @available / API_AVAILABLE.
Bridged beyond plain values:
- Collections:
[T]and[String: T]of bridged or adapted non-optional types cross asNSArray/NSDictionary(adapted elements are wrapped/unwrapped element by element). - Raw-value enums (
IntorString): values cross as the raw value;<Module>_<Enum>has one class property per case (NABSwiftFixtures_Level.high). An invalid raw value passed in traps with a clear message. throws: methods returningVoidor a non-optional object, and throwing initializers, bridge toNSError **(checkLimit:error:→NativeObjCErrorin Dart /NativeObjCErrorin JS). Throwing members returning scalars or optionals are not representable (E002).async:foo(...) async [throws] -> RbecomesfooCompletion:with a completion handler run from aTask((R) -> Void, or(R?, Error?) -> Voidwhen throwing). Flutter gets aFutureform for primitive results (listener block); results with objects or errors are delivered on React Native.
Not adapted (with reasons): closures (E004), generics (E003), tuples, nested or optional-element collections, enums without raw values, protocols, failable initializers, async initializers/properties, members colliding with NSObject (E002), iOS-unavailable APIs (E012). Adapters copy values, so object identity is not preserved across calls; alloc() must be followed by one of the adapter's init… methods.
Measured with Xcode 27.0 (packages/native_api_ios/tool/swift_sdk_report.dart, deployment target 18.0): of the members of three Swift-only SDK modules (enum cases now counted as members), 8 of 111 (TipKit), 73 of 3,466 (Charts) and 183 of 615 (WeatherKit; before this change 89 of 446) are adaptable; the generated adapters for all three type-check with zero errors. The synthetic fixture (fixtures/swift/basic) runs on the iPhone 17 / iOS 26.4 simulator (4 integration tests in examples/flutter/ios_slice).
Not supported yet (with codes)#
| Construct | Code |
|---|---|
| Blocks (completion handlers) | E004 |
| Implementing protocols in Dart (delegates) | E004 (info: protocols are usable for typing and calls) |
Variadic methods, SEL, Class parameters, C arrays, bit-field structs |
E002 |
Raw C pointers (void *) |
exposed as ffi.Pointer (E002 info, partial) |
| Types outside the generation closure | exposed as objc.ObjCObject (E016 info) |
_-prefixed (private) selectors |
E005 |
API_UNAVAILABLE(ios) |
E012 |
| Swift-only APIs outside the adaptable subset | see "Swift-only APIs" above |
Tested (2026-10-05, maintainer machine)#
-
React Native 0.87.1 (
examples/react-native/slice, release build with bundled JS): 19/19 self-tests passing, including the showcase tasks on the same iPhone 17 / iOS 26.4 simulator (tools/run_rn_ios_tests.sh). All of Foundation + UIKit as React Native bindings: 15,401 bound members, TypeScript type-checks with zero errors, C++ tables compile with-Werror. -
Xcode 27.0, iPhoneSimulator SDK 27.0;
examples/flutter/ios_sliceintegration tests: 12/12 passing on an iPhone 17 simulator running iOS 26.4 (flutter test integration_test -d <udid>). The availability-guard test calls an iOS 27.0 API on the iOS 26.4 runtime and expectsOsVersionError. -
Foundation + UIKit generated entirely: 1,527 types, 14,362 bound members, extraction + emission ≈ 0.5–0.6 s,
dart analyze: 0 issues (packages/native_api_flutter_ios/tool/gen_sdk.dart). Only the example's APIs are runtime-tested. -
Fixture IR snapshot, golden Dart output, determinism, traceability and
dart analyze --fatal-infoson goldens:packages/native_api_ios/test,packages/native_api_flutter_ios/test(macOS + Xcode only; skipped elsewhere).
Legal policy: docs/legal/apple.md.
docs/ios/README.md