All docs / Mobile + native Carrier SDKs

Swift Carrier SDK

Swift API over the native Carrier library for iOS: identity and signatures, friends, messages with receipts, Express offline delivery, groups, files and sessions. Add it with Swift Package Manager.

Version
6.1.0 · SwiftPM · GitHub release
Platform
iOS · Swift
.package(url: "https://github.com/0xli/Elastos.CarrierClassic.Swift", exact: "6.1.0")

Integration verified on 2026-11-08 with the published Swift package 6.1.0: a new Swift package depending on .package(url: "https://github.com/0xli/Elastos.CarrierClassic.Swift", exact: "6.1.0") resolved (binary checksum verified by SwiftPM) and built the samples on this page for the iOS Simulator with xcodebuild (Xcode 27.0). The Quickstart ran on an iOS Simulator against that framework: the node got an identity, signed and verified a message, and reached ready / Connected on the live network. Only the Quickstart ran against the network; the friend, message, file, session and extension samples were compiled, not run.

The Swift Carrier SDK (module ElastosCarrierSDK) wraps the native Carrier C library for iOS. It gives an app a self-generated Carrier identity, plus friends, presence, text and binary messages with delivery receipts, offline delivery through Express nodes, groups, file transfer, and peer-to-peer sessions with streams. Beagle Chat for iOS uses this SDK (the feat/agentnet-proto-v2 branch) as a prebuilt xcframework.

When to use it

Use it in a native iOS app written in Swift or Objective-C (every public class has an ELA… Objective-C name) that needs to be a full peer on the Decent Network. For voice and video calls on iOS, see the iOS WebRTC SDK, which signals over this SDK. If you write C or C++, or target macOS, Linux or Windows, use the native Carrier SDK directly. For JavaScript, use @decentnetwork/peer.

Add it to your project

Release 6.1.0 ships ElastosCarrierSDK.xcframework as a SwiftPM binary target: a dynamic framework with the native Carrier libraries linked in, built from this repository and the native Carrier SDK v6.1.0. It contains slices for iOS devices (arm64) and the iOS Simulator (arm64 and x86_64), and needs iOS 15 or later.

In Xcode: File › Add Package Dependencies…, enter https://github.com/0xli/Elastos.CarrierClassic.Swift, and choose version 6.1.0. Or in Package.swift:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "CarrierQuickstart",
    platforms: [.iOS(.v15)],
    products: [
        .library(name: "CarrierQuickstart", targets: ["CarrierQuickstart"]),
    ],
    dependencies: [
        .package(url: "https://github.com/0xli/Elastos.CarrierClassic.Swift", exact: "6.1.0"),
    ],
    targets: [
        .target(name: "CarrierQuickstart", dependencies: [
            .product(name: "ElastosCarrierSDK", package: "Elastos.CarrierClassic.Swift"),
        ]),
    ]
)

Then import ElastosCarrierSDK.

CocoaPods

The release also carries a podspec. The pod is not on CocoaPods trunk (the trunk pod named ElastosCarrierSDK, 6.0.4 from 2023, is the older upstream Elastos build, and its download URL returns 404), so point at the podspec directly:

pod 'ElastosCarrierSDK', :podspec => 'https://raw.githubusercontent.com/0xli/Elastos.CarrierClassic.Swift/6.1.0/ElastosCarrierSDK.podspec'

The podspec has not yet been verified with pod install. Swift Package Manager is the verified path.

Build from source

To build the framework yourself (for example to change the native code), build the native libraries, stage them into this repository, and build the framework. These commands were run for the iOS Simulator on Apple silicon on 2026-11-06, at Swift commit 7895768 and native commit e8eb0b0. The release tags are one commit ahead of those and differ only in packaging: 6.1.0 adds Package.swift and the podspec, and native v6.1.0 downloads c-toxcore from a mirror release instead of the old URL, which no longer exists. A fresh clone of native v6.1.0 with no download cache builds for macOS arm64, iOS arm64 and the iOS Simulator (arm64 and x86_64).

1. Build the native libraries from 0xli/Elastos.NET.Carrier.Native.SDK, tag v6.1.0 (or branch feat/agentnet-proto-v2 for the latest). You need Xcode, CMake and brew install autoconf automake libtool shtool pkg-config gettext.

git clone --branch v6.1.0 https://github.com/0xli/Elastos.NET.Carrier.Native.SDK.git native
mkdir -p native/build/ios-sim-arm64 && cd native/build/ios-sim-arm64
cmake -DIOS_PLATFORM=iphonesimulator -DIOS_ARCH=arm64 \
      -DCMAKE_TOOLCHAIN_FILE=../../cmake/iOSToolchain2.cmake \
      -DCMAKE_BUILD_TYPE=Release -DENABLE_APPS=OFF -DENABLE_TESTS=OFF \
      -DENABLE_SHARED=OFF -DENABLE_STATIC=ON ../..
make -j8 && make install

For a device slice, use -DIOS_PLATFORM=iphoneos (and for an Intel simulator, -DIOS_ARCH=x86_64).

2. Stage the 18 static libraries into the Swift repository's NativeDistributions/lib/-iphonesimulator (use -iphoneos for device). The headers in NativeDistributions/include already match the native branch.

git clone --branch 6.1.0 https://github.com/0xli/Elastos.CarrierClassic.Swift.git carrier-swift
B=native/build/ios-sim-arm64; D=carrier-swift/NativeDistributions/lib/-iphonesimulator
mkdir -p "$D"
for l in libcarrier.a libcarriersession.a libcarrierfiletrans.a libcjson.a libcrypto.a \
         libcrystal.a libcurl.a libflatccrt.a libpj.a libpjlib-util.a libpjmedia.a \
         libpjnath.a libsodium.a libsrtp.a libssl.a libtls.a libtoxcore.a libz.a; do
  for p in $B/src/carrier/$l $B/src/session/$l $B/src/filetransfer/$l $B/intermediates/lib/$l; do
    [ -f "$p" ] && cp "$p" "$D/$l" && break
  done
done

3. Build the framework and wrap it in an xcframework. The project's defaults (iOS 9.0, simulator arm64 excluded) do not suit current Xcode, so override them:

cd carrier-swift
xcodebuild -project ElastosCarrierSDK.xcodeproj -target ElastosCarrierSDK \
  -configuration Release -sdk iphonesimulator ARCHS=arm64 EXCLUDED_ARCHS= \
  ONLY_ACTIVE_ARCH=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES SKIP_INSTALL=NO \
  IPHONEOS_DEPLOYMENT_TARGET=15.0 CODE_SIGNING_ALLOWED=NO \
  CONFIGURATION_BUILD_DIR=$PWD/../out/iphonesimulator build
cd ..
xcodebuild -create-xcframework \
  -framework out/iphonesimulator/ElastosCarrierSDK.framework \
  -output ElastosCarrierSDK.xcframework

To ship to devices, build the -sdk iphoneos framework the same way and pass both -framework arguments to -create-xcframework.

4. Use the xcframework from a local Swift package, or drag it into your Xcode target ("Embed & Sign"; it is a dynamic framework):

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "CarrierQuickstart",
    platforms: [.iOS(.v15)],
    products: [
        .library(name: "CarrierQuickstart", targets: ["CarrierQuickstart"]),
    ],
    targets: [
        // The xcframework you built yourself.
        .binaryTarget(name: "ElastosCarrierSDK", path: "ElastosCarrierSDK.xcframework"),
        .target(name: "CarrierQuickstart", dependencies: ["ElastosCarrierSDK"]),
    ]
)

A simulator-only xcframework builds only for arm64, so pass ARCHS=arm64 when you build for generic/platform=iOS Simulator.

Initialize

Create one Carrier with Carrier.createInstance(options:delegate:), then call start(iterateInterval:). Wait for didBecomeReady before you call friend or message APIs. persistentLocation and bootstrapNodes are required: the SDK force-unwraps bootstrapNodes, so leaving it nil crashes. The full lists of public bootstrap and Express nodes are in config/carrier.conf in the native repository.

This is the verified Quickstart:

import Foundation
import ElastosCarrierSDK

/// Receives every Carrier callback. All methods are optional; they run on
/// the SDK's background queue, not the main thread.
public final class QuickstartNode: NSObject, CarrierDelegate {
    public let ready = DispatchSemaphore(value: 0)

    public func connectionStatusDidChange(_ carrier: Carrier,
                                          _ newStatus: CarrierConnectionStatus) {
        print("connection: \(newStatus)")
    }

    public func didBecomeReady(_ carrier: Carrier) {
        print("ready")
        ready.signal()
    }

    public func didReceiveFriendRequest(_ carrier: Carrier, _ userId: String,
                                        _ userInfo: CarrierUserInfo, _ hello: String) {
        print("friend request from \(userId): \(hello)")
        try? carrier.acceptFriend(with: userId)
    }

    public func didReceiveFriendMessage(_ carrier: Carrier, _ from: String, _ data: Data,
                                        _ timestamp: Date, _ isOffline: Bool) {
        print("message from \(from) (offline: \(isOffline)): \(String(decoding: data, as: UTF8.self))")
    }
}

public func makeOptions(dataDir: String) -> CarrierOptions {
    let options = CarrierOptions()
    options.persistentLocation = dataDir        // identity + friend list live here
    options.udpEnabled = true
    options.loglevel = .Warning

    // Bootstrap and Express nodes: config/carrier.conf in the native SDK repo.
    let bootstrap = BootstrapNode()
    bootstrap.ipv4 = "13.58.208.50"
    bootstrap.port = "33445"
    bootstrap.publicKey = "89vny8MrKdDKs7Uta9RdVmspPjnRMdwMmaiEW27pZ7gh"
    options.bootstrapNodes = [bootstrap]        // required: must not be nil

    let express = ExpressNode()
    express.ipv4 = "lens.beagle.chat"
    express.port = "443"
    express.publicKey = "ECbs4GxwGzxGerNkmqDJFibEmevu8jAXqAZtikccvD95"
    options.expressNodes = [express]            // offline (Express) delivery
    return options
}

public func runQuickstart(dataDir: String, waitSeconds: Int = 60) throws {
    print("Carrier version: \(Carrier.getVersion())")

    let delegate = QuickstartNode()
    let carrier = try Carrier.createInstance(options: makeOptions(dataDir: dataDir),
                                             delegate: delegate)
    print("userId:  \(carrier.getUserId())")
    print("address: \(carrier.getAddress())")
    print("address valid: \(Carrier.isValidAddress(carrier.getAddress()))")

    // Identity signatures (XEdDSA): anyone holding the userId can verify.
    let message = Data("hello decent".utf8)
    let signature = try carrier.identitySign(message)
    let ok = Carrier.identityVerify(userId: carrier.getUserId(),
                                    message: message, signature: signature)
    print("signature: \(signature.count) bytes, verifies: \(ok)")

    try carrier.setClientInfo(CarrierClientInfo(protoVersion: 1, platform: "ios",
                                                osVersion: "18", appVersion: "0.1"))

    try carrier.start(iterateInterval: 1000)
    if delegate.ready.wait(timeout: .now() + .seconds(waitSeconds)) == .success {
        print("friends: \(try carrier.getFriends().count)")
    } else {
        print("not ready after \(waitSeconds) s")
    }
    carrier.kill()
    print("killed")
}

Verified output (built from source as above, iOS 27.0 simulator, 2026-11-06). The I/Carrier log lines are trimmed. A second run with the same data directory printed the same userId.

Carrier version: 52master
userId:  BZb6K3TqEhpaMXsNZC5f6R9nUjk1caAkg8iGP7TmJidA
address: QDS1fGKx48cgsbMtEbJuHAwYXVkFC3sFtWKgXSgsf5btxqL4toRK
address valid: true
signature: 64 bytes, verifies: true
ready
connection: Connected
friends: 0
killed

Identity

On first start the SDK generates a key pair and stores it in persistentLocation, as the file carrier.data. Every later start with the same directory reuses that identity. Each node has two identifiers:

  • getUserId() returns the 32-byte public key in Base58. This is the permanent identity, used as the friend id in every API.
  • getAddress() returns the userId plus a 4-byte nospam and checksum (38 bytes in Base58). This is what you share so others can send a friend request. Change the nospam with setSelfNospam(_:) to stop new requests to an old address.

Carrier.isValidAddress(_:), Carrier.isValidUserId(_:) and Carrier.getUserIdFromAddress(_:) are static helpers. identitySign(_:) returns a 64-byte XEdDSA signature, and the static Carrier.identityVerify(userId:message:signature:) checks it without a Carrier instance. Signatures are compatible with the JavaScript peer. To move an identity, call exportSecretKey() and load the key on a fresh data directory:

func identityBackup(carrier: Carrier) throws -> Data {
    return try carrier.exportSecretKey()   // 32 bytes; keep it in the Keychain
}
func restoreOptions(secretKey: Data, freshDataDir: String) -> CarrierOptions {
    let options = makeOptions(dataDir: freshDataDir)
    options.secret_key = secretKey
    return options
}

Profile data is set with setSelfUserInfo(_:) (name, description, gender, phone, email, region) and setSelfProfileExt(_:) (avatar URL, link, ENS name, extra):

func profileSample(carrier: Carrier) throws {
    let me = try carrier.getSelfUserInfo()
    me.name = "Bob"
    me.briefDescription = "testing Carrier on iOS"
    try carrier.setSelfUserInfo(me)
    try carrier.setSelfPresence(.None)
    try carrier.setSelfProfileExt(CarrierProfileExt(avatarUrl: "https://example.com/a.png",
                                                    url: "", ens: "", extra: ""))
}

Friends

A friendship is mutual. One side calls addFriend(with:withGreeting:) with the other side's address: the parameter is labelled with userId, but the native call takes an address. The other side gets didReceiveFriendRequest and calls acceptFriend(with:) with the requester's userId. Both sides then get newFriendAdded. Presence and connection changes arrive as friendConnectionDidChange, friendPresenceDidChange and friendInfoDidChange.

func friendsSample(carrier: Carrier, friendAddress: String) throws {
    // addFriend takes the friend's *address* (not the userId).
    if Carrier.isValidAddress(friendAddress) {
        try carrier.addFriend(with: friendAddress, withGreeting: "hi, it's me")
    }
    for friend in try carrier.getFriends() {
        print(friend.userId ?? "", friend.name ?? "", friend.status, friend.presence)
    }
    if let friendId = Carrier.getUserIdFromAddress(friendAddress),
       carrier.isFriend(with: friendId) {
        try carrier.setFriendLabel(forFriend: friendId, newLabel: "Alice")
        // try carrier.removeFriend(friendId)
    }
}

getFriendRequestProfileExt(_:), called from inside didReceiveFriendRequest, returns the requester's avatar URL if the request carried one.

Messages

sendFriendMessage(to:withMessage:responseHandler:) sends a Data or String payload of up to 5 MB (the Swift wrapper's limit; the native limit is 16 MB). It returns a message id, and the handler later reports a CarrierReceiptState for that id. The Swift overloads differ only in return type (UInt32 and, for Objective-C, NSNumber), so annotate the result. Incoming messages arrive in didReceiveFriendMessage(_:_:_:_:_:) as Data, with the sender's userId and a timestamp.

func messageSample(carrier: Carrier, friendId: String) throws {
    let payload = Data("hello".utf8)
    let msgId: UInt32 = try carrier.sendFriendMessage(to: friendId, withMessage: payload) { id, state in
        switch state {
        case .ByFriend: print("\(id) delivered to the friend")
        case .Offline:  print("\(id) stored for offline delivery (Express)")
        case .Error:    print("\(id) failed")
        @unknown default: break
        }
    }
    print("queued message \(msgId)")
}

The String overload sends the UTF-8 bytes plus a trailing NUL byte, and the receiver sees that byte in its Data. Use the Data overload when the exact bytes matter.

For a request that needs an answer, use sendInviteFriendRequest(to:withData:responseHandler:). The receiver gets didReceiveFriendInviteRequest and answers with replyFriendInviteRequest(to:withStatus:reason:data:):

func inviteSample(carrier: Carrier, friendId: String) throws {
    try carrier.sendInviteFriendRequest(to: friendId, withData: "ping") { _, from, status, reason, data in
        print("invite reply from \(from): status \(status) reason \(reason ?? "-") data \(data ?? "-")")
    }
}
// The receiving side gets CarrierDelegate.didReceiveFriendInviteRequest and answers with:
func replyInvite(carrier: Carrier, from: String) throws {
    try carrier.replyFriendInviteRequest(to: from, withStatus: 0, reason: nil, data: "pong")
}

Groups. createGroup(), joinGroup(createdBy:withCookie:) (with the cookie from didReceiveGroupInvite), leaveGroup(from:) and getGroups() live on Carrier. CarrierGroup has inviteFriend(_:), sendMessage(_:), getTitle() / setTitle(_:), getPeers() and getPeer(byPeerid:). Group events go to the same delegate object, because CarrierDelegate inherits CarrierGroupDelegate (groupDidConnect, didReceiveGroupMessage, groupTitleDidChange, groupPeerNameDidChange, groupPeerListDidChange).

func groupSample(carrier: Carrier, friendId: String) throws -> CarrierGroup {
    let group = try carrier.createGroup()
    try group.setTitle("Team")
    try group.inviteFriend(friendId)
    try group.sendMessage(Data("hello group".utf8))
    return group
}

Files

File transfer is pull-based and works between friends. Create one CarrierFileTransferManager per Carrier. The sender creates a CarrierFileTransfer with the file's info and calls sendConnectionRequest(). The receiver gets the manager's connect handler, creates its own transfer and calls acceptConnectionRequest(). After that the receiver gets didReceiveFileRequest and asks for data with sendPullRequest(fileId:withOffset:). The sender gets didReceivePullRequest and replies with sendData(fileId:withData:). Data arrives in didReceiveFileTransferData.

final class FileSender: NSObject, CarrierFileTransferDelegate {
    let path: String
    init(path: String) { self.path = path }

    func fileTransferStateDidChange(_ fileTransfer: CarrierFileTransfer,
                                    _ newState: CarrierFileTransferConnectionState) {
        print("file transfer: \(newState)")
    }
    // The receiver asked for data starting at `offset`.
    func didReceivePullRequest(_ fileTransfer: CarrierFileTransfer, _ fileId: String, _ offset: UInt64) {
        guard let handle = FileHandle(forReadingAtPath: path) else { return }
        handle.seek(toFileOffset: offset)
        while case let chunk = handle.readData(ofLength: 4096), !chunk.isEmpty {
            try? fileTransfer.sendData(fileId: fileId, withData: chunk)
        }
        handle.closeFile()
    }
}

func fileSendSample(manager: CarrierFileTransferManager, friendId: String, path: String,
                    delegate: FileSender) throws -> CarrierFileTransfer {
    let info = CarrierFileTransferInfo()
    info.fileName = (path as NSString).lastPathComponent
    info.fileSize = (try FileManager.default.attributesOfItem(atPath: path)[.size] as? UInt64) ?? 0
    let transfer = try manager.createFileTransfer(to: friendId, withFileInfo: info, delegate: delegate)
    try transfer.sendConnectionRequest()
    return transfer
}

final class FileReceiver: NSObject, CarrierFileTransferDelegate {
    func didReceiveFileRequest(_ fileTransfer: CarrierFileTransfer, _ fileId: String,
                               _ fileName: String, _ size: UInt64) {
        try? fileTransfer.sendPullRequest(fileId: fileId, withOffset: 0)
    }
    func didReceiveFileTransferData(_ fileTransfer: CarrierFileTransfer, _ fileId: String,
                                    _ data: Data) -> Bool {
        // append `data` to the file being written; return true to keep receiving
        return true
    }
}

/// Accepts incoming file transfers. Keep the manager and the transfers alive.
final class FileInbox {
    private(set) var manager: CarrierFileTransferManager!
    private var transfers: [CarrierFileTransfer] = []
    private let receiver = FileReceiver()

    init(carrier: Carrier) throws {
        manager = try CarrierFileTransferManager.createInstance(carrier: carrier) { [weak self] _, from, fileInfo in
            guard let self,
                  let transfer = try? self.manager.createFileTransfer(to: from, withFileInfo: fileInfo,
                                                                      delegate: self.receiver) else { return }
            try? transfer.acceptConnectionRequest()
            self.transfers.append(transfer)
        }
    }
}

Track the expected size from didReceiveFileRequest to know when a file is complete. pendTransfering, resumeTransfering and cancelTransfering control a transfer in progress. Small files can also be sent inline as a message, up to the 5 MB message limit.

Sessions and calls

A CarrierSession is a peer-to-peer connection to a friend, negotiated with ICE. The two sides exchange SDP over the friend link. Create one CarrierSessionManager per Carrier. A session holds one or more CarrierStreams (.Audio, .Video, .Text, .Application, .Message) with CarrierStreamOptions (.compress, .plain, .reliable, .multiplexing, .portForwarding). A multiplexing stream adds channels (openChannel, writeChannel) and TCP port forwarding (addService on one side, openPortForwarding on the other).

The handshake follows the native SDK's port-forwarding app (apps/pfd). The caller sends the invite once its stream is Initialized, and starts the session with the SDP in the reply. The callee replies once its stream is Initialized, and starts the session with the caller's SDP once its stream is TransportReady.

final class StreamHandler: NSObject, CarrierStreamDelegate {
    var onStateChange: ((CarrierStream, CarrierStreamState) -> Void)?
    func streamStateDidChange(_ stream: CarrierStream, _ newState: CarrierStreamState) {
        onStateChange?(stream, newState)
    }
    func didReceiveStreamData(_ stream: CarrierStream, _ data: Data) {
        print("stream \(stream.getStreamId()) got \(data.count) bytes")
    }
}

// Caller: add a stream, send the invite once the stream is Initialized,
// start the session with the callee's SDP, write once Connected.
func openSession(manager: CarrierSessionManager, friendId: String,
                 handler: StreamHandler) throws -> CarrierSession {
    let session = try manager.createSession(to: friendId)
    handler.onStateChange = { stream, state in
        switch state {
        case .Initialized:
            try? session.sendInviteRequest { session, status, reason, sdp in
                guard status == 0, let sdp = sdp else { return print("refused: \(reason ?? "")") }
                try? session.start(remoteSdp: sdp)
            }
        case .Connected:
            _ = try? stream.writeData(Data("hello over a stream".utf8))
        default: break
        }
    }
    _ = try session.addStream(type: .Application, options: [.reliable], delegate: handler)
    return session
}

// Callee: createInstance(carrier:sessionRequestHandler:) receives (carrier, from, sdp).
func acceptSessions(carrier: Carrier, handler: StreamHandler) throws -> CarrierSessionManager {
    var accepted: [CarrierSession] = []
    var manager: CarrierSessionManager?
    manager = try CarrierSessionManager.createInstance(carrier: carrier) { _, from, sdp in
        guard let session = try? manager?.createSession(to: from) else { return }
        accepted.append(session)
        handler.onStateChange = { _, state in
            if state == .Initialized { try? session.replyInviteRequest(with: 0, reason: nil) }
            if state == .TransportReady { try? session.start(remoteSdp: sdp) }
        }
        _ = try? session.addStream(type: .Application, options: [.reliable], delegate: handler)
    }
    return manager!
}

Calls. The SDK has no media engine. Beagle's audio and video calls use the iOS WebRTC SDK, which sends its signaling through CarrierExtension. That class has registerExtension(_:) to receive, sendInviteFriendRequest(to:withData:_:) and replyFriendInviteRequest(to:withStatus:_:_:) to send and answer, and turnServerInfo(), which returns TURN credentials from the Carrier network:

func extensionSample(carrier: Carrier, friendId: String) throws -> CarrierExtension {
    let ext = CarrierExtension(carrier)
    try ext.registerExtension { _, from, data in
        print("extension invite from \(from): \(data ?? "")")
    }
    let turn = try ext.turnServerInfo()
    print("turn:\(turn.server):\(turn.port) user \(turn.username)")
    try ext.sendInviteFriendRequest(to: friendId, withData: "{\"type\":\"offer\"}") { _, from, status, _, data in
        print("reply from \(from): \(status) \(data ?? "")")
    }
    return ext
}

Offline messages (Express)

Express has no separate API. Set CarrierOptions.expressNodes, as in the Quickstart. When a friend is offline, sendFriendMessage stores the message on an Express node, and the receipt handler reports .Offline instead of .ByFriend. When the friend comes online, the message arrives in didReceiveFriendMessage with isOffline == true. The public Express node is listed in config/carrier.conf.

Threading and lifecycle

  • start(iterateInterval:) returns at once and runs the native loop on a background DispatchQueue (org.elastos.queue, QoS .background). All delegate callbacks run on that queue, so dispatch to the main queue before you touch UI.
  • The Carrier holds its delegate weak, but the callbacks force-unwrap it. Keep a strong reference to the delegate for as long as the Carrier lives, or callbacks will crash.
  • didBecomeReady fires once the node is connected. Calls made before it may fail. isReady() reports the same state.
  • kill() stops the loop and blocks until the loop thread exits. Call it from your own code, not from inside a Carrier callback, because the loop is running that callback. deinit also calls kill(). After kill() the instance cannot be used; create a new one with the same persistentLocation to resume with the same identity.
  • Keep strong references to CarrierSessionManager, CarrierFileTransferManager, CarrierExtension, sessions and transfers. Call cleanup() on the managers and extension and close() on sessions and transfers before you kill() the Carrier.
  • The SDK has no iOS background-mode or push support of its own. What happens when the app is suspended is up to the app.

API reference

Public API on branch feat/agentnet-proto-v2. Errors are thrown as CarrierError (InvalidArgument, IllegalState, GeneralError(ecode:), DHTError(ecode:), SystemError(ecode:), ICEError(ecode:), UnknowError(ecode:), each with errorCode and errorDescription).

Carrier

MemberSignature
versionstatic func getVersion() -> String
validatorsstatic func isValidAddress(_ address: String) -> Bool, static func isValidUserId(_ id: String) -> Bool, static func getUserIdFromAddress(_ address: String) -> String?
createstatic func createInstance(options: CarrierOptions, delegate: CarrierDelegate) throws -> Carrier
run / stopfunc start(iterateInterval: Int = 0) throws, func kill(), func isReady() -> Bool
idsfunc getAddress() -> String, func getNodeId() -> String, func getUserId() -> String
nospamfunc setSelfNospam(_ newNospam: UInt32) throws, func getSelfNospam() throws -> UInt32
self infofunc setSelfUserInfo(_ newUserInfo: CarrierUserInfo) throws, func getSelfUserInfo() throws -> CarrierUserInfo
presencefunc setSelfPresence(_ newPresence: CarrierPresenceStatus) throws, func getSelfPresence() throws -> CarrierPresenceStatus
client infofunc setClientInfo(_ info: CarrierClientInfo) throws, func getClientInfo() throws -> CarrierClientInfo, func getFriendClientInfo(_ friendId: String) throws -> CarrierClientInfo
profile extfunc setSelfProfileExt(_ ext: CarrierProfileExt) throws, func getSelfProfileExt() throws -> CarrierProfileExt, func getFriendProfileExt(_ friendId: String) throws -> CarrierProfileExt, func getFriendRequestProfileExt(_ userId: String) -> CarrierProfileExt?
identityfunc identitySign(_ message: Data) throws -> Data, static func identityVerify(userId: String, message: Data, signature: Data) -> Bool, func exportSecretKey() throws -> Data, func createIdentityAuthProof(serverPublicKey: Data, transcript: Data) throws -> Data
friendsfunc getFriends() throws -> [CarrierFriendInfo], func getFriendInfo(_ friendId: String) throws -> CarrierFriendInfo, func setFriendLabel(forFriend friendId: String, newLabel: String) throws, func isFriend(with userId: String) -> Bool, func addFriend(with userId: String, withGreeting hello: String) throws, func acceptFriend(with userId: String) throws, func removeFriend(_ friendId: String) throws
messagesfunc sendFriendMessage(to target: String, withMessage msg: Data, responseHandler: @escaping CarrierFriendMessageReceiptResponseHandler) throws -> UInt32 (also String, and NSNumber-returning variants)
invitesfunc sendInviteFriendRequest(to target: String, withData data: String, responseHandler: @escaping CarrierFriendInviteResponseHandler) throws, func replyFriendInviteRequest(to target: String, withStatus status: Int, reason: String?, data: String?) throws
groupsfunc createGroup() throws -> CarrierGroup, func joinGroup(createdBy friendId: String, withCookie cookie: Data) throws -> CarrierGroup, func leaveGroup(from group: CarrierGroup) throws, func getGroups() throws -> [CarrierGroup]
constantstatic let MAX_APP_MESSAGE_LEN: Int = 2048

Handler types: CarrierFriendMessageReceiptResponseHandler = (_ msgid: UInt32, _ status: CarrierReceiptState) -> Void and CarrierFriendInviteResponseHandler = (_ carrier: Carrier, _ from: String, _ status: Int, _ reason: String?, _ data: String?) -> Void.

CarrierOptions

PropertyTypeNotes
persistentLocationString?Required. Data directory for identity and friends.
bootstrapNodes[BootstrapNode]?Required (force-unwrapped). ipv4, ipv6, port, publicKey (all String?).
expressNodes[ExpressNode]?Optional. Same fields. Enables offline delivery.
udpEnabledBoolDefault true. false forces TCP.
loglevelCarrierLogLevelDefault .Debug. .None through .Verbose.
secret_keyData?Optional 32-byte key from exportSecretKey(), to import an identity into a fresh directory.

CarrierDelegate (all methods optional, @objc)

willBecomeIdle(_:), connectionStatusDidChange(_:_:), didBecomeReady(_:), selfUserInfoDidChange(_:_:), didReceiveFriendsList(_:_:), friendConnectionDidChange(_:_:_:), friendInfoDidChange(_:_:_:), friendPresenceDidChange(_:_:_:), didReceiveFriendRequest(_ carrier:, _ userId:, _ userInfo: CarrierUserInfo, _ hello:), newFriendAdded(_:_:), friendRemoved(_:_:), didReceiveFriendMessage(_ carrier:, _ from: String, _ data: Data, _ timestamp: Date, _ isOffline: Bool), didReceiveFriendInviteRequest(_:_:_:), didReceiveGroupInvite(_ carrier:, _ from: String, _ cookie: Data), plus the CarrierGroupDelegate methods.

Data types

TypeMembers
CarrierUserInfouserId (read-only), name, briefDescription, hasAvatar, gender, phone, email, region
CarrierFriendInfo (subclass)status: CarrierConnectionStatus, label: String?, presence: CarrierPresenceStatus
CarrierClientInfoprotoVersion: UInt32, platform, osVersion, appVersion; init(protoVersion:platform:osVersion:appVersion:)
CarrierProfileExtavatarUrl, url, ens, extra; init(avatarUrl:url:ens:extra:)
CarrierConnectionStatus.Connected, .Disconnected
CarrierPresenceStatus.None, .Away, .Busy
CarrierReceiptState.ByFriend, .Offline, .Error
CarrierGroup / CarrierGroupPeergetId(), inviteFriend(_:), sendMessage(_:), getTitle(), setTitle(_:), getPeers(), getPeer(byPeerid:), leave() / userId, name

Sessions

TypeMembers
CarrierSessionManagerstatic func createInstance(carrier: Carrier) throws, static func createInstance(carrier: Carrier, sessionRequestHandler: @escaping CarrierSessionRequestHandler) throws, func createSession(to target: String) throws -> CarrierSession, func cleanup()
CarrierSessiongetPeer(), sendInviteRequest(handler:), replyInviteRequest(with:reason:), start(remoteSdp:), addStream(type:options:delegate:) -> CarrierStream, removeStream(stream:), addService(serviceName:protocol:host:port:), removeService(serviceName:), close()
CarrierStreamgetType(), getStreamId(), getTransportInfo(), writeData(_:) -> NSNumber, openChannel(cookie:), closeChannel(_:), writeChannel(_:data:), pendChannel(_:), resumeChannel(_:), openPortForwarding(service:ptotocol:host:port:), closePortForwarding(_:)
CarrierStreamDelegate (optional)streamStateDidChange, didReceiveStreamData, shouldOpenNewChannel, didOpenNewChannel, didCloseChannel, didReceiveChannelData, channelPending, channelResumed
CarrierStreamState.Initialized, .TransportReady, .Connecting, .Connected, .Deactivated, .Closed, .Error

File transfer

TypeMembers
CarrierFileTransferManagerstatic func createInstance(carrier: Carrier) throws, static func createInstance(carrier: Carrier, connectHandler: @escaping CarrierFileTransferConnectHandler) throws, func createFileTransfer(to address: String, withFileInfo: CarrierFileTransferInfo?, delegate: CarrierFileTransferDelegate) throws -> CarrierFileTransfer, func cleanup()
CarrierFileTransfergetPeer(), acquireFileId(by:), acquireFileName(by:), sendConnectionRequest(), acceptConnectionRequest(), addFile(_:), sendPullRequest(fileId:withOffset:), sendData(fileId:withData:), cancelTransfering(fileId:withReason:reason:), pendTransfering(fileId:), resumeTransfering(fileId:), close()
CarrierFileTransferInfofileName, fileId, fileSize: UInt64
CarrierFileTransferDelegate (optional)fileTransferStateDidChange, didReceiveFileRequest, didReceivePullRequest, didReceiveFileTransferData -> Bool, fileTransferPending, fileTransferResumed, fileTransferWillCancel

CarrierExtension

init(_ carrier: Carrier), func turnServerInfo() throws -> TurnServerInfo (server, port, username, password, realm), func registerExtension(_ handler: @escaping CarrierExtensionInviteCallback) throws, func sendInviteFriendRequest(to:withData:_:) throws, func replyFriendInviteRequest(to:withStatus:_:_:) throws, func cleanup().

Source

  • Release: 6.1.0, commit 48bd045240bd8202db9e46b72a9bc1f15dde2f52, with ElastosCarrierSDK.xcframework.zip (SwiftPM checksum 635e02b50d8cd7bdf2e2ee226f5cc5f7532bf28f71136069f33ab2917bf26d2d).
  • Repository: 0xli/Elastos.CarrierClassic.Swift, branch feat/agentnet-proto-v2 for the latest. The API on this page was read at commit 789576869a3b304f459c5a4c22c11c4742db3efe; 6.1.0 adds only packaging on top of it. This is the branch Beagle Chat for iOS ships. Branch agent/permissionless-realtime-network (commit 61666e4) is two commits behind it, without the client-info, profile-extension, identity sign/verify and key-export APIs.
  • Native libraries: 0xli/Elastos.NET.Carrier.Native.SDK release v6.1.0 (commit fd7dfc8); branch feat/agentnet-proto-v2 for the latest.
  • License: the repository's LICENSE file is GPLv3. The source file headers carry an MIT-style notice from the Elastos Foundation.