This commit is contained in:
启星
2025-08-08 10:49:36 +08:00
parent 6400cf78bb
commit b5ce3d580a
8780 changed files with 978183 additions and 0 deletions

View File

@@ -0,0 +1,501 @@
//
// GroupCallVideoCell.swift
// TUICallKit
//
// Created by vincepzhang on 2023/3/2.
//
import Foundation
import TUICallEngine
import SnapKit
class GroupCallVideoCell: UICollectionViewCell {
let selfUserStatusObserver = Observer()
let remoteUserStatusObserver = Observer()
let selfUserVideoAvailableObserver = Observer()
let remoteUserVideoAvailableObserver = Observer()
let isCameraOpenObserver = Observer()
let isMicMuteObserver = Observer()
let remotePlayoutVolumeObserver = Observer()
let selfPlayoutVolumeObserver = Observer()
let isShowLargeViewUserIdObserver = Observer()
let enableBlurBackgroundObserver = Observer()
let selfNetworkQualityObserver = Observer()
let remoteNetworkQualityObserver = Observer()
private var viewModel = GroupCallVideoCellViewModel(remote: User())
private var user: User = User()
private var isSelf: Bool = false
private var renderView = VideoView()
private let titleLabel = {
let titleLabel = UILabel(frame: CGRect.zero)
titleLabel.textColor = UIColor.t_colorWithHexString(color: "FFFFFF")
titleLabel.font = UIFont.systemFont(ofSize: 12)
titleLabel.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
return titleLabel
}()
private let loadingView = {
let view = GroupLoadingView()
view.backgroundColor = .clear
return view
}()
private let avatarImageView = {
let imageView = UIImageView(frame: CGRect.zero)
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
return imageView
}()
private let micImageView = {
let imageView = UIImageView(frame: CGRect.zero)
imageView.contentMode = .scaleAspectFill
if let image = TUICallKitCommon.getBundleImage(name: "icon_mic_off") {
imageView.image = image
}
return imageView
}()
private let volumeImageView = {
let imageView = UIImageView(frame: CGRect.zero)
imageView.contentMode = .scaleAspectFill
if let image = TUICallKitCommon.getBundleImage(name: "icon_volume") {
imageView.image = image
}
return imageView
}()
private let networkQualityView = {
let imageView = UIImageView(frame: CGRect.zero)
imageView.contentMode = .scaleAspectFill
if let image = TUICallKitCommon.getBundleImage(name: "group_network_low_quality") {
imageView.image = image
}
return imageView
}()
lazy var switchCameraBtn: GroupCallVideoCustomButton = {
let btn = GroupCallVideoCustomButton(type: .system)
btn.contentMode = .scaleAspectFit
if let image = TUICallKitCommon.getBundleImage(name: "group_switch_camera") {
btn.setBackgroundImage(image, for: .normal)
}
btn.addTarget(self, action: #selector(switchCameraTouchEvent(sender: )), for: .touchUpInside)
return btn
}()
lazy var virtualBackgroundBtn: GroupCallVideoCustomButton = {
let btn = GroupCallVideoCustomButton(type: .system)
btn.contentMode = .scaleAspectFit
let imageName = TUICallState.instance.enableBlurBackground.value ? "group_virtual_background_on" : "group_virtual_background_off"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
btn.setBackgroundImage(image, for: .normal)
}
btn.addTarget(self, action: #selector(virtualBackgroundTouchEvent(sender: )), for: .touchUpInside)
return btn
}()
override init(frame: CGRect) {
super.init(frame: frame)
constructViewHierarchy()
activateConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
if !loadingView.isHidden {
loadingView.startAnimating()
}
}
deinit {
if isSelf {
TUICallState.instance.selfUser.value.callStatus.removeObserver(selfUserStatusObserver)
TUICallState.instance.selfUser.value.videoAvailable.removeObserver(selfUserVideoAvailableObserver)
TUICallState.instance.selfUser.value.playoutVolume.removeObserver(selfPlayoutVolumeObserver)
TUICallState.instance.enableBlurBackground.removeObserver(enableBlurBackgroundObserver)
TUICallState.instance.selfUser.value.networkQualityReminder.removeObserver(selfNetworkQualityObserver)
} else {
viewModel.remoteUserStatus.removeObserver(remoteUserStatusObserver)
viewModel.remoteUserVideoAvailable.removeObserver(remoteUserVideoAvailableObserver)
viewModel.remoteIsShowLowNetworkQuality.removeObserver(remoteNetworkQualityObserver)
if TUICallState.instance.showVirtualBackgroundButton {
viewModel.remoteUserVolume.removeObserver(remotePlayoutVolumeObserver)
}
}
TUICallState.instance.showLargeViewUserId.removeObserver(isMicMuteObserver)
for view in contentView.subviews {
view.removeFromSuperview()
}
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
if !loadingView.isHidden {
loadingView.startAnimating()
}
}
func constructViewHierarchy() {
contentView.addSubview(renderView)
contentView.addSubview(avatarImageView)
contentView.addSubview(loadingView)
contentView.addSubview(titleLabel)
contentView.addSubview(micImageView)
contentView.addSubview(volumeImageView)
contentView.addSubview(networkQualityView)
contentView.addSubview(switchCameraBtn)
if TUICallState.instance.showVirtualBackgroundButton {
contentView.addSubview(virtualBackgroundBtn)
}
}
func activateConstraints() {
renderView.snp.makeConstraints { make in
make.edges.equalTo(self.contentView)
}
avatarImageView.snp.makeConstraints { make in
make.edges.equalTo(self.contentView)
}
loadingView.snp.makeConstraints { make in
make.center.equalTo(self)
make.width.height.equalTo(40.scaleWidth())
}
if viewModel.isShowLargeViewUserId.value {
activateLargeViewConstraints()
} else {
activateSmallViewConstraints()
}
}
func activateLargeViewConstraints() {
titleLabel.snp.remakeConstraints { make in
make.leading.equalTo(self.contentView).offset(8.scaleWidth())
make.height.equalTo(24.scaleWidth())
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
}
micImageView.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.leading.equalTo(titleLabel.snp.trailing).offset(8.scaleWidth())
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
}
volumeImageView.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.leading.equalTo(titleLabel.snp.trailing).offset(8.scaleWidth())
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
}
if TUICallState.instance.showVirtualBackgroundButton {
virtualBackgroundBtn.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.bottom.trailing.equalTo(self.contentView).offset(-8.scaleWidth())
}
switchCameraBtn.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.centerY.equalTo(virtualBackgroundBtn)
make.trailing.equalTo(virtualBackgroundBtn.snp.leading).offset(-10.scaleWidth())
}
} else {
switchCameraBtn.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.bottom.trailing.equalTo(self.contentView).offset(-8.scaleWidth())
}
}
networkQualityView.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.centerY.equalTo(switchCameraBtn)
make.trailing.equalTo(switchCameraBtn.snp.leading).offset(-10.scaleWidth())
}
}
func activateSmallViewConstraints() {
volumeImageView.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.leading.equalTo(self.contentView).offset(8.scaleWidth())
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
}
networkQualityView.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
if self.micImageView.isHidden {
make.trailing.equalTo(self.contentView).offset(-8.scaleWidth())
} else {
make.trailing.equalTo(self.micImageView.snp.leading).offset(-8.scaleWidth())
}
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
}
micImageView.snp.remakeConstraints { make in
make.width.height.equalTo(24.scaleWidth())
make.trailing.equalTo(self.contentView).offset(-8.scaleWidth())
make.bottom.equalTo(self.contentView).offset(-8.scaleWidth())
}
}
func initCell(user: User) {
self.user = user
viewModel = GroupCallVideoCellViewModel(remote: user)
isSelf = TUICallState.instance.selfUser.value.id.value == user.id.value ? true : false
initWaitingUI()
registerObserveState()
}
// MARK: Action Event
@objc func switchCameraTouchEvent(sender: UIButton) {
CallEngineManager.instance.switchCamera()
}
@objc func virtualBackgroundTouchEvent(sender: UIButton ) {
CallEngineManager.instance.setBlurBackground()
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
callStatusChanged()
videoAvailableChanged()
isCameraOpenChanged()
volumeChanged()
isShowLargeViewUserIdChanged()
isMicMuteChanged()
enableBlurBackgroundChanged()
networkQualityChanged()
}
func callStatusChanged() {
if isSelf {
TUICallState.instance.selfUser.value.callStatus.addObserver(selfUserStatusObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateSelfUserUI()
})
} else {
viewModel.remoteUserStatus.addObserver(remoteUserStatusObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateRemoteUserCellUI()
})
}
}
func videoAvailableChanged() {
if isSelf {
TUICallState.instance.selfUser.value.videoAvailable.addObserver(selfUserVideoAvailableObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateSelfUserUI()
})
} else {
viewModel.remoteUserVideoAvailable.addObserver(remoteUserVideoAvailableObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateRemoteUserCellUI()
})
}
}
func volumeChanged() {
if isSelf {
TUICallState.instance.selfUser.value.playoutVolume.addObserver(selfPlayoutVolumeObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.volumeImageView.isHidden = newValue == 0 ? true : false
})
} else {
viewModel.remoteUserVolume.addObserver(remotePlayoutVolumeObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.volumeImageView.isHidden = newValue == 0 ? true : false
})
}
}
func isCameraOpenChanged() {
if isSelf {
TUICallState.instance.isCameraOpen.addObserver(isCameraOpenObserver) { [weak self] newValue, _ in
guard let self = self else { return }
self.updateSelfUserUI()
}
}
}
func isShowLargeViewUserIdChanged() {
viewModel.isShowLargeViewUserId.addObserver(isShowLargeViewUserIdObserver) { [weak self] newValue, _ in
guard let self = self else { return }
if newValue {
self.updateLargeViewUI()
} else {
self.updateSmallViewUI()
}
}
}
func isMicMuteChanged() {
if isSelf {
TUICallState.instance.isMicMute.addObserver(isMicMuteObserver) { [weak self] newValue, _ in
guard let self = self else { return }
self.micImageView.isHidden = !newValue
}
}
}
func enableBlurBackgroundChanged () {
if isSelf && TUICallState.instance.showVirtualBackgroundButton {
TUICallState.instance.enableBlurBackground.addObserver(enableBlurBackgroundObserver) { [weak self] newValue, _ in
guard let self = self else { return }
let imageName = TUICallState.instance.enableBlurBackground.value ? "group_virtual_background_on" : "group_virtual_background_off"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
self.virtualBackgroundBtn.setBackgroundImage(image, for: .normal)
}
}
}
}
func networkQualityChanged () {
if isSelf {
TUICallState.instance.selfUser.value.networkQualityReminder.addObserver(selfNetworkQualityObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.networkQualityView.isHidden = !newValue
})
} else {
viewModel.remoteIsShowLowNetworkQuality.addObserver(remoteNetworkQualityObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.networkQualityView.isHidden = !newValue
})
}
}
func updateLargeViewUI() {
if isSelf {
switchCameraBtn.isHidden = !TUICallState.instance.isCameraOpen.value
virtualBackgroundBtn.isHidden = !TUICallState.instance.isCameraOpen.value || !TUICallState.instance.showVirtualBackgroundButton
titleLabel.isHidden = false
micImageView.isHidden = !TUICallState.instance.isMicMute.value
} else {
switchCameraBtn.isHidden = true
virtualBackgroundBtn.isHidden = true
titleLabel.isHidden = false
micImageView.isHidden = true
}
activateLargeViewConstraints()
}
func updateSmallViewUI() {
switchCameraBtn.isHidden = true
virtualBackgroundBtn.isHidden = true
titleLabel.isHidden = true
if isSelf {
micImageView.isHidden = !TUICallState.instance.isMicMute.value
} else {
micImageView.isHidden = true
}
activateSmallViewConstraints()
}
// MARK: Update UI
func initWaitingUI() {
titleLabel.text = User.getUserDisplayName(user: viewModel.remoteUser)
setUserAvatar()
if isSelf {
initSelfUserUI()
} else {
updateRemoteUserCellUI()
}
}
func initSelfUserUI() {
hiddenAllSubView()
micImageView.isHidden = !TUICallState.instance.isMicMute.value
if !VideoFactory.instance.isExistVideoView(videoView: renderView) {
renderView = VideoFactory.instance.createVideoView(userId: TUICallState.instance.selfUser.value.id.value, frame: CGRect.zero)
renderView.isUserInteractionEnabled = false
}
if TUICallState.instance.isCameraOpen.value == true {
renderView.isHidden = false
switchCameraBtn.isHidden = false
CallEngineManager.instance.openCamera(videoView: renderView)
} else {
renderView.isHidden = true
avatarImageView.isHidden = false
}
}
func updateSelfUserUI() {
hiddenAllSubView()
micImageView.isHidden = !TUICallState.instance.isMicMute.value
if TUICallState.instance.isCameraOpen.value == true {
renderView.isHidden = false
switchCameraBtn.isHidden = false
virtualBackgroundBtn.isHidden = !TUICallState.instance.showVirtualBackgroundButton
} else {
switchCameraBtn.isHidden = true
virtualBackgroundBtn.isHidden = true
avatarImageView.isHidden = false
}
}
func updateRemoteUserCellUI() {
hiddenAllSubView()
if viewModel.remoteUserStatus.value == .waiting {
loadingView.isHidden = false
loadingView.startAnimating()
avatarImageView.isHidden = false
} else if viewModel.remoteUserStatus.value == .accept {
CallEngineManager.instance.startRemoteView(user: viewModel.remoteUser, videoView: renderView)
if viewModel.remoteUserVideoAvailable.value == true {
renderView.isUserInteractionEnabled = false
renderView.isHidden = false
} else {
avatarImageView.isHidden = false
}
}
}
func hiddenAllSubView() {
renderView.isHidden = true
loadingView.isHidden = true
loadingView.stopAnimating()
avatarImageView.isHidden = true
micImageView.isHidden = true
volumeImageView.isHidden = true
switchCameraBtn.isHidden = true
virtualBackgroundBtn.isHidden = true
networkQualityView.isHidden = true
}
// MARK: Private Method
func setUserAvatar() {
let userIcon: UIImage? = TUICallKitCommon.getBundleImage(name: "default_user_icon")
if viewModel.remoteUser.avatar.value == "" {
guard let image = userIcon else { return }
avatarImageView.image = image
} else {
avatarImageView.sd_setImage(with: URL(string: viewModel.remoteUser.avatar.value), placeholderImage: userIcon)
}
}
}
class GroupCallVideoCustomButton: UIButton {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let expandedBounds = bounds.insetBy(dx: -4.scaleWidth(), dy: -4.scaleWidth())
return expandedBounds.contains(point) ? self : nil
}
}

View File

@@ -0,0 +1,265 @@
//
// GroupCallVideoFlowLayout.swift
// TUICallKit
//
// Created by noah on 2023/11/1.
//
import Foundation
import UIKit
public var showLargeViewIndex: Int = -1
enum MosaicSegmentStyle {
case fullWidth
case fiftyFifty
case oneThird
case threeOneThirds
case twoThirdsOneThirdRight
case twoThirdsOneThirdCenter
case oneThirdTwoThirds
}
class GroupCallVideoFlowLayout: UICollectionViewFlowLayout {
private var deletingIndexPaths = [IndexPath]()
private var insertingIndexPaths = [IndexPath]()
private var contentBounds = CGRect.zero
private var cachedAttributes = [UICollectionViewLayoutAttributes]()
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
cachedAttributes.removeAll()
contentBounds = CGRect(origin: .zero, size: collectionView.bounds.size)
let count = collectionView.numberOfItems(inSection: 0)
var currentIndex = 0
var segment: MosaicSegmentStyle = getSegment(count: count, currentIndex: currentIndex)
let cvWidth = collectionView.bounds.size.width
var lastFrame: CGRect = (count != 2 || showLargeViewIndex >= 0) ? .zero : CGRect(x: 0, y: cvWidth / 5, width: 0, height: 0)
while currentIndex < count {
var segmentRects = [CGRect]()
switch segment {
case .fullWidth:
segmentRects = [CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth)]
case .fiftyFifty:
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth / 2)
let horizontalSlices = segmentFrame.dividedIntegral(fraction: 0.5, from: .minXEdge)
segmentRects = [horizontalSlices.first, horizontalSlices.second]
case .oneThird:
segmentRects = [CGRect(x: cvWidth / 4.0, y: lastFrame.maxY + 1.0, width: cvWidth / 2.0, height: cvWidth / 2.0)]
case .threeOneThirds:
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth / 3)
let horizontalSlicesFirst = segmentFrame.dividedIntegral(fraction: 1.0 / 3, from: .minXEdge)
let horizontalSlices = horizontalSlicesFirst.second.dividedIntegral(fraction: 0.5, from: .minXEdge)
segmentRects = [horizontalSlicesFirst.first, horizontalSlices.first, horizontalSlices.second]
case .twoThirdsOneThirdRight:
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth * 2 / 3)
let horizontalSlices = segmentFrame.dividedIntegral(fraction: (1.0 / 3), from: .minXEdge)
let verticalSlices = horizontalSlices.first.dividedIntegral(fraction: 0.5, from: .minYEdge)
segmentRects = [verticalSlices.first, verticalSlices.second, horizontalSlices.second]
case .twoThirdsOneThirdCenter:
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth * 2 / 3)
let horizontalSlices = segmentFrame.dividedIntegral(fraction: (1.0 / 3), from: .minXEdge)
let verticalSlices = horizontalSlices.first.dividedIntegral(fraction: 0.5, from: .minYEdge)
segmentRects = [verticalSlices.first, horizontalSlices.second, verticalSlices.second]
case .oneThirdTwoThirds:
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth * 2 / 3)
let horizontalSlices = segmentFrame.dividedIntegral(fraction: (2.0 / 3), from: .minXEdge)
let verticalSlices = horizontalSlices.second.dividedIntegral(fraction: 0.5, from: .minYEdge)
segmentRects = [horizontalSlices.first, verticalSlices.first, verticalSlices.second]
}
// Create and cache layout attributes for calculated frames.
for rect in segmentRects {
let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: currentIndex, section: 0))
attributes.frame = rect
cachedAttributes.append(attributes)
contentBounds = contentBounds.union(lastFrame)
currentIndex += 1
lastFrame = rect
}
segment = getSegment(count: count, currentIndex: currentIndex)
}
}
func getSegment(count: Int, currentIndex: Int) -> MosaicSegmentStyle {
var segment: MosaicSegmentStyle
// Determine first segment style.
if currentIndex == 0 {
var segment: MosaicSegmentStyle = .threeOneThirds
if count == 1 {
segment = .fullWidth
} else if count >= 2 && count <= 4 {
if showLargeViewIndex >= 0 {
segment = .fullWidth
} else {
segment = .fiftyFifty
}
} else if showLargeViewIndex == 0 {
segment = .oneThirdTwoThirds
} else if showLargeViewIndex == 1 {
segment = .twoThirdsOneThirdCenter
} else if showLargeViewIndex == 2 {
segment = .twoThirdsOneThirdRight
}
return segment
}
// Determine the next segment style.
switch count - currentIndex {
case 1:
if count == 3 {
segment = .oneThird
} else if count > 4 && showLargeViewIndex == (count - 1) {
segment = .oneThirdTwoThirds
} else {
segment = .threeOneThirds
}
case 2:
if count == 4 {
segment = .fiftyFifty
} else if count > 4 && showLargeViewIndex == currentIndex {
segment = .oneThirdTwoThirds
} else if count > 4 && (showLargeViewIndex == currentIndex + 1) {
segment = .twoThirdsOneThirdCenter
} else if count > 4 && (showLargeViewIndex == currentIndex + 2) {
segment = .twoThirdsOneThirdRight
} else {
segment = .threeOneThirds
}
default:
if count > 4 && showLargeViewIndex == currentIndex {
segment = .oneThirdTwoThirds
} else if count > 4 && (showLargeViewIndex == currentIndex + 1) {
segment = .twoThirdsOneThirdCenter
} else if count > 4 && (showLargeViewIndex == currentIndex + 2) {
segment = .twoThirdsOneThirdRight
} else {
segment = .threeOneThirds
}
break
}
return segment
}
override var collectionViewContentSize: CGSize {
return contentBounds.size
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
guard let collectionView = collectionView else { return false }
return !newBounds.size.equalTo(collectionView.bounds.size)
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cachedAttributes[indexPath.item]
}
override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let attributes = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath) else { return nil }
if !deletingIndexPaths.isEmpty {
if deletingIndexPaths.contains(itemIndexPath) {
attributes.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
attributes.alpha = 0.0
attributes.zIndex = 0
}
}
return attributes
}
override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let attributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath) else { return nil }
if insertingIndexPaths.contains(itemIndexPath) {
attributes.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
attributes.alpha = 0.0
attributes.zIndex = 0
}
return attributes
}
override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
super.prepare(forCollectionViewUpdates: updateItems)
for update in updateItems {
switch update.updateAction {
case .delete:
guard let indexPath = update.indexPathBeforeUpdate else { return }
deletingIndexPaths.append(indexPath)
case .insert:
guard let indexPath = update.indexPathAfterUpdate else { return }
insertingIndexPaths.append(indexPath)
default:
break
}
}
}
override func finalizeCollectionViewUpdates() {
super.finalizeCollectionViewUpdates()
deletingIndexPaths.removeAll()
insertingIndexPaths.removeAll()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributesArray = [UICollectionViewLayoutAttributes]()
// Find any cell that sits within the query rect.
guard let lastIndex = cachedAttributes.indices.last,
let firstMatchIndex = binSearch(rect, start: 0, end: lastIndex) else { return attributesArray }
// Starting from the match, loop up and down through the array until all the attributes
// have been added within the query rect.
for attributes in cachedAttributes[..<firstMatchIndex].reversed() {
guard attributes.frame.maxY >= rect.minY else { break }
attributesArray.append(attributes)
}
for attributes in cachedAttributes[firstMatchIndex...] {
guard attributes.frame.minY <= rect.maxY else { break }
attributesArray.append(attributes)
}
return attributesArray
}
// Perform a binary search on the cached attributes array.
func binSearch(_ rect: CGRect, start: Int, end: Int) -> Int? {
if end < start { return nil }
let mid = (start + end) / 2
let attributes = cachedAttributes[mid]
if attributes.frame.intersects(rect) {
return mid
} else {
if attributes.frame.maxY < rect.minY {
return binSearch(rect, start: (mid + 1), end: end)
} else {
return binSearch(rect, start: start, end: (mid - 1))
}
}
}
}

View File

@@ -0,0 +1,242 @@
//
// GroupCallVideoLayout.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/15.
//
import Foundation
class GroupCallVideoLayout: UIView, UICollectionViewDelegate, UICollectionViewDataSource {
let selfCallStatusObserver = Observer()
let isCameraOpenObserver = Observer()
let selfUser = TUICallState.instance.selfUser.value
var allUserList = [User]()
lazy var calleeCollectionView = {
let flowLayout = GroupCallVideoFlowLayout()
let calleeCollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: flowLayout)
calleeCollectionView.delegate = self
calleeCollectionView.dataSource = self
calleeCollectionView.showsVerticalScrollIndicator = false
calleeCollectionView.showsHorizontalScrollIndicator = false
calleeCollectionView.backgroundColor = UIColor.clear
return calleeCollectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clear
processUserList(remoteUserList: TUICallState.instance.remoteUserList.value)
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.isCameraOpen.removeObserver(isCameraOpenObserver)
TUICallState.instance.remoteUserList.removeObserver(selfCallStatusObserver)
showLargeViewIndex = -1
for view in subviews {
view.removeFromSuperview()
}
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
showHistoryLargeView()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(calleeCollectionView)
}
func activateConstraints() {
calleeCollectionView.snp.makeConstraints { make in
make.edges.equalTo(self)
}
}
func bindInteraction() {
for i in 0..<9 {
calleeCollectionView.register(GroupCallVideoCell.self, forCellWithReuseIdentifier: "GroupCallVideoCell_\(i)")
}
}
// MARK: Set TUICallState showLargeViewUserId
func setShowLargeViewUserId(userId: String) {
TUICallState.instance.showLargeViewUserId.value = userId
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
remoteUserChanged()
isCameraOpenChanged()
}
func remoteUserChanged() {
TUICallState.instance.remoteUserList.addObserver(selfCallStatusObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.processUserList(remoteUserList: newValue)
self.calleeCollectionView.reloadData()
})
}
func processUserList(remoteUserList: [User]) {
allUserList.removeAll()
selfUser.index = 0
allUserList.append(selfUser)
for (index, value) in remoteUserList.enumerated() {
value.index = index + 1
allUserList.append(value)
}
}
func isCameraOpenChanged() {
TUICallState.instance.isCameraOpen.addObserver(isCameraOpenObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
if newValue {
self.showMySelfAsLargeView()
}
})
}
func showMySelfAsLargeView() {
var row = -1
for (index, element) in allUserList.enumerated() where element.id.value == selfUser.id.value {
row = index
}
if row >= 0 && selfUser.id.value != TUICallState.instance.showLargeViewUserId.value {
let indexPath = IndexPath(row: row, section: 0)
performUpdates(indexPath: indexPath)
}
}
func showHistoryLargeView() {
var row = -1
for (index, element) in allUserList.enumerated() where element.id.value == TUICallState.instance.showLargeViewUserId.value {
row = index
}
if row >= 0 && row < allUserList.count {
let indexPath = IndexPath(row: row, section: 0)
performUpdates(indexPath: indexPath)
}
}
}
// MARK: UICollectionViewDelegate, UICollectionViewDataSource
extension GroupCallVideoLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return allUserList.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GroupCallVideoCell_\(indexPath.row)",
for: indexPath) as! GroupCallVideoCell
cell.initCell(user: allUserList[indexPath.row])
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
performUpdates(indexPath: indexPath)
}
func performUpdates(indexPath: IndexPath) {
let count = allUserList.count
let remoteUpdates = getRemoteUpdates(indexPath: indexPath)
var firstBigFlag = false
if count >= 2 && count <= 4 && indexPath.row != showLargeViewIndex {
firstBigFlag = true
}
showLargeViewIndex = (showLargeViewIndex == indexPath.row) ? -1 : indexPath.row
if firstBigFlag {
showLargeViewIndex = 0
}
setShowLargeViewUserId(userId: (showLargeViewIndex >= 0) ? allUserList[indexPath.row].id.value : " ")
// Animate all other update types together.
calleeCollectionView.cancelInteractiveMovement()
calleeCollectionView.performBatchUpdates({
var deletes = [Int]()
var inserts = [(user:User, index:Int)]()
for update in remoteUpdates {
switch update {
case let .delete(index):
calleeCollectionView.deleteItems(at: [IndexPath(item: index, section: 0)])
deletes.append(index)
case let .insert(user, index):
calleeCollectionView.insertItems(at: [IndexPath(item: index, section: 0)])
inserts.append((user, index))
case let .move(fromIndex, toIndex):
calleeCollectionView.moveItem(at: IndexPath(item: fromIndex, section: 0),
to: IndexPath(item: toIndex, section: 0))
deletes.append(fromIndex)
inserts.append((allUserList[fromIndex], toIndex))
}
}
for deletedIndex in deletes.sorted().reversed() {
allUserList.remove(at: deletedIndex)
}
let sortedInserts = inserts.sorted(by: { (userA, userB) -> Bool in
return userA.index <= userB.index
})
for insertion in sortedInserts {
if insertion.index >= allUserList.startIndex && insertion.index <= allUserList.endIndex {
allUserList.insert(insertion.user, at: insertion.index)
}
}
}) { [weak self] _ in
guard let self = self else { return }
self.calleeCollectionView.endInteractiveMovement()
}
}
func getRemoteUpdates(indexPath: IndexPath) -> [UserUpdate] {
let count = allUserList.count
if count < 2 || count > 4 || indexPath.row >= count {
return [UserUpdate]()
}
if indexPath.row == showLargeViewIndex {
return [
UserUpdate.move(0, allUserList[indexPath.row].index)
]
}
if count == 2 || allUserList[0].index == 0 {
return [
UserUpdate.move(indexPath.row, 0)
]
}
return [
UserUpdate.move(0, allUserList[0].index),
UserUpdate.move(indexPath.row, 0)
]
}
}

View File

@@ -0,0 +1,142 @@
//
// GroupLoadingView.swift
// TUICallKit
//
// Created by noah on 2023/11/28.
//
import Foundation
let kGroupLoadingViewDotSize: CGFloat = 6.scaleWidth()
class GroupLoadingView: UIView {
let dotSpacing: CGFloat = 8.scaleWidth()
let animationDuration: TimeInterval = 0.5
private let dotLeft: UIView = {
let view = UIView()
view.backgroundColor = UIColor.white
view.layer.cornerRadius = kGroupLoadingViewDotSize / 2
view.alpha = 0.8
return view
}()
private let dotCenter: UIView = {
let view = UIView()
view.backgroundColor = UIColor.white
view.layer.cornerRadius = kGroupLoadingViewDotSize / 2
view.alpha = 0.5
return view
}()
private let dotRight: UIView = {
let view = UIView()
view.backgroundColor = UIColor.white
view.layer.cornerRadius = kGroupLoadingViewDotSize / 2
view.alpha = 0.2
return view
}()
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
}
private func constructViewHierarchy() {
addSubview(dotLeft)
addSubview(dotCenter)
addSubview(dotRight)
}
private func activateConstraints() {
dotLeft.snp.makeConstraints { make in
make.centerY.equalTo(self)
make.centerX.equalTo(dotCenter).offset(-(dotSpacing + kGroupLoadingViewDotSize))
make.width.height.equalTo(kGroupLoadingViewDotSize)
}
dotCenter.snp.makeConstraints { make in
make.center.equalTo(self)
make.width.height.equalTo(kGroupLoadingViewDotSize)
}
dotRight.snp.makeConstraints { make in
make.centerY.equalTo(self)
make.centerX.equalTo(dotCenter).offset(dotSpacing + kGroupLoadingViewDotSize)
make.width.height.equalTo(kGroupLoadingViewDotSize)
}
}
func resetDotsAlpha() {
dotLeft.alpha = 0.8
dotCenter.alpha = 0.5
dotRight.alpha = 0.2
}
// MARK: UI Animation
private var isAnimation: Bool = false
func startAnimating() {
if isAnimation {
return
} else {
isAnimation = true
}
UIView.animate(withDuration: animationDuration) {
self.dotLeft.alpha = 0.2
self.dotCenter.alpha = 0.8
self.dotRight.alpha = 0.5
} completion: { finished in
if finished {
self.startSecondAnimation()
}
}
}
private func startSecondAnimation() {
UIView.animate(withDuration: animationDuration, delay: 0, options: [.curveEaseInOut ], animations: {
self.dotLeft.alpha = 0.2
self.dotCenter.alpha = 0.5
self.dotRight.alpha = 0.8
}, completion: { finished in
if finished {
self.startThirdAnimation()
}
})
}
private func startThirdAnimation() {
UIView.animate(withDuration: animationDuration, delay: 0, options: [.curveEaseInOut ], animations: {
self.dotRight.alpha = 0.5
}, completion: { finished in
if finished {
self.startFourthAnimation()
}
})
}
private func startFourthAnimation() {
UIView.animate(withDuration: animationDuration, delay: 0, options: [.curveEaseInOut ], animations: {
self.dotLeft.alpha = 0.8
self.dotCenter.alpha = 0.5
self.dotRight.alpha = 0.2
}, completion: { finished in
if finished {
self.isAnimation = false
self.startAnimating()
}
})
}
func stopAnimating() {
dotLeft.layer.removeAllAnimations()
dotCenter.layer.removeAllAnimations()
dotRight.layer.removeAllAnimations()
resetDotsAlpha()
isAnimation = false
}
}

View File

@@ -0,0 +1,259 @@
//
// SingleLayoutView.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/15.
//
import Foundation
private let kCallKitSingleSmallVideoViewWidth = 100.0
private let kCallKitSingleSmallVideoViewFrame = CGRect(x: ScreenSize.width - kCallKitSingleSmallVideoViewWidth,
y: StatusBar_Height + 40,
width: kCallKitSingleSmallVideoViewWidth,
height: kCallKitSingleSmallVideoViewWidth / 9.0 * 16.0)
private let kCallKitSingleLargeVideoViewFrame = CGRect(x: 0, y: 0, width: ScreenSize.width, height: ScreenSize.height)
class SingleCallVideoLayout: UIView {
let selfCallStatusObserver = Observer()
let isCameraOpenObserver = Observer()
let enableBlurBackgroundObserver = Observer()
let remoteUserListObserver = Observer()
var remoteHadInit = false
var isLocalPreViewLarge: Bool = true
var localPreView: VideoView {
if VideoFactory.instance.viewMap[TUICallState.instance.selfUser.value.id.value] == nil {
let _ = VideoFactory.instance.createVideoView(userId: TUICallState.instance.selfUser.value.id.value, frame: CGRect.zero)
}
return VideoFactory.instance.viewMap[TUICallState.instance.selfUser.value.id.value]?.videoView ?? VideoView(frame: CGRect.zero)
}
var remotePreView: VideoView {
guard let remoteUser = TUICallState.instance.remoteUserList.value.first else { return VideoView(frame: CGRect.zero) }
if VideoFactory.instance.viewMap[remoteUser.id.value] == nil {
let _ = VideoFactory.instance.createVideoView(userId: remoteUser.id.value, frame: CGRect.zero)
}
return VideoFactory.instance.viewMap[remoteUser.id.value]?.videoView ?? VideoView(frame: CGRect.zero)
}
var remoteUser: User?
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.t_colorWithHexString(color: "#242424")
if TUICallState.instance.mediaType.value != .video {
return
}
initPreView()
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.selfUser.value.callStatus.removeObserver(selfCallStatusObserver)
TUICallState.instance.isCameraOpen.removeObserver(isCameraOpenObserver)
TUICallState.instance.enableBlurBackground.removeObserver(enableBlurBackgroundObserver)
TUICallState.instance.remoteUserList.removeObserver(remoteUserListObserver)
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
callStatusChanged()
cameraStateChanged()
enableBlurBackgroundStateChange()
remoteUserListChange()
}
func callStatusChanged() {
TUICallState.instance.selfUser.value.callStatus.addObserver(selfCallStatusObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
if TUICallState.instance.mediaType.value == .video &&
TUICallState.instance.selfUser.value.callStatus.value == .accept {
self.initRemotePreView()
self.setBeginAcceptPreview()
}
if TUICallState.instance.mediaType.value == .unknown &&
TUICallState.instance.selfUser.value.callStatus.value == .none {
self.setEndPreview()
self.deinitPreView()
}
})
}
func cameraStateChanged() {
TUICallState.instance.isCameraOpen.addObserver(isCameraOpenObserver) { [weak self] newValue, _ in
guard let self = self else { return }
if newValue == false && self.isLocalPreViewLarge == false {
self.localPreView.isHidden = true
} else {
self.localPreView.isHidden = false
}
}
}
func enableBlurBackgroundStateChange() {
TUICallState.instance.enableBlurBackground.addObserver(enableBlurBackgroundObserver) { [weak self] newValue, _ in
guard let self = self else { return }
if newValue == true && self.isLocalPreViewLarge == false {
switchPreview()
}
}
}
func remoteUserListChange() {
TUICallState.instance.remoteUserList.addObserver(remoteUserListObserver) { [weak self] newValue, _ in
guard let self = self else { return }
if !remoteHadInit && TUICallState.instance.selfUser.value.callStatus.value == .accept {
self.initRemotePreView()
self.setBeginAcceptPreview()}
}
}
// MARK: update UI
func switchPreview() {
if isLocalPreViewLarge {
UIView.animate(withDuration: 0.3) {
self.localPreView.frame = kCallKitSingleSmallVideoViewFrame
self.remotePreView.frame = kCallKitSingleLargeVideoViewFrame
} completion: { finished in
self.sendSubviewToBack(self.remotePreView)
}
isLocalPreViewLarge = false
} else {
UIView.animate(withDuration: 0.3) {
self.localPreView.frame = kCallKitSingleLargeVideoViewFrame
self.remotePreView.frame = kCallKitSingleSmallVideoViewFrame
} completion: { finished in
self.sendSubviewToBack(self.localPreView)
}
isLocalPreViewLarge = true
}
}
func setBeginAcceptPreview() {
remotePreView.isHidden = false
UIView.animate(withDuration: 0.3) {
self.localPreView.frame = kCallKitSingleSmallVideoViewFrame
self.remotePreView.frame = kCallKitSingleLargeVideoViewFrame
self.localPreView.isHidden = TUICallState.instance.isCameraOpen.value ? false : true
} completion: { finished in
self.sendSubviewToBack(self.remotePreView)
}
isLocalPreViewLarge = false
}
func setEndPreview() {
CallEngineManager.instance.closeCamera()
CallEngineManager.instance.stopRemoteView(user: self.remoteUser ?? User())
self.remoteUser = nil
isLocalPreViewLarge = true
}
func initPreView() {
if TUICallState.instance.selfUser.value.callStatus.value == .waiting {
initLocalPreView()
} else if TUICallState.instance.selfUser.value.callStatus.value == .accept {
initLocalPreView()
initRemotePreView()
setBeginAcceptPreview()
}
}
func initLocalPreView() {
localPreView.frame = kCallKitSingleLargeVideoViewFrame
localPreView.delegate = self
localPreView.isUserInteractionEnabled = true
localPreView.isHidden = false
addSubview(localPreView)
if TUICallState.instance.selfUser.value.callStatus.value == .waiting {
CallEngineManager.instance.openCamera(videoView: localPreView)
} else if TUICallState.instance.selfUser.value.callStatus.value == .accept && TUICallState.instance.isCameraOpen.value == true {
CallEngineManager.instance.openCamera(videoView: localPreView)
}
if TUICallState.instance.isCameraOpen.value == false {
localPreView.isHidden = true
} else {
localPreView.isHidden = false
}
}
func initRemotePreView() {
guard let remoteUser = TUICallState.instance.remoteUserList.value.first else { return }
self.remoteUser = remoteUser
remotePreView.frame = kCallKitSingleSmallVideoViewFrame
remotePreView.isUserInteractionEnabled = true
remotePreView.delegate = self
remotePreView.isHidden = true
addSubview(self.remotePreView)
remoteHadInit = true
CallEngineManager.instance.startRemoteView(user: remoteUser, videoView: remotePreView)
}
func deinitPreView() {
localPreView.removeFromSuperview()
remotePreView.removeFromSuperview()
VideoFactory.instance.viewMap.removeAll()
}
}
extension SingleCallVideoLayout: VideoViewDelegate {
func tapGestureAction(tapGesture: UITapGestureRecognizer) {
if tapGesture.view?.frame.size.width == CGFloat(kCallKitSingleSmallVideoViewWidth) {
switchPreview()
} else {
self.clickFullScreen()
}
if TUICallState.instance.isCameraOpen.value == false && self.isLocalPreViewLarge == false {
self.localPreView.isHidden = true
} else {
self.localPreView.isHidden = false
}
}
func clickFullScreen() {
if (TUICallState.instance.selfUser.value.callStatus.value == .accept) {
TUICallState.instance.isShowFullScreen.value = !TUICallState.instance.isShowFullScreen.value
}
}
func panGestureAction(panGesture: UIPanGestureRecognizer) {
if panGesture.view?.frame.size.width != CGFloat(kCallKitSingleSmallVideoViewWidth) { return }
let smallView = panGesture.view?.superview
if panGesture.state == .changed {
let translation = panGesture.translation(in: self)
let newCenterX = translation.x + (smallView?.center.x ?? 0.0)
let newCenterY = translation.y + (smallView?.center.y ?? 0.0)
if newCenterX < ((smallView?.bounds.size.width ?? 0.0) / 2.0) ||
newCenterX > self.bounds.size.width - (smallView?.bounds.size.width ?? 0.0) / 2.0 {
return
}
if newCenterY < ((smallView?.bounds.size.height ?? 0.0) / 2.0) ||
newCenterY > self.bounds.size.height - (smallView?.bounds.size.height ?? 0.0) / 2.0 {
return
}
UIView.animate(withDuration: 0.1) {
smallView?.center = CGPoint(x: newCenterX, y: newCenterY)
}
panGesture.setTranslation(CGPointZero, in: self)
}
}
}

View File

@@ -0,0 +1,44 @@
//
// VideoFactory.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/15.
//
import Foundation
class VideoFactory {
static let instance = VideoFactory()
var viewMap: [String: UserVideoEntity] = Dictionary()
func createVideoView(userId: String,frame: CGRect) -> VideoView {
let videoView = VideoView(frame: frame)
let userVideoEntity = UserVideoEntity()
userVideoEntity.userId = userId
userVideoEntity.videoView = videoView
if TUICallState.instance.selfUser.value.id.value == userId {
userVideoEntity.user = TUICallState.instance.selfUser.value
} else {
for user in TUICallState.instance.remoteUserList.value where user.id.value == userId {
userVideoEntity.user = user
}
}
viewMap[userId] = userVideoEntity
return videoView
}
func isExistVideoView(videoView: VideoView) -> Bool {
let isExist = false
for view in viewMap where view.value.videoView == videoView {
return true
}
return isExist
}
}
class UserVideoEntity {
var userId: String = ""
var user: User = User()
var videoView: VideoView = VideoView(frame: CGRect.zero)
}

View File

@@ -0,0 +1,89 @@
//
// VideoView.swift
// TUICallKit
//
// Created by : on 2023/2/14.
//
import Foundation
import TUICallEngine
@objc protocol VideoViewDelegate: NSObjectProtocol {
@objc optional func tapGestureAction(tapGesture: UITapGestureRecognizer)
@objc optional func panGestureAction(panGesture: UIPanGestureRecognizer)
}
class VideoView: TUIVideoView {
weak var delegate: VideoViewDelegate?
let volumeProgress: UIProgressView = {
let volumeProgress = UIProgressView(progressViewStyle: .default)
volumeProgress.backgroundColor = UIColor.clear
return volumeProgress
}()
let gestureView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.t_colorWithHexString(color: "#55534F")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(gestureView)
addSubview(volumeProgress)
}
func activateConstraints() {
gestureView.snp.makeConstraints { make in
make.edges.equalTo(self)
}
volumeProgress.snp.makeConstraints { make in
make.leading.trailing.bottom.equalTo(self)
make.height.equalTo(4)
}
}
func bindInteraction() {
gestureView.backgroundColor = UIColor.clear
let tap = UITapGestureRecognizer(target: self, action: #selector(tapGesture(tapGesture: )))
let pan = UIPanGestureRecognizer(target: self, action: #selector(panGesture(panGesture: )))
gestureView.addGestureRecognizer(tap)
pan.require(toFail: tap)
gestureView.addGestureRecognizer(pan)
}
// MARK: Gesture Action
@objc func tapGesture(tapGesture: UITapGestureRecognizer) {
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("tapGestureAction")))) != nil) {
self.delegate?.tapGestureAction?(tapGesture: tapGesture)
}
}
@objc func panGesture(panGesture: UIPanGestureRecognizer) {
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("panGestureAction")))) != nil) {
self.delegate?.panGestureAction?(panGesture: panGesture)
}
}
}