Skip to content

Commit

Permalink
Small improvements around callEnded and runtime warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
ipavlidakis committed May 9, 2024
1 parent fed3a58 commit f24516e
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,12 @@ fileprivate func content() {

container {
struct CallContainer: View {
@Injected(\.streamVideo) var streamVideo

var body: some View {
YourRootView()
.modifier(CallModifier(viewModel: viewModel))
.onCallEnded { call, dismiss in
.onCallEnded(additionalPresentationValidator: { $0?.state.createdBy?.id == streamVideo.user.id }) { call, dismiss in
if let call {
DemoFeedbackView(call, dismiss: dismiss)
}
Expand Down
26 changes: 20 additions & 6 deletions Sources/StreamVideo/Call.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
await state.update(from: response)
let updated = await state.callSettings
updateCallSettingsManagers(with: updated)
streamVideo.state.activeCall = self
Task { @MainActor in
streamVideo.state.activeCall = self
}
return response
})
}
Expand All @@ -141,7 +143,9 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
)
await state.update(from: response)
if ring {
streamVideo.state.ringingCall = self
Task { @MainActor in
streamVideo.state.ringingCall = self
}
}
return response
}
Expand Down Expand Up @@ -199,7 +203,9 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
)
await state.update(from: response)
if ring {
streamVideo.state.ringingCall = self
Task { @MainActor in
streamVideo.state.ringingCall = self
}
}
return response.call
}
Expand All @@ -226,7 +232,9 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
public func reject() async throws -> RejectCallResponse {
let response = try await coordinatorClient.rejectCall(type: callType, id: callId)
if streamVideo.state.ringingCall?.cId == cId {
streamVideo.state.ringingCall = nil
Task { @MainActor in
streamVideo.state.ringingCall = nil
}
}
return response
}
Expand Down Expand Up @@ -361,8 +369,14 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
cancellables.removeAll()
eventHandlers.removeAll()
callController.cleanUp()
streamVideo.state.ringingCall = nil
streamVideo.state.activeCall = nil
Task { @MainActor in
if streamVideo.state.ringingCall?.cId == cId {
streamVideo.state.ringingCall = nil
}
if streamVideo.state.activeCall?.cId == cId {
streamVideo.state.activeCall = nil
}
}
}

/// Starts noise cancellation asynchronously.
Expand Down
24 changes: 16 additions & 8 deletions Sources/StreamVideo/CallKit/CallKitService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
let call = streamVideo.call(callType: callType, callId: callId)
let callState = try await call.get()

if streamVideo.state.ringingCall?.cId != call.cId {
Task { @MainActor in
streamVideo.state.ringingCall = call
}
}

if !checkIfCallWasHandled(callState: callState), state == .idle {
setUpRingingTimer(for: callState)
state = .joining
Expand Down Expand Up @@ -233,8 +239,8 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
do {
log.debug("Answering VoIP incoming call with callId:\(callId) callType:\(callType).")
call = streamVideo.call(callType: callType, callId: callId)
try await call?.accept()
try await call?.join()
try await call?.accept()
state = .inCall
action.fulfill()
} catch {
Expand Down Expand Up @@ -272,7 +278,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
///
/// - Parameter transaction: The transaction to be requested.
/// - Throws: An error if the request fails.
public func requestTransaction(
open func requestTransaction(
_ action: CXAction
) async throws {
try await callController.requestTransaction(with: action)
Expand All @@ -282,7 +288,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
///
/// - Parameter callState: The state of the call.
/// - Returns: A boolean value indicating whether the call was handled.
public func checkIfCallWasHandled(callState: GetCallResponse) -> Bool {
open func checkIfCallWasHandled(callState: GetCallResponse) -> Bool {
guard let streamVideo else {
log.warning("CallKit operation:\(#function) cannot be fulfilled because StreamVideo is nil.")
return false
Expand All @@ -300,7 +306,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
/// Sets up a ringing timer for the call.
///
/// - Parameter callState: The state of the call.
public func setUpRingingTimer(for callState: GetCallResponse) {
open func setUpRingingTimer(for callState: GetCallResponse) {
createdBy = callState.call.createdBy.toUser
let timeout = TimeInterval(callState.call.settings.ring.autoCancelTimeoutMs / 1000)
ringingTimerCancellable = Foundation.Timer.publish(
Expand All @@ -315,6 +321,12 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
}
}

/// A method that's being called every time the StreamVideo instance is getting updated.
/// - Parameter streamVideo: The new StreamVideo instance (nil if none)
open func didUpdate(_ streamVideo: StreamVideo?) {
subscribeToCallEvents()
}

// MARK: - Private helpers

private func subscribeToCallEvents() {
Expand Down Expand Up @@ -382,10 +394,6 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {

return update
}

private func didUpdate(_ streamVideo: StreamVideo?) {
subscribeToCallEvents()
}
}

extension CallKitService: InjectionKey {
Expand Down
6 changes: 4 additions & 2 deletions Sources/StreamVideo/StreamVideo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
oldValue?.leave()
}
if ringingCall != nil {
ringingCall = nil
Task { @MainActor in
ringingCall = nil
}
}
}
}
Expand Down Expand Up @@ -654,8 +656,8 @@ extension StreamVideo: WSEventsSubscriber {
)
executeOnMain {
call.state.update(from: ringEvent)
self.state.ringingCall = call
}
self.state.ringingCall = call
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@ private final class CallEndedViewModifierViewModel: ObservableObject {
@available(iOS 14.0, *)
private struct CallEndedViewModifier<Subview: View>: ViewModifier {

private var additionalPresentationValidator: (Call?) -> Bool
private var subviewProvider: (Call?, @escaping () -> Void) -> Subview

@StateObject private var viewModel: CallEndedViewModifierViewModel

init(
additionalPresentationValidator: @escaping (Call?) -> Bool,
@ViewBuilder subviewProvider: @escaping (Call?, @escaping () -> Void) -> Subview
) {
self.additionalPresentationValidator = additionalPresentationValidator
self.subviewProvider = subviewProvider
_viewModel = .init(wrappedValue: .init())
}
Expand All @@ -46,6 +49,7 @@ private struct CallEndedViewModifier<Subview: View>: ViewModifier {
.sheet(isPresented: $viewModel.isPresentingSubview) {
subviewProvider(viewModel.lastCall) {
viewModel.lastCall = nil
viewModel.maxParticipantsCount = 0
viewModel.isPresentingSubview = false
}
}
Expand All @@ -56,7 +60,9 @@ private struct CallEndedViewModifier<Subview: View>: ViewModifier {
)

switch (call, viewModel.lastCall, viewModel.isPresentingSubview) {
case (nil, let activeCall, false) where activeCall != nil && viewModel.maxParticipantsCount > 1:
case (nil, let activeCall, false)
where activeCall != nil && viewModel
.maxParticipantsCount > 1 && additionalPresentationValidator(viewModel.lastCall):
/// The following presentation criteria are required:
/// - The activeCall was ended.
/// - Participants, during call's duration, grew to more than one.
Expand Down Expand Up @@ -99,14 +105,18 @@ private struct CallEndedViewModifier<Subview: View>: ViewModifier {
@available(iOS, introduced: 13, obsoleted: 14)
private struct CallEndedViewModifier_iOS13<Subview: View>: ViewModifier {

private var additionalPresentationValidator: (Call?) -> Bool
private var subviewProvider: (Call?, @escaping () -> Void) -> Subview

@BackportStateObject private var viewModel: CallEndedViewModifierViewModel = .init()
@BackportStateObject private var viewModel: CallEndedViewModifierViewModel

init(
additionalPresentationValidator: @escaping (Call?) -> Bool,
@ViewBuilder subviewProvider: @escaping (Call?, @escaping () -> Void) -> Subview
) {
self.additionalPresentationValidator = additionalPresentationValidator
self.subviewProvider = subviewProvider
_viewModel = .init(wrappedValue: .init())
}

func body(content: Content) -> some View {
Expand All @@ -124,7 +134,9 @@ private struct CallEndedViewModifier_iOS13<Subview: View>: ViewModifier {
)

switch (call, viewModel.lastCall, viewModel.isPresentingSubview) {
case (nil, let activeCall, false) where activeCall != nil && viewModel.maxParticipantsCount > 1:
case (nil, let activeCall, false)
where activeCall != nil && viewModel
.maxParticipantsCount > 1 && additionalPresentationValidator(viewModel.lastCall):
/// The following presentation criteria are required:
/// - The activeCall was ended.
/// - Participants, during call's duration, grew to more than one.
Expand Down Expand Up @@ -172,21 +184,27 @@ extension View {
/// - Active call was ended.
/// - Participants, during call's duration, grew to more than one.
///
/// - Parameter content: A viewBuilder that returns the modal's content. The viewModifier
/// - Parameters:
/// - additionalPresentationValidator: A closure that can be used to provide additional
/// validation rules for presentation. The modifier will inject the last available call when calling.
/// - content: A viewBuilder that returns the modal's content. The viewModifier
/// will provide a dismiss closure that can be called from the content to close the modal.
@ViewBuilder
public func onCallEnded(
additionalPresentationValidator: @escaping (Call?) -> Bool = { _ in true },
@ViewBuilder _ content: @escaping (Call?, @escaping () -> Void) -> some View
) -> some View {
if #available(iOS 14.0, *) {
modifier(
CallEndedViewModifier(
additionalPresentationValidator: additionalPresentationValidator,
subviewProvider: content
)
)
} else {
modifier(
CallEndedViewModifier_iOS13(
additionalPresentationValidator: additionalPresentationValidator,
subviewProvider: content
)
)
Expand Down
24 changes: 18 additions & 6 deletions Sources/StreamVideoSwiftUI/CallingViews/CallConnectingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,32 @@
import StreamVideo
import SwiftUI

struct CallConnectingView<CallControls: View, CallTopView: View>: View {
public struct CallConnectingView<CallControls: View, CallTopView: View>: View {
@Injected(\.streamVideo) var streamVideo

@Injected(\.colors) var colors
@Injected(\.fonts) var fonts
@Injected(\.images) var images
@Injected(\.utils) var utils

var outgoingCallMembers: [Member]
var title: String
var callControls: CallControls
var callTopView: CallTopView
public var outgoingCallMembers: [Member]
public var title: String
public var callControls: CallControls
public var callTopView: CallTopView

var body: some View {
public init(
outgoingCallMembers: [Member],
title: String,
callControls: CallControls,
callTopView: CallTopView
) {
self.outgoingCallMembers = outgoingCallMembers
self.title = title
self.callControls = callControls
self.callTopView = callTopView
}

public var body: some View {
ZStack {
VStack(spacing: 16) {
callTopView
Expand Down
13 changes: 10 additions & 3 deletions docusaurus/docs/iOS/05-ui-cookbook/18-call-quality-rating.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,20 @@ struct DemoStarRatingView: View {
}
```

With the FeedbackView declared, the next step is to find a way to inject the View in the call's lifecycle in order to be presented to the user at the right time. To simplify this step, the Swift Video SDK provides the `onCallEnded` ViewModifier . The modifier accepts a closure with inputs an optional `Call` object and a dismiss closure.
With the FeedbackView declared, the next step is to find a way to inject the View in the call's lifecycle in order to be presented to the user at the right time. To simplify this step, the Swift Video SDK provides the `onCallEnded` ViewModifier . The modifier accepts two closures with inputs an optional `Call` object while onlye the second one also receives a dismiss closure.

The first closure can be used to provide additional logic when calculating the decision to present or not the modal. The second closure is a `ViewBuilder` that will be called to provide the modal's content.

The example below, presents the feedback modal **only** to the call's creator.

```swift
struct CallContainer: View {
struct CallContainer: View {
@Injected(\.streamVideo) var streamVideo

var body: some View {
YourRootView()
.modifier(CallModifier(viewModel: viewModel))
.onCallEnded { call, dismiss in
.onCallEnded(additionalPresentationValidator: { call in call?.state.createdBy?.id == streamVideo.user.id }) { call, dismiss in
if let call {
DemoFeedbackView(call, dismiss: dismiss)
}
Expand All @@ -203,6 +209,7 @@ struct CallContainer: View {
The ViewModifier observes the Call's lifecycle and looks for the following triggering criteria:
- Once the active call has ended
- If the max number of joined participants, during call's duration, grew to more than one
- It will evaluate the `additionalPresentationValidator`

Then the modifier will trigger the provided closure and will expect a view that will presented inside the modal.

Expand Down

0 comments on commit f24516e

Please sign in to comment.