Querying Calls

The StreamVideo SDK allows you to query calls and watch them. This allows you to build apps that display feeds of calls with real-time updates (without joining them), similar to Clubhouse.

You can query calls based on built-in fields as well as any custom field you add to the calls. Multiple filters can be combined using AND, OR logical operators, each filter can use its comparison (equality, inequality, greater than, greater or equal, etc.).

Client

You can query calls by using the client directly. By using the queryCalls(filters:sort:limit:watch:) method we will fetch the first page. The result will be an array of calls and a cursor to the next page (if one is available). Finally, we can use the next page cursor to fetch the next page.

let filters: [String: RawJSON] = ["ended_at": .nil]
let sort = [SortParamRequest.descending("created_at")]
let limit = 10

// Fetch the first page of calls 
let (firstPageCalls, secondPageCursor) = try await streamVideo.queryCalls(
    filters: filters, 
    sort: sort, 
    limit: limit
)

// Use the cursor we received from the previous call to fetch the second page
let (secondPageCalls, _) = try await streamVideo.queryCalls(next: secondPageCursor)

CallsController

In order to query calls, you need to create an instance of the CallsController, via the StreamVideo object’s method makeCallsController:

private lazy var callsController: CallsController = {
    let sortParam = CallSortParam(direction: .descending, field: .createdAt)
    let filters: [String: RawJSON] = ["type": .dictionary(["$eq": .string("audio_room")])]
    let callsQuery = CallsQuery(sortParams: [sortParam], filters: filters, watch: true)
    return streamVideo.makeCallsController(callsQuery: callsQuery)
}()

The controller requires the CallsQuery parameter, which provides sorting and filtering information for the query, as well as whether the calls should be watched.

Sort Parameters

The CallSortParam model contains two properties - direction and field. The direction can be ascending and descending, while the field can be one of the following values:

/// The sort field for the call start time.
static let startsAt: Self = "starts_at"
/// The sort field for the call creation time.
static let createdAt: Self = "created_at"
/// The sort field for the call update time.
static let updatedAt: Self = "updated_at"
/// The sort field for the call end time.
static let endedAt: Self = "ended_at"
/// The sort field for the call type.
static let type: Self = "type"
/// The sort field for the call id.
static let id: Self = "id"
/// The sort field for the call cid.
static let cid: Self = "cid"

You can provide an array of CallSortParam’s in order to have sorting by multiple fields.

Filters

The StreamVideo API supports MongoDB style queries to make it easier to fetch the required data. For example, if you want to query the channels that are of type audio_room, you would need to write the following filter:

let filters: [String: RawJSON] = ["type": .dictionary(["$eq": .string("audio_room")])]

You can find the supported operators here.

Pagination

You can encapsulate the querying calls login into an object CallsViewModel for simplicity and state management.

@MainActor
final class CallsViewModel: ObservableObject {

    @Injected(\.streamVideo) internal var streamVideo

    @Published internal var calls = [Call]()

    private var cancellables = Set<AnyCancellable>()

    private lazy var callsController: CallsController = {
        let sortParam = CallSortParam(direction: .descending, field: .createdAt)
        let filters: [String: RawJSON] = ["type": .dictionary(["$eq": .string("audio_room")])]
        let callsQuery = CallsQuery(sortParams: [sortParam], filters: filters, watch: true)
        return streamVideo.makeCallsController(callsQuery: callsQuery)
    }()

    init() {
        subscribeToCallsUpdates()
        loadCalls()
    }

    func onCallAppear(_ call: Call) {
        let index = calls.firstIndex { callData in
            callData.cId == call.cId
        }
        guard let index else { return }

        if index < calls.count - 10 {
            return
        }

        loadCalls()
    }

    func loadCalls() {
        Task {
            try await callsController.loadNextCalls()
        }
    }

    func subscribeToCallsUpdates() {
        callsController.$calls.sink { calls in
            DispatchQueue.main.async {
                self.calls = calls
            }
        }
        .store(in: &cancellables)
    }
}

You can then fetch the next calls from the specified query, by calling the loadNextCalls method. For example, if you are doing pagination in SwiftUI, you can use the onAppear modifier on each entry, and based on its index fetch the next calls.

First, in your SwiftUI view, you can call a method from your presentation layer (for example a view model), on the view item appearance:

ScrollView {
    LazyVStack {
        ForEach(callsViewModel.calls, id: \.callId) { call in
            CallView(viewFactory: viewFactory, viewModel: viewModel)
                .padding(.vertical, 4)
        }
    }
}

Next, in your presentation layer, you can check based on the call index, if the next page should be fetched:

func onCallAppear(_ call: CallData) {
    let index = calls.firstIndex { callData in
        callData.callCid == call.callCid
    }
    guard let index else { return }
    
    if index < calls.count - 10 {
        return
    }
    
    loadCalls()
}

func loadCalls() {
    Task {
        try await callsController.loadNextCalls()
    }
}

The CallsController automatically manages the cursors for the pagination. You only need to be careful not to call the loadNextCalls method before it’s necessary (like in the example above).

Watching Calls

You are able to watch real-time updates of the calls. The @Published calls variable will provide all the updates to the calls, that you can use to update your UI.

Cleanup

When you are done watching channels, you should cleanup the controller (which will stop the WS updates):

callsController.cleanUp()
© Getstream.io, Inc. All Rights Reserved.