Skip to content

Commit

Permalink
[Swift6 Migration]StreamVideoSwiftUI (#679)
Browse files Browse the repository at this point in the history
  • Loading branch information
ipavlidakis committed Feb 28, 2025
1 parent 8155783 commit 2344b35
Show file tree
Hide file tree
Showing 69 changed files with 483 additions and 409 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Foundation
#if canImport(UIKit)
import UIKit
#endif
import AVFoundation

/// An enumeration representing device orientations: portrait or landscape.
public enum StreamDeviceOrientation: Equatable, Sendable {
Expand Down Expand Up @@ -49,12 +50,21 @@ public enum StreamDeviceOrientation: Equatable, Sendable {
public var deviceOrientation: UIDeviceOrientation {
switch self {
case let .portrait(isUpsideDown):
return isUpsideDown ? UIDeviceOrientation.portraitUpsideDown : .portrait
return isUpsideDown ? .portraitUpsideDown : .portrait
case let .landscape(isLeft):
return isLeft ? UIDeviceOrientation.landscapeLeft : .landscapeRight
return isLeft ? .landscapeLeft : .landscapeRight
}
}
#endif

public var captureVideoOrientation: AVCaptureVideoOrientation {
switch self {
case let .portrait(isUpsideDown):
return isUpsideDown ? .portraitUpsideDown : .portrait
case let .landscape(isLeft):
return isLeft ? .landscapeLeft : .landscapeRight
}
}
}

/// An observable object that adapts to device orientation changes.
Expand Down
6 changes: 3 additions & 3 deletions Sources/StreamVideoSwiftUI/Appearance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,20 @@ public class Appearance {
}

/// Provider for custom localization which is dependent on App Bundle.
public static var localizationProvider: (_ key: String, _ table: String) -> String = { key, table in
public nonisolated(unsafe) static var localizationProvider: (_ key: String, _ table: String) -> String = { key, table in
Bundle.streamVideoUI.localizedString(forKey: key, value: nil, table: table)
}
}

// MARK: - Appearance + Default

public extension Appearance {
static var `default`: Appearance = .init()
static nonisolated(unsafe) var `default`: Appearance = .init()
}

/// Provides the default value of the `Appearance` class.
enum AppearanceKey: InjectionKey {
static var currentValue: Appearance = Appearance()
static nonisolated(unsafe) var currentValue: Appearance = Appearance()
}

extension InjectedValues {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public struct CallParticipantMenuAction: Identifiable {
/// The name of the icon associated with the action.
public var iconName: String
/// The closure to execute when the action is triggered, passing the participant's ID.
public var action: (String) -> Void
public var action: @MainActor @Sendable(String) -> Void
/// Optional confirmation popup data that may be presented before executing the action.
public var confirmationPopup: ConfirmationPopup?
/// A flag indicating whether the action is destructive (e.g., delete).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import StreamVideo
import SwiftUI

@MainActor
class CallParticipantsInfoViewModel: ObservableObject {
final class CallParticipantsInfoViewModel: ObservableObject {

@Injected(\.streamVideo) var streamVideo

@Published var inviteParticipantsShown = false
Expand All @@ -17,7 +17,7 @@ class CallParticipantsInfoViewModel: ObservableObject {
title: "Mute user",
requiredCapability: .muteUsers,
iconName: "speaker.slash",
action: muteAudio(for:),
action: { [weak self] in self?.muteAudio(for: $0) },
confirmationPopup: nil,
isDestructive: false
)
Expand All @@ -27,7 +27,7 @@ class CallParticipantsInfoViewModel: ObservableObject {
title: "Disable video",
requiredCapability: .muteUsers,
iconName: "video.slash",
action: muteVideo(for:),
action: { [weak self] in self?.muteVideo(for: $0) },
confirmationPopup: nil,
isDestructive: false
)
Expand All @@ -37,7 +37,7 @@ class CallParticipantsInfoViewModel: ObservableObject {
title: "Unblock user",
requiredCapability: .blockUsers,
iconName: "person.badge.plus",
action: unblock(userId:),
action: { [weak self] in self?.unblock(userId: $0) },
confirmationPopup: nil,
isDestructive: false
)
Expand All @@ -47,7 +47,7 @@ class CallParticipantsInfoViewModel: ObservableObject {
title: "Block user",
requiredCapability: .blockUsers,
iconName: "person.badge.minus",
action: block(userId:),
action: { [weak self] in self?.block(userId: $0) },
confirmationPopup: nil,
isDestructive: false
)
Expand Down Expand Up @@ -85,7 +85,7 @@ class CallParticipantsInfoViewModel: ObservableObject {
return []
}
}

private func muteAudio(for userId: String) {
executeMute(userId: userId, audio: true, video: false)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@ import StreamWebRTC
import SwiftUI

/// A custom video renderer based on RTCMTLVideoView for rendering RTCVideoTrack objects.
public class VideoRenderer: RTCMTLVideoView {
public class VideoRenderer: RTCMTLVideoView, @unchecked Sendable {

@Injected(\.thermalStateObserver) private var thermalStateObserver

private let _windowSubject: PassthroughSubject<UIWindow?, Never> = .init()
private let _superviewSubject: PassthroughSubject<UIView?, Never> = .init()
private let _frameSubject: PassthroughSubject<CGRect, Never> = .init()

var windowPublisher: AnyPublisher<UIWindow?, Never> { _windowSubject.eraseToAnyPublisher() }
var superviewPublisher: AnyPublisher<UIView?, Never> { _superviewSubject.eraseToAnyPublisher() }
var framePublisher: AnyPublisher<CGRect, Never> { _frameSubject.eraseToAnyPublisher() }
nonisolated(unsafe) var windowPublisher: AnyPublisher<UIWindow?, Never> { _windowSubject.eraseToAnyPublisher() }
nonisolated(unsafe) var superviewPublisher: AnyPublisher<UIView?, Never> { _superviewSubject.eraseToAnyPublisher() }
nonisolated(unsafe) var framePublisher: AnyPublisher<CGRect, Never> { _frameSubject.eraseToAnyPublisher() }

/// DispatchQueue for synchronizing access to the video track.
let queue = DispatchQueue(label: "video-track")

/// The associated RTCVideoTrack being rendered.
weak var track: RTCVideoTrack?
nonisolated(unsafe) weak var track: RTCVideoTrack?

var participant: CallParticipant?

Expand All @@ -48,7 +48,7 @@ public class VideoRenderer: RTCMTLVideoView {
var trackId: String? { track?.trackId }

/// The size of the renderer's view.
private var viewSize: CGSize?
private nonisolated(unsafe) var viewSize: CGSize?

/// Required initializer (unavailable for use with Interface Builder).
@available(*, unavailable)
Expand Down Expand Up @@ -130,7 +130,7 @@ extension VideoRenderer {
/// - onTrackSizeUpdate: A closure to be called when the track size is updated.
public func handleViewRendering(
for participant: CallParticipant,
onTrackSizeUpdate: @escaping (CGSize, CallParticipant) -> Void
onTrackSizeUpdate: @escaping @Sendable(CGSize, CallParticipant) -> Void
) {
if let track = participant.track {
log.info(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public struct VideoRendererView: UIViewRepresentable {
/// Extension for `VideoRendererView` to define the `Coordinator` class.
extension VideoRendererView {
/// A class to coordinate the `VideoRendererView` and manage its lifecycle.
public final class Coordinator {
public final class Coordinator: @unchecked Sendable {
/// Injected dependency for accessing the video renderer pool.
@Injected(\.videoRendererPool) private var videoRendererPool

Expand All @@ -110,14 +110,16 @@ extension VideoRendererView {
private let disposableBag = DisposableBag()

/// The video renderer managed by this coordinator.
fileprivate private(set) lazy var renderer: VideoRenderer = videoRendererPool
.acquireRenderer(size: .zero)
fileprivate let renderer: VideoRenderer

/// Initializes a new instance of the coordinator.
/// - Parameter handleRendering: A closure to handle the rendering of the video.
@MainActor
init(handleRendering: ((VideoRenderer) -> Void)?) {
self.handleRendering = handleRendering
_ = renderer
renderer = VideoRendererPool
.currentValue
.acquireRenderer(size: .zero)
setupRendererObservation()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Foundation
import StreamVideo
import SwiftUI

private final class CallEndedViewModifierViewModel: ObservableObject {
private final class CallEndedViewModifierViewModel: ObservableObject, @unchecked Sendable {

@Injected(\.streamVideo) private var streamVideo

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ struct SnapshotViewContainer<Content: View>: UIViewRepresentable {

/// A coordinator class that manages snapshot triggering and handling within the
/// `SnapshotViewContainer`.
final class SnapshotViewContainerCoordinator {
final class SnapshotViewContainerCoordinator: @unchecked Sendable {
private var trigger: SnapshotTriggering
private let snapshotHandler: (UIImage) -> Void
private let snapshotHandler: @Sendable(UIImage) -> Void
private var cancellable: AnyCancellable?

/// Weak reference to the contained `UIView`.
weak var content: UIViewType? {
didSet { captureSnapshot() }
nonisolated(unsafe) weak var content: UIViewType? {
didSet { Task { @MainActor in captureSnapshot() } }
}

/// Initializes a new `SnapshotViewContainerCoordinator` with the provided trigger and
Expand All @@ -31,20 +31,21 @@ struct SnapshotViewContainer<Content: View>: UIViewRepresentable {
/// - trigger: The `SnapshotTriggering` object responsible for triggering snapshot
/// events.
/// - snapshotHandler: A closure that handles the captured `UIImage` from snapshots.
init(trigger: SnapshotTriggering, snapshotHandler: @escaping (UIImage) -> Void) {
init(trigger: SnapshotTriggering, snapshotHandler: @escaping @Sendable(UIImage) -> Void) {
self.trigger = trigger
self.snapshotHandler = snapshotHandler

// Set up publisher to capture snapshots based on trigger events
cancellable = trigger.publisher
.removeDuplicates()
.sink { [weak self] triggered in
.sinkTask { @MainActor [weak self] triggered in
guard triggered == true else { return }
self?.captureSnapshot()
}
}

/// Captures a snapshot of the current content if trigger is true.
@MainActor
private func captureSnapshot() {
defer { trigger.binding.wrappedValue = false }
guard let content = content, trigger.binding.wrappedValue == true else { return }
Expand All @@ -53,7 +54,7 @@ struct SnapshotViewContainer<Content: View>: UIViewRepresentable {
}

private let trigger: SnapshotTriggering
private let snapshotHandler: (UIImage) -> Void
private let snapshotHandler: @Sendable(UIImage) -> Void
let contentProvider: () -> Content

/// Initializes a new `SnapshotViewContainer` with the specified trigger, snapshot handler, and
Expand All @@ -65,7 +66,7 @@ struct SnapshotViewContainer<Content: View>: UIViewRepresentable {
/// encapsulated in a UIKit view.
init(
trigger: SnapshotTriggering,
snapshotHandler: @escaping (UIImage) -> Void,
snapshotHandler: @escaping @Sendable(UIImage) -> Void,
@ViewBuilder contentProvider: @escaping () -> Content
) {
self.trigger = trigger
Expand Down Expand Up @@ -95,7 +96,7 @@ struct SnapshotViewContainer<Content: View>: UIViewRepresentable {
struct SnapshotViewModifier: ViewModifier {

var trigger: SnapshotTriggering
var snapshotHandler: (UIImage) -> Void
var snapshotHandler: @Sendable(UIImage) -> Void

/// Applies the `SnapshotViewContainer` with the specified trigger and snapshot handler to the
/// provided content.
Expand Down Expand Up @@ -134,7 +135,7 @@ extension View {
@ViewBuilder
public func snapshot(
trigger: SnapshotTriggering,
snapshotHandler: @escaping (UIImage) -> Void
snapshotHandler: @escaping @Sendable(UIImage) -> Void
) -> some View {
modifier(
SnapshotViewModifier(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import SwiftUI

/// A property wrapper type that instantiates an observable object.
@propertyWrapper @available(iOS, introduced: 13, obsoleted: 14)
public struct BackportStateObject<ObjectType: ObservableObject>: DynamicProperty
public struct BackportStateObject<ObjectType: ObservableObject & Sendable>: @preconcurrency DynamicProperty
where ObjectType.ObjectWillChangePublisher == ObservableObjectPublisher {

/// Wrapper that helps with initialising without actually having an ObservableObject yet
private class ObservedObjectWrapper: ObservableObject {
private class ObservedObjectWrapper: ObservableObject, @unchecked Sendable {
@PublishedObject var wrappedObject: ObjectType? = nil
init() {}
}
Expand Down Expand Up @@ -40,7 +40,7 @@ public struct BackportStateObject<ObjectType: ObservableObject>: DynamicProperty
public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType) {
self.thunk = thunk
}

public mutating func update() {
// Not sure what this does but we'll just forward it
_state.update()
Expand All @@ -53,7 +53,8 @@ public struct BackportStateObject<ObjectType: ObservableObject>: DynamicProperty
@propertyWrapper @available(iOS, introduced: 13, obsoleted: 14)
public struct PublishedObject<Value> {

public init(wrappedValue: Value) where Value: ObservableObject, Value.ObjectWillChangePublisher == ObservableObjectPublisher {
public init(wrappedValue: Value) where Value: ObservableObject & Sendable,
Value.ObjectWillChangePublisher == ObservableObjectPublisher {
self.wrappedValue = wrappedValue
cancellable = nil
_startListening = { futureSelf, wrappedValue in
Expand All @@ -70,7 +71,7 @@ public struct PublishedObject<Value> {
startListening(to: wrappedValue)
}

public init<V>(wrappedValue: V?) where V? == Value, V: ObservableObject,
public init<V>(wrappedValue: V?) where V? == Value, V: ObservableObject & Sendable,
V.ObjectWillChangePublisher == ObservableObjectPublisher {
self.wrappedValue = wrappedValue
cancellable = nil
Expand All @@ -93,7 +94,7 @@ public struct PublishedObject<Value> {
didSet { startListening(to: wrappedValue) }
}

public static subscript<EnclosingSelf: ObservableObject>(
public static subscript<EnclosingSelf: ObservableObject & Sendable>(
_enclosingInstance observed: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, PublishedObject>
Expand All @@ -108,7 +109,7 @@ public struct PublishedObject<Value> {
}
}

public static subscript<EnclosingSelf: ObservableObject>(
public static subscript<EnclosingSelf: ObservableObject & Sendable>(
_enclosingInstance observed: EnclosingSelf,
projected wrappedKeyPath: KeyPath<EnclosingSelf, Publisher>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, PublishedObject>
Expand All @@ -124,11 +125,12 @@ public struct PublishedObject<Value> {
init() {}
}

private func setParent<Parent: ObservableObject>(_ parentObject: Parent)
private func setParent<Parent: ObservableObject & Sendable>(_ parentObject: Parent)
where Parent.ObjectWillChangePublisher == ObservableObjectPublisher {
guard parent.objectWillChange == nil else { return }
parent.objectWillChange = { [weak parentObject] in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
Task { @MainActor [weak parentObject] in
try? await Task.sleep(nanoseconds: 10_000_000)
parentObject?.objectWillChange.send()
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamVideoSwiftUI/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class Utils {

/// Provides the default value of the `Utils` class.
public enum UtilsKey: InjectionKey {
public static var currentValue: Utils = .init()
public nonisolated(unsafe) static var currentValue: Utils = .init()
}

extension InjectedValues {
Expand Down
Loading

0 comments on commit 2344b35

Please sign in to comment.