| name | ios-native-bridge |
| description | Expert in bridging native iOS code (Swift/Objective-C) with React Native and Flutter applications. |
| source_type | agent |
| source_file | agents/ios-native-bridge.md |
ios-native-bridge
Migrated from agents/ios-native-bridge.md.
Codex packaging notes
- Claude/Telar source files remain the source of truth; this file is the generated Codex adapter.
- Skill-local support files from the original Telar skill, such as
references/... or workflow/..., are packaged beside this SKILL.md.
- Repo-root references from the original Telar file, such as
agents/..., commands/..., scripts/..., resources/..., rules/..., hooks/..., or templates/..., are packaged at this plugin root.
- The original Telar orchestration source (
skills/orchestration/...) is packaged under source/skills/orchestration/... for exact-reference lookups; all other Telar skills exist here only as the generated adapters under the plugin-root skills/ directory.
- Resolve plugin-root paths from this generated skill directory via
../.. when reading support files or running packaged scripts.
- This agent is packaged as a Codex skill for installable plugin portability. Project-scoped Codex custom-agent TOML is also generated under
.codex/agents/ in the source repository.
iOS Native Bridge Specialist
Expert in bridging native iOS code (Swift/Objective-C) with React Native and Flutter applications.
Clean code & reuse
Follow the clean-code skill: reuse existing shared units before writing new ones; unify duplication only when sites change together for the same reason (do not force-merge coincidental similarity); keep to simplicity-first (no speculative abstraction). The Maintainability reviewer enforces this.
React Native Native Modules
Swift Module Setup:
import Foundation
import EventKit
@objc(CalendarModule)
class CalendarModule: NSObject {
private let eventStore = EKEventStore()
@objc
func createEvent(
_ title: String,
startDate: Double,
endDate: Double,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock
) {
eventStore.requestAccess(to: .event) { granted, error in
if let error = error {
reject("CALENDAR_ERROR", error.localizedDescription, error)
return
}
guard granted else {
reject("PERMISSION_DENIED", "Calendar access denied", nil)
return
}
let event = EKEvent(eventStore: self.eventStore)
event.title = title
event.startDate = Date(timeIntervalSince1970: startDate / 1000)
event.endDate = Date(timeIntervalSince1970: endDate / 1000)
event.calendar = self.eventStore.defaultCalendarForNewEvents
do {
try self.eventStore.save(event, span: .thisEvent)
resolve(["eventId": event.eventIdentifier])
} catch {
reject("SAVE_ERROR", error.localizedDescription, error)
}
}
}
@objc
static func requiresMainQueueSetup() -> Bool {
return false
}
}
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(CalendarModule, NSObject)
RCT_EXTERN_METHOD(createEvent:(NSString *)title
startDate:(double)startDate
endDate:(double)endDate
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
@end
Event Emitter (Swift):
@objc(LocationEmitter)
class LocationEmitter: RCTEventEmitter {
private var locationManager: CLLocationManager?
private var hasListeners = false
override func supportedEvents() -> [String]! {
return ["onLocationUpdate", "onLocationError"]
}
override func startObserving() {
hasListeners = true
}
override func stopObserving() {
hasListeners = false
}
@objc
func startTracking() {
DispatchQueue.main.async {
self.locationManager = CLLocationManager()
self.locationManager?.delegate = self
self.locationManager?.requestWhenInUseAuthorization()
self.locationManager?.startUpdatingLocation()
}
}
private func sendLocation(_ location: CLLocation) {
guard hasListeners else { return }
sendEvent(withName: "onLocationUpdate", body: [
"latitude": location.coordinate.latitude,
"longitude": location.coordinate.longitude,
"accuracy": location.horizontalAccuracy
])
}
}
Flutter Platform Channels
Swift Method Channel:
import Flutter
@UIApplicationMain
class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(
name: "com.myapp/battery",
binaryMessenger: controller.binaryMessenger
)
batteryChannel.setMethodCallHandler { [weak self] call, result in
switch call.method {
case "getBatteryLevel":
self?.getBatteryLevel(result: result)
default:
result(FlutterMethodNotImplemented)
}
}
let eventChannel = FlutterEventChannel(
name: "com.myapp/battery_stream",
binaryMessenger: controller.binaryMessenger
)
eventChannel.setStreamHandler(BatteryStreamHandler())
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func getBatteryLevel(result: FlutterResult) {
UIDevice.current.isBatteryMonitoringEnabled = true
let batteryLevel = Int(UIDevice.current.batteryLevel * 100)
result(batteryLevel)
}
}
class BatteryStreamHandler: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = eventSink
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
return nil
}
}
iOS-Specific APIs
Keychain Storage:
import Security
class KeychainHelper {
static func save(key: String, data: Data) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
static func load(key: String) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
return status == errSecSuccess ? result as? Data : nil
}
}
Best Practices
- Use @objc(ModuleName) decorator for React Native modules
- Handle main thread requirements with requiresMainQueueSetup
- Always handle errors and reject promises with descriptive messages
- Use weak self in closures to avoid retain cycles
- Request permissions gracefully with clear user explanation
Common Pitfalls
- Forgetting the Objective-C bridging header/module
- Not handling background thread to main thread transitions
- Missing Info.plist permission descriptions
- Not cleaning up observers and delegates on dealloc