Swift Client SDK

Installation

See the repository README for installation instructions

Quick Overview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let chatManager = ChatManager(
    instanceLocator: "your-instance-locator",
    tokenProvider: yourTokenProvider,
    userID: "your-user-id"
)

chatManager.connect(delegate: yourDelegate) { currentUser, error in
    guard error == nil else {
        print("Error connecting: \(error.localizedDescription)")
        return
    }

    let rooms = currentUser.rooms
    print("Connected! \(currentUser.name)'s rooms: \(rooms)")
}

Initialization

The minimum you need to initialize a client is the following:

1
2
3
4
5
let chatManager = ChatManager(
    instanceLocator: "your-instance-locator",
    tokenProvider: yourTokenProvider,
    userID: "your-user-id"
)
You must retain a strong reference to theChatManager instance in your code while your app is using the SDK. If the ChatManager becomes unreferenced then there will be no root reference to prevent the SDK components being deallocated, which will result in crashes when they are used later.

PCTokenProvider

1
2
3
4
5
6
7
8
9
10
let tokenProvider = PCTokenProvider(
    url: "your.auth.url",
    requestInjector: { req in
        req.addHeaders(["My-Header": "some value"])
        req.addQueryItems([
            URLQueryItem(name: "my_query", value: "a_value")
        ])
        return req
    }
)

PCTokenProvider Arguments

The TokenProvider constructor takes a single options object with the following properties.

ArgumentTypeDescription
urlstring (required)The URL that the ChatManager should make a POST request to in order to fetch a valid token. This will be either the test token provider or your own custom auth endpoint.
requestInjectorfunction (optional)A function with signature (PCTokenProviderRequest) -> PCTokenProviderRequest that provides a basic token provider request object on which you can call addHeaders([String: String]) or addQueryItems([URLQueryItem]) to set one or both of the request's headers and query items. A PCTokenProviderRequest must be returned by this function.

When making requests to a token endpoint, the TokenProvider expects the response to have the following format:

1
2
3
4
{
  "access_token": "<your token here>",
  "expires_in": "<seconds until token expiry here>"
}
For more information on the auth process please refer to the authentication docs.

Connecting

Once you've initialized your ChatManager object you are ready to connect to the Chatkit servers. This will establish a connection that will ensure that the client receives relevant updates. The full range of updates that the client can receive are described in the PCChatManagerDelegate section.

1
2
3
4
5
6
7
8
9
10
11
12
13
let chatManager = ChatManager(
    instanceLocator: "your-instance-locator",
    tokenProvider: yourTokenProvider,
    userID: "your-user-id"
)

chatManager.connect(delegate: yourDelegate) { currentUser, error in
    guard error == nil else {
        print("Error connecting: \(error.localizedDescription)")
        return
    }
    print("Successfully connected")
}

The currentUser object that will be accessible in the completionHandler block, provided there were no errors on connection, gives you access to the list of rooms that the connected user is a member of.

PCCurrentUser

When an initial connection is successfully made to the Chatkit servers the client will receive a PCCurrentUser object, named currentUser in this example.

1
2
3
4
5
6
7
chatManager.connect(delegate: yourDelegate) { currentUser, error in
    guard error == nil else {
        print("Error connecting: \(error.localizedDescription)")
        return
    }
    print("Successfully connected with current user: \(currentUser)")
}

The PCCurrentUser object will almost always be the most useful in terms of updating UI components on initial load.

The available properties and what they represent are listed below:

rooms ([Room]) - the rooms that the connected user is a member of

PCChatManagerDelegate

The PCChatManagerDelegate you provide when initializing the ChatManager object will receive updates when events relevant to the connected user occur in the system. Please be aware that if you're updating the UI from any of the methods below then you'll need to ensure that you perform the updates on the main thread.

This lists the functions from the delegate protocol that you can implement, along with when they get called:

  • func onAddedToRoom(_ room: PCRoom) - the current user is added to a room
  • func onRemovedFromRoom(_ room: PCRoom) - the current user is removed from a room
  • func onRoomUpdated(room: PCRoom) - a room that the current user is a member of has its name changed
  • func onRoomDeleted(room: PCRoom) - a room that the current user is a member of is deleted
  • func onError(error: Error) - an error is received which relates to the current user

The following functions can be implemented as part of the PCChatManagerDelegate, but it's likely that implementing them as part of a PCRoomDelegate will be more useful. This is because they're likely to be useful to an end user when displayed at the room-specific level.

  • func onUserStartedTyping(inRoom: PCRoom, user: PCUser) - a user started typing in a room in which the current user is a member
  • func onUserStoppedTyping(inRoom: PCRoom, user: PCUser) - a user stopped typing in a room in which the current user is a member
  • func onUserJoinedRoom(_ room: PCRoom, user: PCUser) - a user joined a room in which the current user is a member
  • func onUserLeftRoom(_ room: PCRoom, user: PCUser) - a user left a room in which the current user is a member
  • func onPresenceChanged(stateChange: PCPresenceStateChange, user: PCUser) - a user came online or went offline

Rooms

There are a few important things to remember about Chatkit rooms:

  • rooms are either public or private
  • the users that are members of a room can change over time
  • all chat messages belong to a room

Creating a Room

All that you need to provide when creating a room is a name. The user that creates the room will automatically be added as a member of the room.

The following code will create a public room called my room name. Note that a room name must be no longer than 60 characters.

1
2
3
4
5
6
7
currentUser.createRoom(name: "my room name") { room, error in
    guard error == nil else {
        print("Error creating room: \(error.localizedDescription)")
        return
    }
    print("Created public room called \(room.name)")
}

Note that unless explicitly specified otherwise all rooms created will be public by default.

To create a private room you need to add in an extra parameter, isPrivate, and set it to true:

1
2
3
4
5
6
7
8
9
10
currentUser.createRoom(
    name: "my room name",
    isPrivate: true
) { room, error in
    guard error == nil else {
        print("Error creating room: \(error.localizedDescription)")
        return
    }
    print("Created private room called \(room.name)")
}

If you want to create a room and add some other users to the room at the point of creation then you need to specify the IDs of users you wish to include:

1
2
3
4
5
6
7
8
9
10
currentUser.createRoom(
    name: "my room name",
    addUserIDs: yourListOfUserIDs
) { room, error in
    guard error == nil else {
        print("Error creating room: \(error.localizedDescription)")
        return
    }
    print("Created room called \(room.name)")
}

You can also add custom data to a room by providing a customData parameter of type [String: Any]:

1
2
3
4
5
6
7
8
9
10
currentUser.createRoom(
    name: "my room name",
    customData: ["testing": "custom data"]
) { room, error in
    guard error == nil else {
        print("Error creating room: \(error.localizedDescription)")
        return
    }
    print("Created room called \(room.name)")
}

Subscribing to a Room

Subscribing to a room means that the client will receive new messages as they are added. Along with the room that you wish to subscribe to, you must also provide an object that conforms to the PCRoomDelegate protocol. This will ensure that you receive new information pertaining to the room in realtime.

1
currentUser.subscribeToRoomMultipart(room: myRoom, roomDelegate: aRoomDelegate)

By default when you subscribe to a room you will receive up to the 20 most recent messages that have been added to the room. This is configurable by an extra parameter, messageLimit, as shown below:

1
2
3
4
5
currentUser.subscribeToRoomMultipart(
    room: myRoom,
    roomDelegate: aRoomDelegate,
    messageLimit: 10
)
subscribeToRoomMultipart replaces its deprecated predecessor subscribeToRoom. In order to receive message from the multipart room subscription, the onMultipartMessagemethod from the PCRoomDelegate interface should be implemented.

If fewer messages have been added to the room than are requsted by the messageLimit parameter then only as many messages as have been added will be returned. Note that the upper limit on the number of messages you can receive upon subscription is 100.

If you wish to not receive any of the most recent messages upon subscription then you just need to set the messageLimit parameter to 0.

When a subscription opens, if you requested to receive some of the most recent messages then they will be delivered using the same mechanism as any other new messages: the onMessage function which is part of the PCRoomDelegate will be called for each message.

Check here for specifics on the PCRoomDelegate that you need to provide when subscribing to a room.

Fetching Messages From a Room

Subscribing to a room means that you'll get updated when new messages are added, and it can also provide you with up to the 100 most recent messages added to the room, but sometimes you'll also want the ability to fetch older messages.

In the sample code below the initialID parameter has a value of oldestMessageIDReceived. This is used to represent, as the name suggests, the ID of the oldest message that a client had received up until now.

As an example, let's assume a client subscribed to a room and upon subscription had received messages with IDs from 120 through to 139. Let's also assume that all message IDs for this room are contiguous. If the user wanted to see older messages in the room then the code they'd use would be something like this:

1
2
3
4
5
6
7
8
9
10
11
currentUser.fetchMultipartMessages(
    myRoom,
    initialID: oldestMessageIDReceived
) { messages, err in
    guard err == nil else {
        print("Error fetching messages from \(myRoom.name): \(err!.localizedDescription)")
        return
    }

    // do something with the messages
}
fetchMultipartMessages replaces its deprecated predecessor fetchMessagesFromRoom. The latter functions similarly but return the legacy message type.

Here we assume that either the oldest message ID that has been received so far is being tracked in a variable, or the value is calculated as the request to fetch older messages is being formed, which could look like:

1
let oldestMessageIDReceived = String(roomMessages.last!.id)

In the snippet above we're using roomMessages to represent the array that is being used to keep track of received messages in the relevant room.

There is also an optional limit parameter that takes an Int value to describe the maximum number of messages you'd like to be returned. The default value for the limit is 20, with the maximum being 100 (for a single request).

The initialID parameter is also optional and if you don't provide a value then you will get up to the n most recent messages, where n corresponds to the limit value.

There is one final parameter you can provide: direction. This defaults to PCRoomMessageFetchDirection.older, which means that the messages will be fetched starting from the newest first and working backwards chronologically. If you want to fetch messages starting with the oldest first and working forwards chronologically then you need to set the direction parameter to be PCRoomMessageFetchDirection.newer.

As another example, the following code would fetch (up to) the next 10 messages with IDs greater than the initialID provided, "72".

1
2
3
4
5
6
7
8
9
10
11
12
13
currentUser.fetchMultipartMessages(
    myRoom,
    initialID: "72",
    limit: 10,
    direction: .newer
) { messages, err in
    guard err == nil else {
        print("Error fetching messages from \(myRoom.name): \(err!.localizedDescription)")
        return
    }

    // do something with the messages
}

Adding Users to a Room

If a user is a member of a room and they want to add another user to the room then you'd use the following code:

1
2
3
4
5
6
7
currentUser.addUser(anotherUser, to: myRoom) { error in
    guard error == nil else {
        print("Error adding user to \(myRoom.name): \(error.localizedDescription)")
        return
    }
    print("Added user \(anotherUser.id) to \(myRoom.name)")
}

You can also just provide the ID of the user to be added:

1
2
3
4
5
6
7
currentUser.addUser(id: anotherUserID, to: myRoomID) { error in
    guard error == nil else {
        print("Error adding user to \(myRoom.name): \(error.localizedDescription)")
        return
    }
    print("Added user \(anotherUserID) from \(myRoom.name)")
}

If you want to add multiple users at a time you can do so by providing an array of PCUser objects:

1
2
3
4
5
6
7
8
currentUser.addUsers(usersToAddArray, to: myRoom) { error in
    guard error == nil else {
        print("Error adding users to \(myRoom.name): \(error.localizedDescription)")
        return
    }
    let userIDs = usersToAddArray.map { $0.id }.joined(separator: ", ")
    print("Added users \(userIDs) to \(myRoom.name)")
}

Again, you can also just provide an array of user IDs:

1
2
3
4
5
6
7
8
currentUser.addUsers(usersIDsToAddArray, to: myRoomID) { error in
    guard error == nil else {
        print("Error adding users to \(myRoom.name): \(error.localizedDescription)")
        return
    }
    let userIDs = usersIDsToAddArray.joined(separator: ", ")
    print("Added users \(userIDs) to \(myRoom.name)")
}

Removing Users From a Room

If a user is a member of a room and they want to remove a user from the room then you'd use the following code:

1
2
3
4
5
6
7
currentUser.removeUser(anotherUser, from: myRoom) { error in
    guard error == nil else {
        print("Error removing user from \(myRoom.name): \(error.localizedDescription)")
        return
    }
    print("Removed user \(anotherUser.id) from \(myRoom.name)")
}

You can also just provide the ID of the user to be removed:

1
2
3
4
5
6
7
currentUser.removeUser(id: anotherUserID, from: myRoomID) { error in
    guard error == nil else {
        print("Error removing user from \(myRoom.name): \(error.localizedDescription)")
        return
    }
    print("Removed user \(anotherUserID) from \(myRoom.name)")
}

If you want to remove multiple users at a time you can do so by providing an array of PCUser objects:

1
2
3
4
5
6
7
8
currentUser.removeUsers(usersToRemoveArray, from: myRoom) { error in
    guard error == nil else {
        print("Error removing users from \(myRoom.name): \(error.localizedDescription)")
        return
    }
    let userIDs = usersToRemoveArray.map { $0.id }.joined(separator: ", ")
    print("Removed users \(userIDs) from \(myRoom.name)")
}

Again, you can also just provide an array of user IDs:

1
2
3
4
5
6
7
8
currentUser.removeUsers(userIDsToRemoveArray, from: myRoomID) { error in
    guard error == nil else {
        print("Error removing users from \(myRoom.name): \(error.localizedDescription)")
        return
    }
    let userIDs = userIDsToRemoveArray.joined(separator: ", ")
    print("Removed users \(userIDs) from \(myRoom.name)")
}

Joining a Room

To join a room you need to provide either the room's ID or a room object that contains the relevant ID. You could have obtained such a room object if you made a request to get the list of joinableRooms for a given user.

If you want to join a room using the room's ID then you'd use the following code:

1
2
3
4
5
6
7
currentUser.joinRoom(roomID: someRoomID) { error in
    guard error == nil else {
        print("Error joining room with ID \(someRoomID): \(error.localizedDescription)")
        return
    }
    print("Joined room with ID: \(someRoomID)")
}

If you want to join a room using an appropriate room object then you'd use the following code:

1
2
3
4
5
6
7
currentUser.joinRoom(someRoom) { error in
    guard error == nil else {
        print("Error joining room: \(error.localizedDescription)")
        return
    }
    print("Joined room \(someRoomID)")
}

Leaving a Room

To leave a room you need a reference to the room object that represents the room the user will leave. Here we have a room object called myRoom.

1
2
3
4
5
6
7
currentUser.leaveRoom(myRoom) { error in
            guard error == nil else {
                print("Error leaving room \(myRoom.name): \(error.localizedDescription)")
                return
            }
            print("Left room \(myRoom.name)")
        }

If you've kept a reference to the numerical ID for the room you wish to have the user leave then you can also use the following code:

1
2
3
4
5
6
7
currentUser.leaveRoom(id: myRoomID) { error in
    guard error == nil else {
        print("Error leaving room with ID \(myRoomID): \(error.localizedDescription)")
        return
    }
    print("Left room with ID \(myRoomID)")
}

Deleting a Room

Just like leaving a room you need a reference to the room object that represents the room that the user wishes to delete. Here we again have a room object called myRoom.

1
2
3
4
5
6
7
currentUser.deleteRoom(myRoom) { error in
    guard error == nil else {
        print("Error deleting room \(room.name): \(error.localizedDescription)")
        return
    }
    print("Deleted room \(room.name)")
}

All other users that are memebers of myRoom (and connected to the Chatkit servers) will receive an event that informs them that the room has been deleted. No further updates to the room can be made and, upon subsequent connections by any user who was a member of the room at the point of its deletion, the room will no longer appear in the rooms property of the PCCurrentUser object.

Note that when a user deletes a room all of the associated messages will be deleted as well.

Updating a Room

You can update a room"s name, isPrivate, and customData properties by using the updateRoom function.

1
2
3
4
5
6
7
8
9
10
11
12
currentUser.updateRoom(
    myRoom,
    name: "a new name",
    isPrivate: false,
    customData: ["some new": "custom data"]
) { error in
    guard error == nil else {
        print("Error updating room \(room.name): \(error.localizedDescription)")
        return
    }
    print("Updated room \(room.name)")
}

Note that once customData for a room has been set you then cannot completely clear the customData for the room. You can, however, set the customData to be an empty dictionary, i.e. [:].

Getting Joinable Rooms

To fetch a list of the rooms that a user is able to join you can use the following code:

1
2
3
4
5
6
7
8
currentUser.getJoinableRooms { joinableRooms, err in
    guard err == nil else {
        print("Error getting joinable rooms: \(err!)")
        return
    }

    // do something with the joinableRooms
}

The rooms returned will be a list of the public rooms of which the currentUser is not a member of.

PCRoomDelegate

The PCRoomDelegate object you set for a room will receive events when there are relevant updates. Please be aware that if you're updating the UI from any of the methods below then you'll need to ensure that you perform the updates on the main thread.

These are the delegate functions that you can implement along with when they get called:

  • func onMessage(_ message: PCMessage) - a new message has been added to the room. Invoked when used in conjunction with subscribeToRoom
  • func onMultipartMessage(_ message: PCMultipartMessage) - a new multipart message has been added to the room. Invoked when used in conjunction with subscribeToRoomMultipart
  • func onUserStartedTyping(user: PCUser) - a user started typing in the room
  • func onUserStoppedTyping(user: PCUser) - a user stopped typing in the room
  • func onUserJoined(user: PCUser) - a user joined the room
  • func onUserLeft(user: PCUser) - a user left the room
  • func onPresenceChanged(stateChange: PCPresenceStateChange, user: PCUser) - a user came online or went offline
  • func onNewCursor(_ cursor: PCCursor) - a new cursor has been set for a member of the room
  • func onUsersUpdated() - theusers property of thePCRoom object has been updated
  • func onError(error: Error) - an error occurred in relation to room-specific events

Messages

Every message belongs to a Room and has an associated sender, which is represented by a PCUser object. The body of the message is made up of one or more parts, represented by the PCPart type each with a mime type. For example, a message might have a text part "see attachment!" as well as an image part.

Message Properties

AttributeTypeDescription
idIntThe ID assigned to the message by the Chatkit servers.
senderPCUserThe user who sent the message.
roomPCRoomThe room to which the message belongs.
parts[PCPart]The parts that make up the message.
createdAtStringThe timestamp at which the message was created.
updatedAtStringThe timestamp at which the message was last updated.

Message parts have the following properties:

AttributeTypeDescription
payloadPCMultipartPayloadCan be one of PCMultipartInlinePayload,PCMultipartURLPayload orPCMultipartAttachmentPayload

Inline payload:

AttributeTypeDescription
typeStringThe MIME type of the content of this part.
contentStringThe content of the message part.

URL payload:

AttributeTypeDescription
typeStringThe MIME type of the content of this part.
urlStringA url.

Attachment payload:

AttributeTypeDescription
typeStringThe MIME type of the content of this part.
nameString?The name of the attached file (optional)
sizeIntThe size of the attached file in bytes.
customData[String: Any]?Arbitrary custom data associated with the file at upload (optional)
url()function (accepts a completion handler that returns a String or Error)A function resolving to the URL of the attachment. Asynchronous because a fresh URL may need to be fetched from our servers.
urlExpiry()function (returns Date)Returns the Date at which the URL expires.

Sending a Message

There are two methods to send messages. In the simplest case you can send plain text messages with sendSimpleMessage. Takes the roomID to send a message to, and thetext of the message.

In the following examples a room object named myRoom is used.

1
2
3
4
5
6
7
currentUser.sendSimpleMessage(roomID: myRoom.id, text: "Hi there!") { message, error in
  guard error == nil else {
      print("Error sending message to \(room.name): \(error.localizedDescription)")
      return
  }
  print("Sent message to \(myRoom.name)")
}

For more complex messages you should use sendMultipartMessage instead. Takes the roomId to send a message to, and an array of parts.

You should use the PCPartRequest type to send multipart messages.PCPartRequest contains a payload attribute that can be one of PCPartInlineRequest,PCPartUrlRequest or PCPartAttachmentRequest. Each part consists of the following:

An inline part.

AttributeTypeDescription
typeStringThe MIME type of the content.
contentStringThe string content of the part.

A URL part.

AttributeTypeDescription
typeStringThe MIME type of the resource that the URL points to.
urlStringA URL.

An attachment part.

AttributeTypeDescription
fileDataAny Data object.
typeStringThe MIME type of the attachment. (Optional if it can be inferred from the file.)
nameString?The name of the attached file. (Optional. May be inferred from the file.)
customData[String: Any]?Arbitrary custom data to associate with the file. (Optional.)
1
2
3
4
5
6
7
8
9
10
currentUser.sendMultipartMessage(
  roomID: myRoom.id,
  parts: [PCPartRequest(.inline(PCPartInlineRequest(content: "Hello")))]
) { messageID, err in
    guard err == nil else {
        print("Error sending message \(err!.localizedDescription)")
        return
    }
    print("Successfully sent message with ID: \(messageID!)")
}

For example, sending a message consisting of one of each of the above part types.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let jsonString = "{\"hi\": \"there\"}"
let jsonData = jsonString.data(using: .utf8)

currentUser.sendMultipartMessage(
  roomID: myRoom.id,
  parts: [
    PCPartRequest(.inline(PCPartInlineRequest(content: "Hello"))),
    PCPartRequest(.url(PCPartUrlRequest(type: "image/jpeg", url: "https://t.com/img.jpeg"))),
    PCPartRequest(
      .attachment(
        PCPartAttachmentRequest(
          type: "application/json",
          file: jsonData!,
          name: "test.json",
          customData: ["key": "value"]
        )
      )
    )
  ]
) { messageID, err in
    guard err == nil else {
        print("Error sending message \(err!.localizedDescription)")
        return
    }
    print("Successfully sent message with ID: \(messageID!)")
}

Receiving New Messages

The PCRoomDelegate function you will need to implement to receive new messages is onMessage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func onMultipartMessage(_ message: PCMultipartMessage) {
  for part in parts {
    switch part.payload {
      case .inline(let payload):
        print("Received message with text: \(payload.content) from \(message.sender.debugDescription)")
      case .url(let payload):
        print("Received message with url: \(payload.url) of type \(payload.type) from \(message.sender.debugDescription)")
      case .attachment(let payload):
        payload.url() { downloadUrl, error in
          // do something with the download url
        }
    }
  }
}
Note that the onMessage method will not be invoked if thesubscribeToRoomMultipart method is used to subscribe to a room. Consequently, when using the subscribeToRoom method, theonMultipartMessage method will not be invoked.

Receiving Multipart Messages With Attachments

Once you've determined that a PCMultipartMessage contains a PCPart that carries a payload of typePCMultipartAttachmentPayload, you should get the download URL of the attachment using the url() method which takes acompletionHandler of type(String?, Error?) -> Void. This will always return a download URL that has been refreshed (which may require a network request to our servers if it has expired)

Receiving New Messages With Attachments

The below documentation is only applicable to PCMessage types.

PCMessages have an optional attachment property of type PCAttachment?. The PCAttachment type looks like this:

1
2
3
4
5
public struct PCAttachment {
    public let link: String
    public let type: String
    public let name: String
}

You can use the attachment.link to download the file, if you so wish. You can either use your own download mechanism of choice or you can use the provideddownloadAttachment function. Usage of it looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
currentUser.downloadAttachment(
    message.attachment.link,
    to: myChosenDestination,
    onSuccess: { url in
        print("Downloaded successfully to \(url.absoluteString)")
    },
    onError: { error in
        print("Failed to download attachment \(error.localizedDescription)")
    },
    progressHandler: { bytesReceived, totalBytesToReceive in
        print("Download progress: \(bytesReceived) / \(totalBytesToReceive)")
    }
)

Here myChosenDestination is an object of type PCDownloadFileDestination. This is a type based on Alamofire's DownloadFileDestination. It lets you specify where you'd like to have the download stored (upon completion).

One option for creating a PCDownloadFileDestination is to use the PCSuggestedDownloadDestination function, which is again based on an Alamofire construct: DownloadRequest.suggestedDownloadDestination. You can provide it a PPDownloadOptions object which determines whether or not the process of moving the downloaded file to the specified destination should be allowed to remove any existing files at the same path and if it should be able to create any required intermediate directories. This is expressed as an OptionSet with the following options:

  • .createIntermediateDirectories
  • .removePreviousFile

Here's an example of using PCSuggestedDownloadDestination:

1
2
3
4
5
6
7
8
9
10
11
12
13
currentUser.downloadAttachment(
    message.attachment.link,
    to: PCSuggestedDownloadDestination(options: [.createIntermediateDirectories, .removePreviousFile]),
    onSuccess: { url in
        print("Downloaded successfully to \(url.absoluteString)")
    },
    onError: { error in
        print("Failed to download attachment \(error.localizedDescription)")
    },
    progressHandler: { bytesReceived, totalBytesToReceive in
        print("Download progress: \(bytesReceived) / \(totalBytesToReceive)")
    }
)

Typing Indicators

Sometimes it's useful to be able to see if another user is typing. If this is the case for your app then you can send typing events to the Chatkit servers that will be delivered to other users in the relevant room.

Triggering a Start / Stop Typing Event

If we assume that you have a UITextView where your user will be typing then you would do the following to send an event to indicate that the current user had started typing:

1
2
3
override func textViewDidChange(textView: UITextView) {
    currentUser.typing(in: myRoom)
}

If the typing() function isn't called for 1.5 seconds, having been called at least once, then the typing indicator will timeout and all users subscribed to the room will be notified that the user has stopped typing.

Receiving Typing Indicator Events

The PCRoomDelegate functions you will need to implement to receive events when a user starts or stops typing are userStartedTyping and userStoppedTyping. Here it's assumed that you'll have access to the myRoom variable.

1
2
3
4
5
6
7
func userStartedTyping(user: PCUser) {
    print("User \(user.name) started typing in room \(myRoom.name)")
}

func userStoppedTyping(user: PCUser) {
    print("User \(user.name) stopped typing in room \(myRoom.name)")
}

User Presence

After a user successfully connects to the Chatkit, the SDK establishes a separate subscription that is reserved for user presence interactions.

As soon as the subscription is established with the Chatkit service, the user is considered online. All users that are members of at least one of the same rooms as that user will receive an update notifying them that the user has come online. The same process occurs when a user goes offline.

The presence updates are only delivered to users who also have an active presence subscription, or in other words, to users who are online themselves. Upon successful connection a user receives an initial state of the presence of all of the users with whom they share a room membership. Similarly, if a user joins a room where there are members who they didn't previously share a room membership with then the client will receive an event that informs it of the presence states of the relevant users.

This combination of initial state payloads and realtime updates means that every client is able to stay completely up-to-date with the presence state of the users that they share a room membership with.

Receiving Presence Updates

This is the delegate function that you'd implement to receive events of user presence changes when implementing them as part of a PCChatManagerDelegate or a PCRoomDelegate implementation:

The stateChange parameter is a struct that has two properties: previous and current. The previous property represents the presence state that the user was previously in and the current property represents the presence state that the user is now in.

1
2
3
4
5
6
func onPresenceChanged(
    stateChange: PCPresenceStateChange,
    user: PCUser
) {
    print("User \(user.displayName)'s presence changed to \(stateChange.current.rawValue)")
}

Read Cursors

Read cursors track how far a user has read through the messages in a room. Each read cursor belongs to a user and a room -- represented by a PCCursor object with the following properties:

  • type (PCCursorType) - the type of the cursor object, currently always
  • position (Int) - the message ID that the user has read to
  • room (PCRoom) - the room that the cursor refers to
  • updatedAt (String) - the timestamp when the cursor was last set
  • user (PCUser) - the user that the cursor belongs to

PCCursorType is an enum that currently only has one case:

1
2
3
public enum PCCursorType: Int {
    case read
}

Set a Read Cursor

Call setReadCursor on the PCCurrentUser object. Takes a position (message ID), room (a PCRoom object), and a completion handler.

1
2
3
4
5
6
7
8
9
10
currentUser.setReadCursor(
    position: 123,
    roomID: myRoom.id
) { error in
    guard error == nil else {
        print("Error setting cursor: \(error!.localizedDescription)")
        return
    }
    print("Succeeded in setting cursor")
}

Receiving Cursor Updates

To receive updates to read cursors for a room, define the onNewCursor function on the PCRoomDelegate when subscribing to a room. onNewCursor should be a function that takes a PCCursor object. e.g.

1
2
3
func onNewCursor(cursor: PCCursor) {
    print("Cursor set for \(cursor.user.displayName) at position \(cursor.position)")
}

Accessing a User's Own Read Cursors

A user's own read cursors are accessible by using the readCursor function on PCCurrentUser. The read cursors stored by the SDK will be kept up to date as new cursor values are set.

1
let myCursor = try? currentUser.readCursor(roomID: myRoom.id)

Accessing Other Users' Read Cursors

You can access the cursors of the other members of a room again by using the readCursor function on PCCurrentUser. You must provide the ID of the user whose cursor you would like to access.

1
2
3
4
let anotherCursor = try? currentUser.readCursor(
    roomID: myRoom.id,
    userID: anotherUser.id
)

Push Notifications

For your iOS app to receive push notifications, it must first register the device with APNs and then with Chatkit.

Register With APNs

Register with APNs when the application finishes launching, i.e. in its application:didFinishLaunchingWithOptions: handler:

import UIKit
 
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
 
    var window: UIWindow?
 
    let pusherChat = ChatManager(
      instanceLocator: "your-instance-locator",
      tokenProvider: yourTokenProvider,
      userID: "your-user-id"
    )
 
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        ChatManager.registerForRemoteNotifications()
 
        return true
    }
}

This is a convenience method that uses alert, sound, and badge as default authorization options.

1
ChatManager.registerForRemoteNotifications()

You can specify constants to request authorization for multiple items.

1
ChatManager.registerForRemoteNotifications(options:)

Register With Chatkit

APNs will respond with a device token identifying your app instance. This device token is passed to your application with the application:didRegisterForRemoteNotificationsWithDeviceToken: method. Chatkit requires the deviceToken in order to send push notifications to the app instance. Your app should register with Push Notifications, passing along its device token. Add a handler for it:

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
   ChatManager.registerDeviceToken(deviceToken)
}

Enable Push Notifications

Enable push notifications on the instance of the PCCurrentUser once the connection with the Chatkit service was successfully established.

chatManager.connect(delegate: yourDelegate) { currentUser, error in
  guard error == nil else {
      print("Error connecting: \(error.localizedDescription)")
      return
  }
 
  currentUser.enablePushNotifications()
}

Disable Push Notifications

1
ChatManager.disablePushNotifications()