The ART Objective-C ADK connects iOS and macOS apps to ART (A Realtime Tech), a realtime communication platform for building intelligent applications with WebSocket-based messaging, AI Agents, AI Orchestrators, presence tracking, end-to-end encrypted channels, and CRDT-backed shared objects.
With the ADK you can:
- Send and receive messages in real time
- See who is online in a channel
- Encrypt conversations end-to-end
- Keep shared data in sync across users and devices
- Check or block messages before they are delivered
- Chat with AI agents and run multi-agent workflows
- Upload files and share them with AI agents
- Receive in-app notifications and register devices for push notifications
- Requirements
- Installation
- Getting started
- Connection
- Channels and messages
- Presence
- Encrypted channels
- Shared objects
- Interceptors
- AI agents
- AI orchestrators
- File storage
- Profiles and connectors
- Notifications
- Calling ART APIs
- Logging
- Documentation
- License
- iOS 15 or later, or macOS 12 or later
- Xcode 15 or later
- An ART project with its environment, project key, organisation and client ID
Add the ADK to your Podfile:
pod 'ArtAdk', '~> 1.0.3'
pod 'ArtAdk/Notifications', '~> 1.0.3' # optionalThen run:
pod installAdd the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/aiotrixdev/art-objectivec-adk.git", from: "1.0.3")
]Then add the libraries to your target:
.target(
name: "MyApp",
dependencies: [
.product(name: "ArtAdkObjC", package: "art-objectivec-adk"),
.product(name: "ArtAdkNotifications", package: "art-objectivec-adk") // optional
]
)In Xcode, choose File › Add Package Dependencies…, enter https://github.com/aiotrixdev/art-objectivec-adk.git, and add ArtAdkObjC to your app target. Add ArtAdkNotifications only if your app uses notifications.
@import ArtAdk;With CocoaPods, the notifications add-on is part of the ArtAdk module once you add ArtAdk/Notifications. With Swift Package Manager, also add @import ArtAdkNotifications;.
ART signs users in with a short-lived passcode. Your backend requests the passcode from ART using your client secret, and returns only the passcode to the app:
- (void)fetchPasscodeForUsername:(NSString *)username
completion:(void (^)(NSString *passcode, NSError *error))completion {
NSMutableURLRequest *request =
[NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"PASSCODE_URL"]];
request.HTTPMethod = @"POST";
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:@{@"username" : username}
options:0
error:nil];
[[[NSURLSession sharedSession]
dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary *json = data ? [NSJSONSerialization JSONObjectWithData:data
options:0
error:nil]
: nil;
NSString *passcode = [json isKindOfClass:[NSDictionary class]] ? json[@"passcode"] : nil;
completion(passcode ?: @"", error);
}] resume];
}Important: Never put your client secret in the app. Anything shipped inside an app can be extracted. Whether users can sign in using only a passcode depends on your ART project settings, so confirm this with the ART team before release.
@import ArtAdk;
AdkConfig *config = [[AdkConfig alloc] initWithUri:@"YOUR_WEBSOCKET_URI"
authToken:passcode
getCredentials:nil
root:nil];
Adk *adk = [[Adk alloc] initWithConfig:config];
[adk setCredentials:[[CredentialStore alloc] initWithEnvironment:@"YOUR_ENV"
projectKey:@"YOUR_PROJECT_KEY"
orgTitle:@"YOUR_ORG"
clientID:@"YOUR_CLIENT_ID"
clientSecret:@""
config:nil
accessToken:passcode]];
[adk connect:nil completion:^{
NSLog(@"Connection state: %@", [adk getState]);
}];[adk subscribe:@"CHANNEL_NAME"
completion:^(BaseSubscription *channel, NSError *error) {
if (!channel) {
NSLog(@"Couldn't subscribe: %@", error);
return;
}
[channel.emitter on:@"message" handler:^(id data) {
NSLog(@"Received: %@", data);
}];
[channel push:@"message"
data:@{@"text" : @"Hello ART!"}
options:nil
completion:^(NSError *pushError) {
if (pushError) {
NSLog(@"Couldn't send: %@", pushError);
}
}];
}];setCredentials: is the simplest option. You can also use one of these:
| Option | When to use it |
|---|---|
AdkConfig.getCredentials |
Your credentials change over time, for example when the passcode is refreshed. The ADK calls the block each time it signs in. |
AdkConfig.autoLoadCredsFromJSON = YES |
You keep credentials in an adk-services.json file, either in the app bundle or in the folder set in AdkConfig.root. The file uses the keys Client-ID, Environment, Org-Title and ProjectKey. |
If you use more than one option, getCredentials takes priority, then setCredentials:, then the JSON file.
connect:completion: opens the connection and signs the user in. If the connection drops, the ADK reconnects automatically.
[adk on:@"connection" handler:^(id data) {
if ([data isKindOfClass:[ConnectionDetail class]]) {
NSLog(@"Connected: %@", ((ConnectionDetail *)data).connectionId);
}
}];
[adk on:@"close" handler:^(id reason) {
NSLog(@"Connection closed: %@", reason);
}];
[adk connect:nil completion:nil];You can pause the connection and resume it later. A paused connection stays closed until you call resume:.
[adk pause];
[adk resume:nil];
[adk disconnect:nil]; // close the connectionadk.state tells you where the connection stands:
| State | Meaning |
|---|---|
AdkStateConnecting |
Opening the connection, or reconnecting after it dropped |
AdkStateConnected |
Connected and signed in |
AdkStatePaused |
Paused with pause |
AdkStateStopped |
Not connected |
If your project reaches its billing or concurrency limit, the ADK emits a limitExceeded event and stops reconnecting.
A channel is a named stream of messages. Subscribe to a channel to send and receive messages on it. subscribe:completion: returns a Subscription for regular channels and a LiveObjSubscription for shared-object channels.
[adk subscribe:@"room-42"
completion:^(BaseSubscription *channel, NSError *error) {
// Use the channel.
// When you no longer need it:
[channel unsubscribe:nil];
}];[channel push:@"message" data:@{@"text" : @"Hello"} options:nil completion:^(NSError *error) {}];To send a message to specific users, list their usernames in the to option:
[channel push:@"message"
data:@{@"text" : @"Hi Bob"}
options:[[PushConfig alloc] initWithTo:@[ @"bob" ]]
completion:^(NSError *error) {}];On targeted channels, push completes once ART confirms delivery. If no confirmation arrives within 50 seconds, it completes with an ErrorCodeAckTimeout error.
[channel.emitter on:@"message" handler:^(id data) {
NSLog(@"Received: %@", data);
}];On regular channels, bind:callback: also delivers messages that arrived before you started listening. It returns a token that you can use to remove that listener later:
Subscription *subscription = (Subscription *)channel;
NSUUID *token = [subscription bind:@"message" callback:^(id content) {
NSLog(@"%@", content);
}];
// Later:
[subscription remove:@"message" identifier:token];listen: works the same way for every event on the channel.
Find out who is online in the channel. The callback runs again whenever someone joins or leaves.
__block PresenceUnsubscribe stopPresence = nil;
[channel fetchPresence:YES
callback:^(NSArray<NSString *> *users) {
NSLog(@"Online: %@", users);
}
completion:^(PresenceUnsubscribe unsubscribe, NSError *error) {
stopPresence = unsubscribe;
}];
// Later, to stop receiving updates:
if (stopPresence) {
stopPresence(nil);
}Pass YES to list each user once, even when they are connected from several devices.
Messages on encrypted channels are encrypted on the sender's device, and only the recipients can read them. Before using an encrypted channel, create a key pair for the current user. generateKeyPair: creates the keys and registers the public key with ART.
[adk generateKeyPair:^(KeyPairType *keyPair, NSError *error) {
[adk subscribe:@"SECURE_CHANNEL"
completion:^(BaseSubscription *secure, NSError *subscribeError) {
[secure.emitter on:@"message" handler:^(id data) {
NSLog(@"Decrypted: %@", data);
}];
[secure push:@"message"
data:@{@"text" : @"Private"}
options:[[PushConfig alloc] initWithTo:@[ @"bob" ]]
completion:^(NSError *pushError) {}];
}];
}];A shared-object channel holds a document that every subscriber can read and edit. Changes are merged automatically using CRDTs (conflict-free replicated data types), so everyone ends up with the same data.
[adk subscribe:@"CRDT_CHANNEL"
completion:^(BaseSubscription *channel, NSError *error) {
if (![channel isKindOfClass:[LiveObjSubscription class]]) {
return;
}
LiveObjSubscription *live = (LiveObjSubscription *)channel;
// Edit the document, then send your changes.
[[live state][@"document"][@"title"] set:@"My Doc"];
[live flush:^{}];
// Read a value.
[[live query:@"document"] executeWithCompletion:^(id document) {
NSLog(@"%@", document);
}];
// Watch for changes. The callback also receives the current value.
void (^stopWatching)(void) = [[live query:@"document"] listenWithCallback:^(id value) {
NSLog(@"Updated: %@", value);
}];
// Later:
stopWatching();
}];Lists support the usual operations:
CRDTProxy *items = [live state][@"items"];
[items pushItem:@"alpha"]; // add to the end
[items unshiftItem:@"zero"]; // add to the start
[items pop]; // remove the last item
[items removeAtIndex:2]; // remove the item at an index
[items spliceStart:1 deleteCount:1 insertItems:@[ @"x" ]]; // replace a range
[items flushWithCompletion:^{}];An interceptor sees messages before they are delivered and decides what happens to each one. Call resolve to deliver the message, with or without changes, or reject to block it. You can resolve with a dictionary or an array.
[adk intercept:@"profanity-filter"
fn:^(NSDictionary *payload, InterceptorResolve resolve, InterceptorReject reject) {
NSString *text = [payload[@"text"] isKindOfClass:[NSString class]] ? payload[@"text"] : @"";
if ([text containsString:@"badword"]) {
reject(@"Message blocked");
return;
}
resolve(payload);
}
completion:^(Interception *interception, NSError *error) {}];The name must match an interceptor set up in your ART project.
Chat with an agent built on ART. Each conversation takes place in a thread.
Agent *agent = [adk agent:@"YOUR_AGENT_ID"];
AgentThread *thread = [agent thread]; // or [agent threadWithId:@"THREAD_ID"] to continue a conversation
[thread listen:^(AgentEventEnvelope *envelope) {
NSLog(@"Event: %@", envelope.event);
}];
[thread run:@"Plan a 3-day trip to Dubai"
replyId:nil
completion:^(Run *run, NSError *error) {
[run done:^(AgentOutput *output, AgentError *agentError) {
if (agentError) {
NSLog(@"The agent couldn't finish: %@", agentError.message);
return;
}
NSLog(@"%@", output.message);
}];
}];done: waits for the agent's final answer. listen: receives every event in the thread as it happens, such as progress updates and questions from the agent. envelope.content holds all the fields the server sent with the event.
Use the thread's state to drive a status indicator, such as "Queued" or "Waiting for your input":
ARTStateUnsubscribe stopUpdates = [thread listenState:^(ARTThreadState *state) {
NSLog(@"%@: %@", state.phase, state.message);
}];
NSLog(@"%@", [thread getState].phase); // the latest state
stopUpdates(); // stop receiving updatesThe phase is an ARTThreadStatePhase: idle, submitted, queued, running, waiting_for_agent, waiting_for_approval, waiting_for_workspace, completed, failed or cancelled, with constants such as ARTThreadStatePhaseRunning. A phase the ADK doesn't know yet is kept as the server's value.
An agent can pause and ask the user for more information. This is often called human-in-the-loop. Register a handler before you start the run, and send the answer with sendFeedback:completion::
[thread feedbackRequest:^(HumanInputRequest *request, Run *run) {
NSLog(@"The agent asks: %@", request.prompt);
[run sendFeedback:@"Budget is 50,000, travelling in December"
completion:^(NSError *error) {
if (error) {
NSLog(@"Couldn't send the answer: %@", error);
}
}];
}];Upload the file first, then attach it to your message:
ARTUploadOptions *options = [[ARTUploadOptions alloc] init];
options.progress = ^(double fraction) {
NSLog(@"Uploaded %d%%", (int)(fraction * 100));
};
[agent uploadFileURL:documentURL
options:options
completion:^(ARTFileRef *file, NSError *error) {
[thread run:@"Summarize this document"
replyId:nil
fileMeta:@[ [ARTFileMeta fileMetaWithFileRef:file] ]
completion:^(Run *run, NSError *runError) {}];
}];By default, only the file's owner can use it. To allow other agents to use it as well, list their IDs in scope, for example [[ARTFileMeta alloc] initWithFileRef:file scope:@[ @"OTHER_AGENT_ID" ]].
An orchestrator coordinates several agents to complete a larger task. Start a thread, listen for its events, and send the user's input:
Orchestrator *orchestrator = [adk orchestrator:@"YOUR_ORCHESTRATOR_ID"];
[orchestrator thread:^(OrchestratorThread *thread, NSError *error) {
[thread listen:^(NSDictionary<NSString *, id> *event) {
NSLog(@"%@", event);
}];
[thread listenState:^(ARTThreadState *state) {
NSLog(@"%@", state.phase);
}];
[thread push:@"user_input"
data:@{@"message" : @"Plan a 3-day trip to Goa"}
completion:^(NSError *pushError) {}];
}];Upload files to ART storage, then list, fetch or delete them. Each upload returns an ARTFileRef with the file's ID, name, size, content type and a URL for reading it.
[adk uploadData:pngData
filename:@"chart.png"
contentType:@"image/png"
options:nil
completion:^(ARTFileRef *ref, NSError *error) {
// A signed URL for reading the file:
[adk getFile:ref.fileId timeoutMs:nil completion:^(ARTStorageFile *file, NSError *getError) {}];
// Pass hard:YES to delete it permanently:
[adk deleteFile:ref.fileId hard:NO timeoutMs:nil completion:nil];
}];
ARTListOptions *filter = [[ARTListOptions alloc] init];
filter.configType = ARTConfigTypeMedia;
filter.page = @1;
filter.limit = @20;
[adk listFilesWithOptions:filter completion:^(ARTStorageFileList *page, NSError *error) {}];To upload a file from disk, use uploadFileURL:options:completion:. The file is streamed rather than loaded into memory, and you can follow its progress with ARTUploadOptions.progress.
Every file has an owner, which depends on where you upload it from:
| Uploaded with | Owner |
|---|---|
adk |
Your project, or the ID you set in ARTUploadOptions.configId |
agent |
The agent |
| An agent thread | The thread |
orchestrator |
The orchestrator |
| An orchestrator thread | The thread |
A Subscription |
The channel (orchestrator-enabled channels only) |
When an upload fails, the error's domain is ARTUploadErrorDomain and its code is the ARTUploadStep that failed. userInfo[ARTHTTPStatusCodeKey] holds the HTTP status code. A 403 at ARTUploadStepSignedURL means the user's role doesn't have storage permission.
[adk uploadData:pngData filename:nil contentType:nil options:nil completion:^(ARTFileRef *ref, NSError *error) {
if ([error.domain isEqualToString:ARTUploadErrorDomain] &&
error.code == ARTUploadStepSignedURL &&
[error.userInfo[ARTHTTPStatusCodeKey] integerValue] == 403) {
NSLog(@"This user's role doesn't allow uploads");
}
}];Update the signed-in user's profile. Only the fields you set are changed.
ARTUpdateProfileData *profile = [[ARTUpdateProfileData alloc] init];
profile.firstName = @"Ada";
profile.email = @"ada@example.com";
[adk updateProfile:profile completion:nil];Each connector keeps its own profile for the user. Read it, or update the fields that the connector allows:
NSError *error = nil;
ARTConnector *crm = [adk connector:@"YOUR_CONNECTOR_ID" error:&error];
// The allowed fields and their current values:
[crm profile:^(ARTConnectorProfile *current, NSError *profileError) {}];
// Only allowed fields can be changed:
[crm updateProfile:@{@"region" : @"eu"} completion:^(ARTConnectorProfile *updated, NSError *updateError) {}];Notifications are in the optional notifications add-on (ArtAdk/Notifications with CocoaPods, ArtAdkNotifications with Swift Package Manager). Add it to your Adk instance once and keep the object it returns:
ARTNotificationsApi *inbox = [adk use:[ARTNotificationsPlugin pluginWithOptions:nil]];
// Receive new notifications as they arrive.
[inbox onNew:^(ARTNotification *notification) {
NSLog(@"%@", notification.title);
}
completion:^(void (^stop)(void), NSError *error) {
// Call stop() to stop receiving them.
}];
ARTNotificationListParams *unread = [[ARTNotificationListParams alloc] init];
unread.status = ARTNotificationStatusUnread;
[inbox list:unread completion:^(ARTNotificationPage *page, NSError *error) {}];
[inbox markRead:nil completion:nil]; // marks every unread notification as read
// Register the device for push notifications.
[inbox registerDevice:[[ARTRegisterDeviceInput alloc] initWithToken:pushToken
platform:ARTDevicePlatformIOS]
completion:nil];You can also send notifications, count unread notifications and manage registered devices. To get the same object elsewhere in your app, call [adk pluginNamed:@"notifications"].
callEndpoint:options:completion: sends an authenticated request to an ART REST endpoint and returns the decoded JSON response:
CallApiProps *options = [[CallApiProps alloc] initWithMethod:@"POST"
payload:@{@"key" : @"value"}
queryParams:nil
headers:nil];
[adk callEndpoint:@"/v1/some-endpoint"
options:options
completion:^(id result, NSError *error) {
// On a non-2xx response, error.userInfo[ARTHTTPStatusCodeKey] holds the status.
}];The ADK doesn't print anything by default. To see its warnings and errors, set a log handler:
ARTLog.handler = ^(ARTLogLevel level, NSString *message) {
NSLog(@"[ART][%@] %@", ARTLogLevelName(level), message);
};For full guides, see the ART ADK documentation. For what's new in each release and how to upgrade, see the changelog.
| Topic | Link |
|---|---|
| Overview | ADK overview |
| Installation | Objective-C installation |
| Publish and subscribe | Pub/sub |
| Connection management | Connections |
| User presence | Presence |
| Encrypted channels | Encryption |
| Shared object channels | Shared objects |
| Interceptors | Interceptors |
| Agents | Agents |
| Orchestrators | Orchestrators |
The ART Objective-C ADK is released under the MIT License.