Core module

Nabla iOS Messaging core module

📘

This guide is about the data/domain layer

This page describes how to to query the patient data, allowing you to build your own UI. If you'd rather use our built-in customisable UI components, check out the Messaging UI Components page.

Watchers and cached data

Our messaging core SDK has a cache layer that persists the latest fetched data from the network to a disk cache, allowing to retrieve these data without network at any time.

Every watcher returns a Response object that contains 3 things:

  • A data property that contains the data itself
  • A isDataFresh boolean property that indicates if the emitted data is fresh or not.
  • A refreshingState property that has 3 different states:
    • refreshing: the data emitted comes from the cache, we are refreshing it in background. isDataFresh should be false when that happens, and you can expect another emit following with the result of the background refresh.
    • refreshed: the data emitted comes from the network or is local only and no follow-up attempt to refresh it will be made. isDataFresh should be true when that happens.
    • failed: the background refresh of the data has failed and no follow-up attempt to refresh will be made. isDataFresh should be false when that happens.

🚧

If no cached data are available and the network fetch fails, the AnyPublisher will terminate with an error, so you need to implement sink(receivedCompletion:receiveValue:) even for cached flows.
For the vast majority of apps, showing cached data is a good user experience, especially if you're showing a hint to the user when it's not fresh. If your app needs to show only fresh data, you can filter items that aren't fresh and you can relaunch the watcher to trigger a new network fetch if needed.

Watch patient conversations list

You can watch the list of conversations a user has access to. The callback closure will be called every time there's a change in those conversations and will always return all the data (you don't need to accumulate it in your own properties):

import Combine // Needed for `AnyCancellable`

private var conversationsWatcher: AnyCancellable?
private var conversations: PaginatedList<Conversation>?

self.conversationsWatcher = NablaMessagingClient.shared.watchConversations()
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { completion in
        print("Completion \(completion)")
     }, receiveValue: { [weak self] response in
        print("User has \(response.data.elements.count) conversations")
        self?.conversations = response.data
    })

watchConversations() returns some AnyPublisher<Response<PaginatedList<Conversation>>, NablaError>. For more details about AnyPublisher<>, visit Apple's documentation.

The sink(receiveCompletion:receiveValue) method returns an AnyCancellable. You must retain it if you want the receiveValue closure to keep getting called. If you release them, they will automatically cancel() themselves and receiveValue will never be called again.

PaginatedList provides a loadMore() method that will fetch more items in the corresponding list. If loadMore is nil, there are no more items to load. This method does not return the new items, instead, the receiveValue closure used for watchConversations().sink() will be called again with the added items. Use it as your only source of truth (you don't need to accumulate conversations in your own properties).

If the first network call made by watchConversatins() fails, it will emit an error with the receiveCompletion closure. The AnyPublisher will be terminated, neither receiveCompletion nor receiveValue will be called again. You must call watchConversations() again to get a new AnyPublisher.

If the loadMore() fails, it will throw an error, but it won't terminate the AnyPublisher from watchConversations().

private var conversations: PaginatedList<Conversation>?

Task {
    do {
        guard let loadMore = conversations?.loadMore else {
            print("All items have already been loaded")
            return
        }
        try await loadMore()
        print("More conversations successfully loaded")
    } catch {
        print("Error \(error)")
    }
}

Create a new conversation

If you want your Patient to be able to start a new conversation, you can allow to create it into your app directly:

let conversation = NablaMessagingClient.shared.startConversation()
print("New conversation created: \(conversation)")

Note that this conversation is created locally on the device first and will be created server side once the Patient sends a first message in it.

Watch a conversation

To watch the messages of a conversation, the same pagination principles applies:

private var itemsWatcher: AnyCancellable?

self.itemsWatcher = NablaMessagingClient.shared.watchItems(
  ofConversationWithId: conversationId
)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
    print("Completion \(completion)")
}, receiveValue: { response in
    print("Received \(response.data.elements.count) messages")
})

You can also watch for a conversation details update (like a Provider typing status):

private var conversationWatcher: AnyCancellable?

self.conversationWatcher = NablaMessagingClient.shared.watchConversation(withId: conversationId)
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { completion in
        print("Completion \(completion)")
    }, receiveValue: { response in
        print("Conversation updated: \(response.data)")
    })

Send a new message

Creating and sending your message

The user can send a message in an existing conversation:

Task {
    do {
        let messageInput  = MessageInput.text(content: "Hello world!")
        try await NablaMessagingClient.shared.sendMessage(
            messageInput,
            replyingToMessageWithId: nil,
            inConversationWithId: conversationId
        )
        print("Message successfully sent")
    } catch {
        print("Error \(error)")
    }
}

You can send 5 types of messages:

  • Text
  • Video
  • Image
  • Document
  • Audio

Handling failure

If sending the message fails, the message will still be included in watchConversationItems but its state will be ConversationItemState.failed. You can then retry sending it:

Task {
    do {
        try await NablaMessagingClient.shared.retrySending(
          itemWithId: myMessage.id,
          inConversationWithId: conversationId
        )
        print("Message successfully sent")
    } catch {
        print("Error \(error)")
    }
}

Automatic data updates

Methods like watchConversations(), watchConverversation() or watchItems(ofConversationWithId:) return some AnyPublisher that will emit new values everytime the data changes.

The SDK relies on websockets to receive updates from the server. When your application is killed, or is sent to background, those websockets will be disconnected and the data will be updated when the SDK can access the network again.

It is a good practice to re-fetch the data for watchers that are currently active when your application enters the foreground. You can register a RefetchTrigger that will do this for you on every Watcher. We provide NotificationRefetchTrigger for this specific scenario, and it can be used like this:

NablaClient.shared.addRefetchTriggers(
    NotificationRefetchTrigger(name: UIApplication.willEnterForegroundNotification)
)

📘

When using NablaMessagingUI, this step is done automatically for you.