skip to content

In React Native on iOS, how do you wrap a vendor's Swift-only smart-lock SDK as a Turbo Module, and how do you split the work between Swift and Objective-C++?

level: seniorimportance: should knowfreq 30%

answer

  1. adapter pattern, thin glue
  2. NSObject subclass marked @objc
  3. the Xcode-generated -Swift.h header
  4. bridging header only for app targets
  5. podspec: swift sources, SDK dependency

basics

~20 s

Use an adapter: a Swift class that calls the vendor SDK and exposes an Objective-C-compatible surface, plus a thin Objective-C++ class that adopts the Codegen spec, owns the adapter and forwards each call, because Swift cannot implement the C++-bearing spec directly.

solid answer

~40 s

The generated spec needs a `getTurboModule:` that returns a C++ `std::shared_ptr`, and its header declares C++ types, so Swift cannot adopt it. React Native's documented answer is the **adapter pattern**: a `public` Swift class inheriting from `NSObject`, marked `@objc` or `@objcMembers`, wraps the vendor SDK and exposes Objective-C-friendly methods — Foundation types in, a completion handler with an `NSError` out. The `.mm` class adopts `NativeSmartLockSpec`, creates the adapter in `init`, imports the Xcode-generated `-Swift.h` header, and forwards each spec method, turning completion handlers into `resolve`/`reject`. In an app target you may also need a bridging header; a library does not. Keep waiting inside the SDK's callbacks, never by blocking the queue the call arrived on.

code

swift · 17 lines
swift
import Foundation
import VendorLockKit // hypothetical vendor SDK

@objcMembers public class SmartLockAdapter: NSObject {
  private let client = LockClient()

  public func unlock(lockId: String, completion: @escaping (NSError?) -> Void) {
    client.unlock(id: lockId) { result in
      switch result {
      case .success:
        completion(nil)
      case .failure(let error):
        completion(error as NSError)
      }
    }
  }
}

go deeper

for a junior

Remember the shape: Swift does the work, a thin Objective-C++ class adopts the spec and forwards calls to it.

for a middle

Explain why Swift cannot adopt the spec, what @objcMembers and NSObject give you, and how the -Swift.h header and bridging header connect the two sides.

for a senior

Design the seam for a real SDK: Foundation-only adapter types, stable error codes, promises settled from completion handlers, no blocking of the shared module queue, and a podspec with the vendor dependency.

for a principal

Weigh hand-written Objective-C++ glue against the Expo Modules API or a pure C++ core, considering team skills, the number of wrapped SDKs and long-term upgrade cost.

## Why a Swift SDK cannot simply adopt the spec A **Turbo Module** on iOS must adopt the protocol Codegen generates from the TypeScript spec and implement `getTurboModule:`, which takes a `const facebook::react::ObjCTurboModule::InitParams &` and returns a `std::shared_ptr` to the generated C++ class `NativeSmartLockSpecJSI`. The generated header declares C++ types, and typed object parameters arrive as C++ `JS::` structs. React Native's own Swift guide states the constraint: the core is mainly C++, Swift–C++ interoperability is limited, so **some Objective-C++ glue is unavoidable** — and the goal is to keep it as thin as possible. ## The adapter pattern, layer by layer | Layer | Language | Owns | Knows about React Native? | |---|---|---|---| | Vendor SDK | Swift | Bluetooth, crypto, lock state | No | | `SmartLockAdapter` | Swift | SDK calls, error mapping, Objective-C-friendly signatures | No | | `RCTSmartLock` | Objective-C++ (`.mm`) | Spec conformance, `getTurboModule:`, `+moduleName`, promise plumbing | Yes | - **The adapter** is a `public` class inheriting from `NSObject` and marked `@objcMembers` (or `@objc` on each member) so Objective-C can see it. It speaks only Objective-C-representable types: `String`, `Bool`, `NSNumber`, arrays and dictionaries of those, and `@escaping` completion handlers that report failure as an `NSError?`. - **The glue class** is created by React Native, keeps a strong reference to the adapter, and forwards every spec method. It converts `JS::` structs and blocks into what the adapter accepts, and completion results into `resolve` or `reject`. - **Neither layer leaks into the other**: the adapter is unit-testable without React Native, and the glue has no business logic to test. ## Making the two languages see each other 1. **Swift to Objective-C.** Xcode generates a header exposing the target's `@objc` Swift API, named after the target — for an app, `<AppName>-Swift.h`. The `.mm` imports it; the official guide uses `#import "SampleApp-Swift.h"`. 2. **Objective-C to Swift.** An app target that needs Objective-C headers from Swift uses a **bridging header** set in Build Settings under "Objective-C Bridging Header". React Native's guide notes that a library author does not need this step. 3. **Keep the spec header out of Swift.** It carries C++, which is exactly what the adapter exists to avoid. ## Shipping it as a library: the podspec When the module lives in its own package, its **podspec** declares the sources — including `.swift` — the vendor SDK dependency, and the React Native dependencies through the `install_modules_dependencies` helper: ```ruby require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) Pod::Spec.new do |s| s.name = "react-native-smart-lock" s.version = package["version"] s.summary = package["description"] s.homepage = package["homepage"] s.license = package["license"] s.authors = package["author"] s.platforms = min_supported_versions s.source = { :git => package["repository"], :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,swift}" s.dependency "VendorLockKit" install_modules_dependencies(s) end ``` `install_modules_dependencies(s)` adds the React Native pods and settings a New Architecture module needs, so the podspec does not hard-code them. ## Threading and errors across the seam - **Don't block the calling queue.** Unless a module supplies its own queue, React Native dispatches its asynchronous (`void` and `Promise`) methods onto a **serial queue shared with other modules**. Waiting there on a semaphore for a Bluetooth handshake stalls every module on that queue; pass `resolve` and `reject` into the SDK's completion handler instead. - **Settle every promise once.** Every path in the completion handler must call `resolve` or `reject`; a dropped path leaves the JavaScript `await` hanging. - **Map errors deliberately.** `reject(code, message, error)` takes a string code, a message and the `NSError`; give JavaScript stable codes (`E_LOCK_OUT_OF_RANGE`) rather than raw vendor messages. - **UIKit stays on main.** If the SDK presents a pairing sheet, hop to the main queue before touching UIKit. ## Why the split pays off - **Upgrades touch one layer.** A React Native upgrade that changes the generated glue touches only the `.mm`; a vendor SDK upgrade touches only the adapter. - **Existing Swift is reused.** The official guide points out that the pattern lets a team keep most of an existing Swift implementation when moving a module to the New Architecture, at the cost of declaring each method twice. - **Tests stay native.** The adapter can be exercised with plain XCTest against a fake SDK client, without starting React Native. ## Common mistakes - Making the adapter a Swift `struct` or leaving it `internal` — Objective-C cannot see it. - Exposing vendor model types through the adapter — map them to Foundation types so the `.mm` never imports the SDK. - Adding `RCT_EXPORT_MODULE` back to the glue — registration lives in `codegenConfig.ios`, and the official guide removes the macro.

  • When do you need a bridging header, and when not?
    An app target needs one when its Swift code must see Objective-C headers; you set it under the target's Objective-C Bridging Header build setting. React Native's Swift guide notes the step is not required for a library author shipping the module as a separate package. Either way, the Codegen spec header stays out of it because it contains C++.
  • Why not wait for the SDK with a semaphore inside the Promise method?
    The method runs on the queue React Native dispatched it to — by default a serial queue shared with other modules that supply none — so blocking it for a Bluetooth round trip stalls those modules' calls too. Passing `resolve` and `reject` into the SDK's completion handler keeps the queue free and settles the promise when the lock answers.
  • Which types should cross the adapter boundary, and where does conversion happen?
    Only Objective-C-representable Foundation types: strings, numbers, booleans, arrays and dictionaries, plus completion handlers reporting an `NSError`. The adapter maps vendor model types into those, so the `.mm` never imports the SDK; the `.mm` in turn unpacks any `JS::` structs from typed spec parameters before calling the adapter.

saying these in an interview costs you the question

  • A Swift class can adopt the generated spec protocol if you mark it @objc
  • The adapter can be a Swift struct because Objective-C bridges value types
  • Every Swift module needs a bridging header, including ones shipped as libraries
  • Blocking the method with a semaphore is fine because each module has its own thread
  • The Objective-C++ class should import the vendor SDK and map its types itself