connect_swift_integration.md
March 18, 2026 · View on GitHub
Integrating NoritoBridgeKit in an Xcode iOS Project
This guide shows how to integrate the Rust Norito bridge (XCFramework) and the Swift wrappers into an iOS app, then exchange Iroha Connect frames over WebSocket using the same Norito codecs as the Rust host.
Prerequisites
- A NoritoBridge.xcframework zip (built by CI workflow) and the Swift helper
NoritoBridgeKit.swift(copy the version underexamples/ios/NoritoDemo/Sourcesif you are not consuming the demo project directly). - Xcode 15+, iOS 13+ target.
Option A: Swift Package Manager (recommended)
- Publish a binary SPM using the
Package.swift.templateincrates/connect_norito_bridge/(fill URL and checksum from CI). - In Xcode: File → Add Packages… → Enter the SPM repo URL → Add the
NoritoBridgeproduct to your target. - Add
NoritoBridgeKit.swiftto your app target (drag into your project, ensure “Copy if needed” is ticked).
Option B: CocoaPods
- Create a Podspec from
NoritoBridge.podspec.template(fill thes.sourcezip URL). pod trunk push NoritoBridge.podspec.- In your Podfile:
pod 'NoritoBridge'→pod install. - Add
NoritoBridgeKit.swiftto your app target.
Imports
import Foundation
import CryptoKit // ChaChaPoly / HKDF
import IrohaSwift // ConnectClient / ConnectSession / ConnectCrypto
import NoritoBridge // Clang module from the XCFramework
// Ensure NoritoBridgeKit.swift is part of the target
Bootstrapping a Connect session
ConnectClient handles the WebSocket, while ConnectSession orchestrates control
frames and ciphertext envelopes. The snippet below shows how a dApp would open a session,
derive Connect keys, and wait for an approval response.
let connectURL = URL(string: "wss://node.example/v1/connect/ws?sid=\(sidB64)&role=app")!
var connectRequest = URLRequest(url: connectURL)
connectRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let connectClient = ConnectClient(request: connectRequest)
let sessionID = Data(base64Encoded: sidB64)!
Task {
await connectClient.start()
let keyPair = try ConnectCrypto.generateKeyPair()
var connectSession = ConnectSession(sessionID: sessionID, client: connectClient)
let open = ConnectOpen(
appPublicKey: keyPair.publicKey,
appMetadata: ConnectAppMetadata(name: "Demo dApp", iconURL: nil, description: nil),
constraints: ConnectConstraints(chainID: "00000000-0000-0000-0000-000000000000"),
permissions: ConnectPermissions(methods: ["sign"], events: [])
)
try await connectSession.sendOpen(open: open)
if case .approve(let approval) = try await connectSession.nextControlFrame() {
let directionKeys = try ConnectCrypto.deriveDirectionKeys(localPrivateKey: keyPair.privateKey,
peerPublicKey: approval.walletPublicKey,
sessionID: sessionID)
connectSession.setDirectionKeys(directionKeys)
// Ready to decrypt ciphertext envelopes
let firstEnvelope = try await connectSession.nextEnvelope()
print("payload:", firstEnvelope.payload)
}
}
Sending ciphertext frames (sign requests, etc.)
When the dApp needs to request a signature it uses the Norito bridge helpers to encode
an envelope, encrypts the payload with ChaChaPoly, and wraps it in a ConnectFrame.
let bridge = NoritoBridgeKit()
let seq = nextSequence()
let txBytes = Data([0x01, 0x02, 0x03])
let envelope = try bridge.encodeEnvelopeSignRequestTx(sequence: seq, txBytes: txBytes)
let aad = ConnectAEAD.header(sessionID: sessionID, direction: .appToWallet, sequence: seq)
let nonce = ConnectAEAD.nonce(sequence: seq)
let ciphertext = try ChaChaPoly.seal(envelope,
using: SymmetricKey(data: directionKeys.appToWallet),
nonce: nonce,
authenticating: aad).combined
let frame = ConnectFrame(sessionID: sessionID,
direction: .appToWallet,
sequence: seq,
kind: .ciphertext(ConnectCiphertext(payload: ciphertext)))
try await connectClient.send(frame: frame)
ConnectAEAD.header / ConnectAEAD.nonce are convenience helpers (see the snippet in
docs/connect_swift_ios.md) built from the shared connect:v1 header definition. They
are easy to inline if you prefer not to add another utility:
enum ConnectAEAD {
static func header(sessionID: Data, direction: ConnectDirection, sequence: UInt64) -> Data {
var buffer = Data()
buffer.append("connect:v1".data(using: .utf8)!)
buffer.append(sessionID)
buffer.append(direction == .appToWallet ? 0 : 1)
var seq = sequence.littleEndian
withUnsafeBytes(of: &seq) { buffer.append(contentsOf: \$0) }
buffer.append(1) // Ciphertext kind
return buffer
}
static func nonce(sequence: UInt64) -> ChaChaPoly.Nonce {
var bytes = Data(count: 12)
var seq = sequence.littleEndian
bytes.replaceSubrange(4..<12, with: withUnsafeBytes(of: &seq) { Data(\$0) })
return try! ChaChaPoly.Nonce(data: bytes)
}
}
Receiving / decrypting frames
ConnectSession already exposes nextEnvelope(), which decrypts payloads when direction
keys are configured. If you need manual access (for example to match an existing decoder
pipeline), you can call the lower-level helper:
func decryptFrame(_ frame: ConnectFrame,
symmetricKey: SymmetricKey,
sessionID: Data) throws -> ConnectEnvelope {
guard case .ciphertext(let payload) = frame.kind else {
throw ConnectEnvelopeError.unsupportedFrameKind
}
let aad = ConnectAEAD.header(sessionID: sessionID,
direction: frame.direction,
sequence: frame.sequence)
let nonce = ConnectAEAD.nonce(sequence: frame.sequence)
let box = try ChaChaPoly.SealedBox(combined: payload.payload)
let plaintext = try ChaChaPoly.open(box, using: symmetricKey, authenticating: aad)
return try ConnectEnvelope.decode(jsonData: plaintext)
}
NoritoBridgeKit also exposes helpers such as decodeCiphertextFrame, decodeEnvelopeJson,
and decodeSignResultAlgorithm for debugging or interoperability testing. For production
apps, rely on ConnectSession and ConnectEnvelope so behaviour matches the Rust and
Android SDKs exactly.
CI validation
-
Before publishing updated bridge artifacts or pushing Connect integrations, run:
make swift-ciThe target validates fixture parity, checks the dashboard feeds, and renders the CLI summaries locally. In Buildkite the same workflow depends on metadata keys such as
ci/xcframework-smoke:<lane>:device_tag; confirm the metadata is present after editing pipelines or agent tags so dashboards can attribute results to the correct simulator or StrongBox lane. -
If the command fails, follow the parity playbook (
docs/source/swift_parity_triage.md) and inspect the renderedmobile_cioutput to identify which lane requires regeneration or incident follow-up before retrying.