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,50 @@
//
// CallWaitingHintView.swift
// TUICallKit
//
// Created by vincepzhang on 2023/3/7.
//
import Foundation
class CallWaitingHintView: UIView {
let label: UILabel = {
let waitingInviteLabel = UILabel(frame: CGRect.zero)
waitingInviteLabel.textColor = UIColor.t_colorWithHexString(color: "#999999")
waitingInviteLabel.font = UIFont.boldSystemFont(ofSize: 14.0)
waitingInviteLabel.backgroundColor = UIColor.clear
waitingInviteLabel.textAlignment = .center
waitingInviteLabel.text = TUICallKitLocalize(key: "TUICallKit.calleeTip")
return waitingInviteLabel
}()
override init(frame: CGRect) {
super.init(frame: frame)
}
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()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(label)
}
func activateConstraints() {
self.label.snp.makeConstraints { make in
make.centerX.size.equalTo(self)
}
}
}

View File

@@ -0,0 +1,101 @@
//
// BaseControlButton.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/14.
//
import Foundation
import UIKit
import SnapKit
typealias ButtonActionBlock = (_ sender: UIButton) -> Void
class BaseControlButton: UIView {
var buttonActionBlock: ButtonActionBlock?
var imageSize: CGSize
let titleLabel: UILabel = {
let titleLabel = UILabel(frame: CGRect.zero)
titleLabel.font = UIFont.systemFont(ofSize: 12.0)
titleLabel.textAlignment = .center
titleLabel.numberOfLines = 2
titleLabel.lineBreakMode = .byTruncatingTail
return titleLabel
}()
let button: UIButton = {
let button = UIButton(type: .system)
return button
}()
static func create(frame: CGRect, title: String, imageSize: CGSize, buttonAction: @escaping ButtonActionBlock) -> BaseControlButton {
let controlButton = BaseControlButton(frame: frame, imageSize: imageSize)
controlButton.titleLabel.text = title
controlButton.buttonActionBlock = buttonAction
return controlButton
}
init(frame: CGRect, imageSize: CGSize) {
self.imageSize = imageSize
super.init(frame: frame)
backgroundColor = UIColor.clear
}
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(button)
addSubview(titleLabel)
}
func activateConstraints() {
button.snp.makeConstraints { make in
make.top.equalTo(self)
make.centerX.equalTo(self)
make.size.equalTo(imageSize)
}
titleLabel.snp.makeConstraints { make in
make.centerX.equalTo(self)
make.top.equalTo(button.snp.bottom).offset(10)
make.width.equalTo(100.scaleWidth())
}
}
func bindInteraction() {
button.addTarget(self, action: #selector(buttonActionEvent(sender: )), for: .touchUpInside)
}
// MARK: Update Info
func updateImage(image: UIImage) {
button.setBackgroundImage(image, for: .normal)
}
func updateTitle(title: String) {
titleLabel.text = title
}
func updateTitleColor(titleColor: UIColor) {
titleLabel.textColor = titleColor
}
// MARK: Event Action
@objc func buttonActionEvent(sender: UIButton) {
guard let buttonActionBlock = buttonActionBlock else { return }
buttonActionBlock(sender)
}
}

View File

@@ -0,0 +1,48 @@
//
// UIButtonCommon.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/10.
//
import Foundation
enum TUIButtonEdgeInsetsStyle {
case TUIButtonEdgeInsetsStyleTop
case TUIButtonEdgeInsetsStyleLeft
case TUIButtonEdgeInsetsStyleBottom
case TUIButtonEdgeInsetsStyleRight
}
extension UIButton {
func layoutButtonWithEdgeInsetsStyle(style: TUIButtonEdgeInsetsStyle, space: CGFloat) {
let imageWidth = imageView?.frame.size.width ?? 0.0
let imageHeight = imageView?.frame.size.height ?? 0.0
let labelWidth = titleLabel?.intrinsicContentSize.width ?? 0.0
let labelHeight = titleLabel?.intrinsicContentSize.height ?? 0.0
var imageEdgeInsets: UIEdgeInsets = .zero
var labelEdgeInsets: UIEdgeInsets = .zero
switch style {
case .TUIButtonEdgeInsetsStyleTop:
imageEdgeInsets = UIEdgeInsets(top: -labelHeight - space / 2.0, left: 0, bottom: 0, right: -labelHeight)
labelEdgeInsets = UIEdgeInsets(top: 0, left: -imageWidth, bottom: -labelHeight - space / 2.0, right: 0)
break
case .TUIButtonEdgeInsetsStyleLeft:
imageEdgeInsets = UIEdgeInsets(top: 0, left: -space / 2.0, bottom: 0, right: space / 2.0)
labelEdgeInsets = UIEdgeInsets(top: 0, left: space / 2.0, bottom: 0 , right: -space / 2.0)
break
case .TUIButtonEdgeInsetsStyleBottom:
imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: -labelHeight - space / 2.0, right: -labelWidth)
labelEdgeInsets = UIEdgeInsets(top: -imageHeight - space / 2.0, left: -imageWidth, bottom: 0 , right: 0)
break
case .TUIButtonEdgeInsetsStyleRight:
imageEdgeInsets = UIEdgeInsets(top: 0, left: labelWidth+space / 2.0, bottom: 0, right: -labelWidth - space / 2.0)
labelEdgeInsets = UIEdgeInsets(top: 0, left: -imageWidth - space / 2.0, bottom: 0 , right: imageWidth + space / 2.0)
break
}
self.titleEdgeInsets = labelEdgeInsets
self.imageEdgeInsets = imageEdgeInsets
}
}

View File

@@ -0,0 +1,103 @@
//
// UIColorCommon.swift
// TUICallKitSwift
//
// Created by vincepzhang on 2023/1/3.
//
import UIKit
extension UIColor {
static func imageWithColor(color: UIColor, size: CGSize) -> UIImage {
var colorImage: UIImage
var rect: CGRect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContext(rect.size)
guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }
context.setFillColor(color.cgColor)
context.fill(rect)
guard let cImage = UIGraphicsGetImageFromCurrentImageContext() else { return UIImage() }
colorImage = cImage
UIGraphicsEndImageContext()
return colorImage
}
static func t_colorWithHexString(color: String) -> UIColor {
return UIColor.t_colorWithHexString(color: color, alpha: 1)
}
static func t_colorWithHexString(color: String, alpha: CGFloat) -> UIColor {
var colorString: String = color.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines)
if colorString.count < 6 {
return UIColor.clear
}
if colorString.hasPrefix("0X") {
colorString = colorString.substring(from: colorString.index(colorString.startIndex, offsetBy: 2))
}
if colorString.hasPrefix("#") {
colorString = colorString.substring(from: colorString.index(colorString.startIndex, offsetBy: 1))
}
if colorString.count < 6 {
return UIColor.clear
}
let rString = String(colorString.prefix(2))
colorString.removeFirst(2)
let gString = String(colorString.prefix(2))
colorString.removeFirst(2)
let bString = String(colorString.prefix(2))
var r = Int32()
var g = Int32()
var b = Int32()
Scanner(string: rString).scanHexInt32(&r)
Scanner(string: gString).scanHexInt32(&g)
Scanner(string: bString).scanHexInt32(&b)
return UIColor(red: CGFloat(r) / 255.0 , green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: alpha)
}
static func t_colorWithAlphaHexString(color: String) -> UIColor {
var colorString: String = color.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines)
if colorString.count < 6 {
return UIColor.clear
}
if colorString.hasPrefix("0X") {
colorString = colorString.substring(from: colorString.index(colorString.startIndex, offsetBy: 2))
}
if colorString.hasPrefix("#") {
colorString = colorString.substring(from: colorString.index(colorString.startIndex, offsetBy: 1))
}
if colorString.count < 6 {
return UIColor.clear
}
let rString = String(colorString.prefix(2))
colorString.removeFirst(2)
let gString = String(colorString.prefix(2))
colorString.removeFirst(2)
let bString = String(colorString.prefix(2))
colorString.removeFirst(2)
let alphaString = String(colorString.prefix(2))
var r: Float = Float()
var g: Float = Float()
var b: Float = Float()
var alpha: Float = Float()
Scanner(string: rString).scanHexFloat(&r)
Scanner(string: gString).scanHexFloat(&g)
Scanner(string: bString).scanHexFloat(&b)
Scanner(string: alphaString).scanHexFloat(&alpha)
return UIColor(red: CGFloat(r), green: CGFloat(g), blue: CGFloat(b), alpha: CGFloat(alpha))
}
}

View File

@@ -0,0 +1,26 @@
//
// UIViewControllerCommon.swift
// TUICallKit
//
// Created by vincepzhang on 2023/1/16.
//
import Foundation
import UIKit
extension UIViewController {
static func getCurrentViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return getCurrentViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
return getCurrentViewController(base: tab.selectedViewController)
}
if let presented = base?.presentedViewController {
return getCurrentViewController(base: presented)
}
return base
}
}

View File

@@ -0,0 +1,39 @@
import UIKit
extension UIWindow {
public func t_makeKeyAndVisible() {
if #available(iOS 13.0, *) {
for windowScene in UIApplication.shared.connectedScenes {
if windowScene.activationState == UIScene.ActivationState.foregroundActive ||
windowScene.activationState == UIScene.ActivationState.background {
self.windowScene = windowScene as? UIWindowScene
break
}
}
}
self.makeKeyAndVisible()
}
static func getKeyWindow() -> UIWindow? {
var keyWindow: UIWindow?
if #available(iOS 13, *) {
keyWindow = UIApplication.shared.connectedScenes
.filter({ $0.activationState == .foregroundActive })
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first(where: { $0.isKeyWindow })
} else {
keyWindow = UIApplication.shared.keyWindow
}
return keyWindow
}
static func getTopFullscreenWindow() -> UIWindow? {
let topWindow = UIApplication.shared.windows
.filter { !$0.isHidden && $0.bounds.equalTo(UIScreen.main.bounds) }
.max(by: { $0.windowLevel.rawValue < $1.windowLevel.rawValue })
return topWindow
}
}

View File

@@ -0,0 +1,83 @@
//
// FloatWindowViewController.swift
// TUICallKit
//
// Created by vincepzhang on 2023/3/2.
//
import Foundation
import TUICore
protocol FloatingWindowViewDelegate: NSObject {
func tapGestureAction(tapGesture: UITapGestureRecognizer)
func panGestureAction(panGesture: UIPanGestureRecognizer)
}
class FloatWindowViewController: UIViewController, FloatingWindowViewDelegate {
let callEventObserver = Observer()
weak var delegate: FloatingWindowViewDelegate?
override func viewDidLoad() {
super.viewDidLoad()
if TUICallState.instance.scene.value == .group {
addFloatingWindowGroupView()
} else if TUICallState.instance.scene.value == .single {
addFloatingWindowSignalView()
}
registerObserver()
}
deinit {
TUICallState.instance.event.removeObserver(callEventObserver)
for view in view.subviews {
view.removeFromSuperview()
}
}
func addFloatingWindowGroupView() {
let floatView = FloatingWindowGroupView(frame: CGRect.zero)
view.addSubview(floatView)
floatView.snp.makeConstraints { make in
make.center.equalTo(self.view)
make.size.equalTo(self.view)
}
floatView.delegate = self
}
func addFloatingWindowSignalView() {
let floatView = FloatingWindowSignalView(frame: CGRect.zero)
view.addSubview(floatView)
floatView.snp.makeConstraints { make in
make.center.equalTo(self.view)
make.size.equalTo(self.view)
}
floatView.delegate = self
}
// MARK: FloatingWindowViewDelegate
func tapGestureAction(tapGesture: UITapGestureRecognizer) {
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("tapGestureAction")))) != nil) {
self.delegate?.tapGestureAction(tapGesture: tapGesture)
}
}
func panGestureAction(panGesture: UIPanGestureRecognizer) {
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("panGestureAction")))) != nil) {
self.delegate?.panGestureAction(panGesture: panGesture)
}
}
func registerObserver() {
TUICallState.instance.event.addObserver(callEventObserver) { newValue, _ in
if newValue.eventType == .ERROR {
guard let errorCode = newValue.param[EVENT_KEY_CODE] as? Int32 else { return }
guard let errorMessage = newValue.param[EVENT_KEY_MESSAGE] as? String else { return }
TUITool.makeToast("error:\(errorCode):\(errorMessage)")
}
}
}
}

View File

@@ -0,0 +1,85 @@
//
// FloatingWindowButton.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/15.
//
import Foundation
class FloatingWindowButton: UIView {
let mediaTypeObserver = Observer()
let floatButton: FloatingWindowCustomButton = {
let floatButton = FloatingWindowCustomButton(type: .system)
return floatButton
}()
override init(frame: CGRect) {
super.init(frame: frame)
updateImage()
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.mediaType.removeObserver(mediaTypeObserver)
}
// 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(floatButton)
}
func activateConstraints() {
floatButton.snp.makeConstraints { make in
make.center.equalTo(self)
make.width.height.equalTo(24)
}
}
func bindInteraction() {
floatButton.addTarget(self, action: #selector(clickFloatButton(sender: )), for: .touchUpInside)
}
// MARK: Action Event
@objc func clickFloatButton(sender: UIButton) {
WindowManager.instance.showFloatWindow()
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
TUICallState.instance.mediaType.addObserver(mediaTypeObserver) { [weak self] newValue, _ in
guard let self = self else { return }
self.updateImage()
}
}
func updateImage() {
if let image = TUICallKitCommon.getBundleImage(name: "icon_min_window") {
floatButton.setBackgroundImage(image, for: .normal)
}
}
}
class FloatingWindowCustomButton: UIButton {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let expandedBounds = bounds.insetBy(dx: -6, dy: -6)
return expandedBounds.contains(point) ? self : nil
}
}

View File

@@ -0,0 +1,96 @@
//
// FloatingWindowStatusView.swift
// TUICallKit
//
// Created by noah on 2023/11/20.
//
import Foundation
class FloatingWindowGroupStatusView: UIView {
let isCameraOpenObserver = Observer()
let isMicMuteObserver = Observer()
let videoImageView: UIImageView = {
let imageView = UIImageView()
let imageName = TUICallState.instance.isCameraOpen.value ? "icon_float_group_video_on" : "icon_float_group_video_off"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
imageView.image = image
}
return imageView
}()
let audioImageView: UIImageView = {
let imageView = UIImageView()
let imageName = TUICallState.instance.isMicMute.value ? "icon_float_group_audio_off" : "icon_float_group_audio_on"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
imageView.image = image
}
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
registerObserveState()
constructViewHierarchy()
activateConstraints()
backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_float_window_group_status_view_bg_color",
defaultHex: "#F9F6F4")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.isCameraOpen.removeObserver(isCameraOpenObserver)
TUICallState.instance.isMicMute.removeObserver(isMicMuteObserver)
}
func constructViewHierarchy() {
addSubview(videoImageView)
addSubview(audioImageView)
}
func activateConstraints() {
videoImageView.snp.makeConstraints { make in
make.centerY.equalTo(self)
make.leading.equalTo(self).offset(14.scaleWidth())
make.width.height.equalTo(16.scaleWidth())
}
audioImageView.snp.makeConstraints { make in
make.centerY.equalTo(self)
make.trailing.equalTo(self).offset(-14.scaleWidth())
make.width.height.equalTo(16.scaleWidth())
}
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
isCameraOpenChange()
isMicMuteChange()
}
func isCameraOpenChange() {
TUICallState.instance.isCameraOpen.addObserver(isCameraOpenObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
let imageName = newValue ? "icon_float_group_video_on" : "icon_float_group_video_off"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
self.videoImageView.image = image
}
})
}
func isMicMuteChange() {
TUICallState.instance.isMicMute.addObserver(isMicMuteObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
let imageName = newValue ? "icon_float_group_audio_off" : "icon_float_group_audio_on"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
self.audioImageView.image = image
}
})
}
}

View File

@@ -0,0 +1,384 @@
//
// FloatingWindowGroupView.swift
// TUICallKit
//
// Created by noah on 2023/11/21.
//
import Foundation
import UIKit
import TUICore
class FloatingWindowGroupView: UIView {
let viewModel = FloatingWindowViewModel()
weak var delegate: FloatingWindowViewDelegate?
let selfCallStatusObserver = Observer()
let remoteVideoAvailableObserver = Observer()
let callTimeObserver = Observer()
let currentSpeakUserObserver = Observer()
let currentSpeakUserVideoAvailableObserver = Observer()
var localPreView: VideoView {
if VideoFactory.instance.viewMap[viewModel.selfUser.value.id.value] == nil {
let _ = VideoFactory.instance.createVideoView(userId: viewModel.selfUser.value.id.value, frame: CGRect.zero)
}
VideoFactory.instance.viewMap[viewModel.selfUser.value.id.value]?.videoView.isUserInteractionEnabled = false
return VideoFactory.instance.viewMap[viewModel.selfUser.value.id.value]?.videoView ?? VideoView(frame: CGRect.zero)
}
var remotePreView: VideoView {
let remoteUser = viewModel.currentSpeakUser.value
if VideoFactory.instance.viewMap[remoteUser.id.value] == nil {
let _ = VideoFactory.instance.createVideoView(userId: remoteUser.id.value, frame: CGRect.zero)
}
VideoFactory.instance.viewMap[remoteUser.id.value]?.videoView.isUserInteractionEnabled = false
return VideoFactory.instance.viewMap[remoteUser.id.value]?.videoView ?? VideoView(frame: CGRect.zero)
}
let avatarImageView: UIImageView = {
let avatarImageView = UIImageView()
avatarImageView.contentMode = .scaleAspectFill
avatarImageView.clipsToBounds = true
avatarImageView.isUserInteractionEnabled = false
return avatarImageView
}()
let containerView: UIView = {
let containerView = UIView()
containerView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_float_window_bg_color",
defaultHex: "#FFFFFF")
containerView.layer.cornerRadius = 12.scaleWidth()
containerView.layer.masksToBounds = true
containerView.isUserInteractionEnabled = false
return containerView
}()
let audioContainerView: UIView = {
let containerView = UIView()
containerView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_float_window_bg_color",
defaultHex: "#FFFFFF")
return containerView
}()
let shadowView: UIView = {
let shadowView = UIView()
shadowView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_float_window_bg_color",
defaultHex: "#FFFFFF")
shadowView.layer.shadowColor = UIColor.t_colorWithHexString(color: "353941").cgColor
shadowView.layer.shadowOpacity = 0.4
shadowView.layer.shadowRadius = 4.scaleWidth()
shadowView.layer.shadowOffset = CGSize(width: 0, height: 0)
shadowView.layer.cornerRadius = 12.scaleWidth()
return shadowView
}()
let imageView: UIImageView = {
let imageView = UIImageView()
if let image = TUICallKitCommon.getBundleImage(name: "icon_float_dialing") {
imageView.image = image
}
imageView.isUserInteractionEnabled = false
return imageView
}()
let audioDescribeLabel: UILabel = {
let describeLabel = UILabel()
describeLabel.font = UIFont.systemFont(ofSize: 12.0)
describeLabel.textColor = UIColor.t_colorWithHexString(color: "#12B969")
describeLabel.textAlignment = .center
describeLabel.isUserInteractionEnabled = false
describeLabel.text = TUICallKitLocalize(key: "TUICallKit.FloatingWindow.waitAccept") ?? ""
return describeLabel
}()
lazy var timerLabel: UILabel = {
let timerLabel = UILabel()
timerLabel.font = UIFont.systemFont(ofSize: 12.0)
timerLabel.textColor = UIColor.t_colorWithHexString(color: "#12B969")
timerLabel.textAlignment = .center
timerLabel.isUserInteractionEnabled = false
timerLabel.text = viewModel.getCallTimeString()
return timerLabel
}()
let groupNameLabel: UILabel = {
let nameLabel = UILabel()
nameLabel.font = UIFont.systemFont(ofSize: 12.0)
nameLabel.textColor = UIColor.t_colorWithHexString(color: "#FFFFFF")
nameLabel.textAlignment = .center
nameLabel.isUserInteractionEnabled = false
return nameLabel
}()
let groupStatusView: UIView = {
let statusView = FloatingWindowGroupStatusView()
return statusView
}()
override init(frame: CGRect) {
super.init(frame: frame)
registerObserverState()
constructViewHierarchy()
activateConstraints()
bindInteraction()
updateUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
viewModel.selfCallStatus.removeObserver(selfCallStatusObserver)
viewModel.selfCallStatus.removeObserver(currentSpeakUserObserver)
viewModel.currentSpeakUser.value.videoAvailable.removeObserver(currentSpeakUserVideoAvailableObserver)
viewModel.selfCallStatus.removeObserver(callTimeObserver)
for index in 0..<viewModel.remoteUserList.value.count {
guard index < viewModel.remoteUserList.value.count else {
break
}
viewModel.remoteUserList.value[index].videoAvailable.removeObserver(remoteVideoAvailableObserver)
}
}
func constructViewHierarchy() {
addSubview(shadowView)
addSubview(containerView)
containerView.addSubview(avatarImageView)
containerView.addSubview(audioContainerView)
audioContainerView.addSubview(imageView)
audioContainerView.addSubview(audioDescribeLabel)
audioContainerView.addSubview(timerLabel)
containerView.addSubview(groupNameLabel)
containerView.addSubview(groupStatusView)
}
func activateConstraints() {
shadowView.snp.makeConstraints { make in
make.edges.equalTo(containerView)
}
containerView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.top.equalTo(8.scaleWidth())
}
audioContainerView.snp.makeConstraints { make in
make.top.centerX.equalTo(containerView)
make.width.height.equalTo(72.scaleWidth())
}
imageView.snp.makeConstraints { make in
make.top.equalTo(audioContainerView).offset(8.scaleWidth())
make.centerX.equalTo(audioContainerView)
make.width.height.equalTo(36.scaleWidth())
}
audioDescribeLabel.snp.makeConstraints { make in
make.centerX.width.equalTo(audioContainerView)
make.top.equalTo(imageView.snp.bottom).offset(4.scaleWidth())
make.height.equalTo(20.scaleWidth())
}
timerLabel.snp.makeConstraints { make in
make.centerX.width.equalTo(audioContainerView)
make.top.equalTo(imageView.snp.bottom).offset(5.scaleWidth())
make.height.equalTo(20.scaleWidth())
}
avatarImageView.snp.makeConstraints { make in
make.top.centerX.width.equalToSuperview()
make.bottom.equalTo(groupStatusView.snp.top)
}
groupNameLabel.snp.makeConstraints { make in
make.centerX.width.equalTo(containerView)
make.bottom.equalTo(groupStatusView.snp.top)
make.height.equalTo(20.scaleWidth())
}
groupStatusView.snp.makeConstraints { make in
make.centerX.width.equalTo(containerView)
make.bottom.equalTo(containerView)
make.height.equalTo(20.scaleWidth())
}
}
func bindInteraction() {
let tap = UITapGestureRecognizer(target: self, action: #selector(tapGestureAction(tapGesture: )))
let pan = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(panGesture: )))
self.addGestureRecognizer(tap)
pan.require(toFail: tap)
self.addGestureRecognizer(pan)
}
func clearSubview() {
for subview in subviews {
subview.removeFromSuperview()
}
}
// MARK: Action Event
@objc func tapGestureAction(tapGesture: UITapGestureRecognizer) {
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("tapGestureAction")))) != nil) {
self.delegate?.tapGestureAction(tapGesture: tapGesture)
}
}
@objc func panGestureAction(panGesture: UIPanGestureRecognizer) {
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("panGestureAction")))) != nil) {
self.delegate?.panGestureAction(panGesture: panGesture)
}
}
// MARK: Register TUICallState Observer && Update UI
func registerObserverState() {
registerCallStatusObserver()
registerCurrentSpeakUserObserver()
registerCurrentSpeakUserVideoAvailableObserver()
registerCallTimeObserver()
registerUserVideoAvailableChange()
}
func registerCallStatusObserver() {
viewModel.selfCallStatus.addObserver(selfCallStatusObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateUI()
})
}
func registerCallTimeObserver() {
viewModel.callTime.addObserver(callTimeObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
DispatchCallKitMainAsyncSafe {
self.timerLabel.text = self.viewModel.getCallTimeString()
}
})
}
func registerCurrentSpeakUserObserver() {
viewModel.currentSpeakUser.addObserver(currentSpeakUserObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
if self.viewModel.scene.value == .group {
self.updateUI()
self.groupNameLabel.text = newValue.nickname.value
self.avatarImageView.sd_setImage(with: URL(string: newValue.avatar.value),
placeholderImage: TUICallKitCommon.getBundleImage(name: "default_user_icon"))
}
})
}
func registerCurrentSpeakUserVideoAvailableObserver() {
viewModel.currentSpeakUser.value.videoAvailable.addObserver(currentSpeakUserVideoAvailableObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
if self.viewModel.scene.value == .group {
self.updateUI()
self.groupNameLabel.text = self.viewModel.currentSpeakUser.value.nickname.value
self.avatarImageView.sd_setImage(with: URL(string: self.viewModel.currentSpeakUser.value.avatar.value),
placeholderImage: TUICallKitCommon.getBundleImage(name: "default_user_icon"))
}
})
}
func registerUserVideoAvailableChange() {
for index in 0..<viewModel.remoteUserList.value.count {
guard index < viewModel.remoteUserList.value.count else {
break
}
viewModel.remoteUserList.value[index].videoAvailable.addObserver(remoteVideoAvailableObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
if (self.viewModel.currentSpeakUser.value.id.value == self.viewModel.remoteUserList.value[index].id.value) {
self.updateUI()
}
})
}
}
// MARK: Update UI
func updateUI() {
cleanView()
if viewModel.selfCallStatus.value == .waiting {
setGroupWaitingUI()
} else if viewModel.selfCallStatus.value == .accept {
setGroupAcceptUI()
}
}
func setGroupWaitingUI() {
audioContainerView.isHidden = false
imageView.isHidden = false
audioDescribeLabel.isHidden = false
groupStatusView.isHidden = false
}
func setGroupAcceptUI() {
if viewModel.currentSpeakUser.value.id.value == viewModel.selfUser.value.id.value {
setGroupLocalUI()
} else if viewModel.currentSpeakUser.value.id.value.count > 0 {
setGroupRemoteUI()
} else {
setGroupDefineUI()
}
}
func setGroupDefineUI() {
audioContainerView.isHidden = false
imageView.isHidden = false
timerLabel.isHidden = false
groupStatusView.isHidden = false
}
func setGroupLocalUI() {
groupStatusView.isHidden = false
groupNameLabel.isHidden = false
if TUICallState.instance.isCameraOpen.value {
setGroupLocalVideoAvailableUI()
} else {
setGroupVideoNotAvailableUI()
}
containerView.bringSubviewToFront(groupNameLabel)
}
func setGroupRemoteUI() {
groupStatusView.isHidden = false
groupNameLabel.isHidden = false
if viewModel.currentSpeakUser.value.videoAvailable.value {
setGroupRemoteVideoAvailableUI()
} else {
setGroupVideoNotAvailableUI()
}
containerView.bringSubviewToFront(groupNameLabel)
}
func setGroupLocalVideoAvailableUI() {
avatarImageView.isHidden = true
containerView.addSubview(localPreView)
localPreView.snp.remakeConstraints { make in
make.edges.equalTo(avatarImageView)
}
}
func setGroupRemoteVideoAvailableUI() {
avatarImageView.isHidden = true
containerView.addSubview(remotePreView)
remotePreView.snp.makeConstraints { make in
make.edges.equalTo(avatarImageView)
}
viewModel.startRemoteView(user: viewModel.currentSpeakUser.value, videoView: remotePreView)
}
func setGroupVideoNotAvailableUI() {
avatarImageView.isHidden = false
containerView.bringSubviewToFront(avatarImageView)
}
func cleanView() {
avatarImageView.isHidden = true
audioContainerView.isHidden = true
imageView.isHidden = true
audioDescribeLabel.isHidden = true
timerLabel.isHidden = true
groupStatusView.isHidden = true
groupNameLabel.isHidden = true
}
}

View File

@@ -0,0 +1,336 @@
//
// FloatingWindow.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/14.
//
import Foundation
import UIKit
import TUICore
class FloatingWindowSignalView: UIView {
let viewModel = FloatingWindowViewModel()
weak var delegate: FloatingWindowViewDelegate?
let selfCallStatusObserver = Observer()
let remoteUserListObserver = Observer()
let callTimeObserver = Observer()
let kFloatingWindowVideoViewRect = CGRect(x: 0,
y: 0,
width: kMicroVideoViewWidth - 16.scaleWidth(),
height: kMicroVideoViewHeight - 16.scaleWidth())
var localPreView: VideoView {
if VideoFactory.instance.viewMap[viewModel.selfUser.value.id.value] == nil {
let _ = VideoFactory.instance.createVideoView(userId: viewModel.selfUser.value.id.value, frame: CGRect.zero)
}
VideoFactory.instance.viewMap[viewModel.selfUser.value.id.value]?.videoView.isUserInteractionEnabled = false
return VideoFactory.instance.viewMap[viewModel.selfUser.value.id.value]?.videoView ?? VideoView(frame: CGRect.zero)
}
var remotePreView: VideoView {
guard let remoteUser = viewModel.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)
}
VideoFactory.instance.viewMap[remoteUser.id.value]?.videoView.isUserInteractionEnabled = false
return VideoFactory.instance.viewMap[remoteUser.id.value]?.videoView ?? VideoView(frame: CGRect.zero)
}
let avatarImageView: UIImageView = {
let avatarImageView = UIImageView()
avatarImageView.contentMode = .scaleAspectFill
avatarImageView.clipsToBounds = true
avatarImageView.layer.cornerRadius = 8.scaleWidth()
avatarImageView.isUserInteractionEnabled = false
return avatarImageView
}()
let containerView: UIView = {
let containerView = UIView()
containerView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_float_window_bg_color",
defaultHex: "#FFFFFF")
containerView.layer.cornerRadius = 12.scaleWidth()
containerView.layer.masksToBounds = true
containerView.isUserInteractionEnabled = false
return containerView
}()
let audioContainerView: UIView = {
let containerView = UIView()
containerView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_float_window_bg_color",
defaultHex: "#FFFFFF")
return containerView
}()
let shadowView: UIView = {
let shadowView = UIView()
shadowView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_float_window_bg_color",
defaultHex: "#FFFFFF")
shadowView.layer.shadowColor = UIColor.t_colorWithHexString(color: "353941").cgColor
shadowView.layer.shadowOpacity = 0.4
shadowView.layer.cornerRadius = 12.scaleWidth()
shadowView.layer.shadowRadius = 4.scaleWidth()
shadowView.layer.shadowOffset = CGSize(width: 0, height: 0)
return shadowView
}()
let imageView: UIImageView = {
let imageView = UIImageView()
if let image = TUICallKitCommon.getBundleImage(name: "icon_float_dialing") {
imageView.image = image
}
imageView.isUserInteractionEnabled = false
return imageView
}()
let audioDescribeLabel: UILabel = {
let describeLabel = UILabel()
describeLabel.font = UIFont.systemFont(ofSize: 12.0)
describeLabel.textColor = UIColor.t_colorWithHexString(color: "#12B969")
describeLabel.textAlignment = .center
describeLabel.isUserInteractionEnabled = false
describeLabel.text = TUICallKitLocalize(key: "TUICallKit.FloatingWindow.waitAccept") ?? ""
return describeLabel
}()
let videoDescribeLabel: UILabel = {
let describeLabel = UILabel()
describeLabel.font = UIFont.systemFont(ofSize: 12.0)
describeLabel.textColor = UIColor.t_colorWithHexString(color: "#FFFFFF")
describeLabel.textAlignment = .center
describeLabel.isUserInteractionEnabled = false
describeLabel.text = TUICallKitLocalize(key: "TUICallKit.FloatingWindow.waitAccept") ?? ""
return describeLabel
}()
lazy var timerLabel: UILabel = {
let timerLabel = UILabel()
timerLabel.font = UIFont.systemFont(ofSize: 12.0)
timerLabel.textColor = UIColor.t_colorWithHexString(color: "#12B969")
timerLabel.textAlignment = .center
timerLabel.isUserInteractionEnabled = false
timerLabel.text = viewModel.getCallTimeString()
return timerLabel
}()
override init(frame: CGRect) {
super.init(frame: frame)
registerObserverState()
constructViewHierarchy()
activateConstraints()
bindInteraction()
updateUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
viewModel.selfCallStatus.removeObserver(selfCallStatusObserver)
viewModel.selfCallStatus.removeObserver(callTimeObserver)
}
func constructViewHierarchy() {
addSubview(shadowView)
addSubview(containerView)
containerView.addSubview(avatarImageView)
containerView.addSubview(audioContainerView)
audioContainerView.addSubview(imageView)
audioContainerView.addSubview(audioDescribeLabel)
audioContainerView.addSubview(timerLabel)
containerView.addSubview(videoDescribeLabel)
}
func activateConstraints() {
shadowView.snp.makeConstraints { make in
make.edges.equalTo(containerView)
}
containerView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.top.equalTo(8.scaleWidth())
}
avatarImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(45.scaleWidth())
}
audioContainerView.snp.makeConstraints { make in
make.top.centerX.equalTo(containerView)
make.width.height.equalTo(72.scaleWidth())
}
imageView.snp.makeConstraints { make in
make.top.equalTo(audioContainerView).offset(8.scaleWidth())
make.centerX.equalTo(audioContainerView)
make.width.height.equalTo(36.scaleWidth())
}
audioDescribeLabel.snp.makeConstraints { make in
make.centerX.width.equalTo(audioContainerView)
make.top.equalTo(imageView.snp.bottom).offset(4.scaleWidth())
make.height.equalTo(20.scaleWidth())
}
timerLabel.snp.makeConstraints { make in
make.centerX.width.equalTo(audioContainerView)
make.top.equalTo(imageView.snp.bottom).offset(4.scaleWidth())
make.height.equalTo(20.scaleWidth())
}
videoDescribeLabel.snp.makeConstraints { make in
make.centerX.width.equalTo(containerView)
make.bottom.equalTo(containerView).offset(-8.scaleWidth())
make.height.equalTo(20.scaleWidth())
}
}
func bindInteraction() {
let tap = UITapGestureRecognizer(target: self, action: #selector(tapGestureAction(tapGesture: )))
let pan = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(panGesture: )))
self.addGestureRecognizer(tap)
pan.require(toFail: tap)
self.addGestureRecognizer(pan)
}
func clearSubview() {
for subview in subviews {
subview.removeFromSuperview()
}
}
// MARK: Action Event
@objc func tapGestureAction(tapGesture: UITapGestureRecognizer) {
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("tapGestureAction")))) != nil) {
self.delegate?.tapGestureAction(tapGesture: tapGesture)
}
}
@objc func panGestureAction(panGesture: UIPanGestureRecognizer) {
if self.delegate != nil && ((self.delegate?.responds(to: Selector(("panGestureAction")))) != nil) {
self.delegate?.panGestureAction(panGesture: panGesture)
}
}
// MARK: Register TUICallState Observer && Update UI
func registerObserverState() {
registerCallStatusObserver()
registerCallTimeObserver()
}
func registerCallStatusObserver() {
viewModel.selfCallStatus.addObserver(selfCallStatusObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateUI()
})
}
func registerCallTimeObserver() {
viewModel.callTime.addObserver(callTimeObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
DispatchCallKitMainAsyncSafe {
self.timerLabel.text = self.viewModel.getCallTimeString()
}
})
}
func userVideoAvailableChange() {
if viewModel.remoteUserList.value.first != nil {
viewModel.remoteUserList.value.first?.videoAvailable.addObserver(remoteUserListObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateUI()
})
}
}
// MARK: Update UI
func updateUI() {
cleanView()
if viewModel.mediaType.value == .audio {
updateSingleAudioUI()
} else if viewModel.mediaType.value == .video {
updateSingleVideoUI()
}
}
func updateSingleAudioUI() {
containerView.backgroundColor = UIColor.t_colorWithHexString(color: "FFFFFF")
if viewModel.selfCallStatus.value == .waiting {
setSingleAudioWaitingUI()
} else if viewModel.selfCallStatus.value == .accept {
setSingleAudioAcceptUI()
}
}
func updateSingleVideoUI() {
containerView.backgroundColor = UIColor.t_colorWithHexString(color: "303132")
if viewModel.selfCallStatus.value == .waiting {
setSingleVideoWaitingUI()
} else if viewModel.selfCallStatus.value == .accept {
guard let remoteUser = viewModel.remoteUserList.value.first else { return }
if remoteUser.videoAvailable.value == true {
setSingleVideoAcceptUI()
} else {
setSingleVideoAcceptWithUnavailableUI()
}
}
userVideoAvailableChange()
}
func setSingleAudioWaitingUI() {
audioContainerView.isHidden = false
imageView.isHidden = false
audioDescribeLabel.isHidden = false
}
func setSingleAudioAcceptUI() {
audioContainerView.isHidden = false
imageView.isHidden = false
timerLabel.isHidden = false
}
func setSingleVideoWaitingUI() {
localPreView.frame = kFloatingWindowVideoViewRect
videoDescribeLabel.isHidden = false
containerView.addSubview(localPreView)
containerView.bringSubviewToFront(videoDescribeLabel)
}
func setSingleVideoAcceptUI() {
remotePreView.frame = kFloatingWindowVideoViewRect
remotePreView.isHidden = false
containerView.addSubview(remotePreView)
guard let remoteUser = self.viewModel.remoteUserList.value.first else { return }
viewModel.startRemoteView(user: remoteUser, videoView: remotePreView)
}
func setSingleVideoAcceptWithUnavailableUI() {
avatarImageView.isHidden = false
remotePreView.isHidden = true
containerView.bringSubviewToFront(avatarImageView)
guard let remoteUser = viewModel.remoteUserList.value.first else { return }
let userIcon: UIImage? = TUICallKitCommon.getBundleImage(name: "default_user_icon")
if remoteUser.avatar.value == "" {
guard let image = userIcon else { return }
avatarImageView.image = image
} else {
avatarImageView.sd_setImage(with: URL(string: remoteUser.avatar.value), placeholderImage: userIcon)
}
}
func cleanView() {
avatarImageView.isHidden = true
audioContainerView.isHidden = true
imageView.isHidden = true
audioDescribeLabel.isHidden = true
timerLabel.isHidden = true
videoDescribeLabel.isHidden = true
}
}

View File

@@ -0,0 +1,78 @@
//
// AudioAndVideoCalleeWaitingView.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/14.
//
import Foundation
import TUICallEngine
class AudioAndVideoCalleeWaitingView: UIView {
lazy var acceptBtn: BaseControlButton = {
weak var weakSelf = self
let acceptBtn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: "TUICallKit.answer") ?? "",
imageSize: kBtnSmallSize) { sender in
weakSelf?.acceptTouchEvent(sender: sender)
}
if let image = TUICallKitCommon.getBundleImage(name: "icon_dialing") {
acceptBtn.updateImage(image: image)
}
acceptBtn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return acceptBtn
}()
lazy var rejectBtn: BaseControlButton = {
weak var weakSelf = self
let rejectBtn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: "TUICallKit.decline") ?? "",
imageSize: kBtnSmallSize) { sender in
weakSelf?.rejectTouchEvent(sender: sender)
}
if let image = TUICallKitCommon.getBundleImage(name: "icon_hangup") {
rejectBtn.updateImage(image: image)
}
rejectBtn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return rejectBtn
}()
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(rejectBtn)
addSubview(acceptBtn)
}
func activateConstraints() {
rejectBtn.snp.makeConstraints { make in
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? 80.scaleWidth() : -80.scaleWidth())
make.top.bottom.equalTo(self)
make.size.equalTo(kControlBtnSize)
}
acceptBtn.snp.makeConstraints { make in
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? -80.scaleWidth() : 80.scaleWidth())
make.top.bottom.equalTo(self)
make.size.equalTo(kControlBtnSize)
}
}
// MARK: Event Action
func rejectTouchEvent(sender: UIButton) {
CallEngineManager.instance.reject()
}
func acceptTouchEvent(sender: UIButton) {
CallEngineManager.instance.accept()
}
}

View File

@@ -0,0 +1,153 @@
//
// AudioCallerWaitingAndAcceptedView.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/14.
//
import Foundation
import UIKit
import TUICallEngine
class AudioCallerWaitingAndAcceptedView : UIView {
let isMicMuteObserver = Observer()
let audioDeviceObserver = Observer()
lazy var muteMicBtn: BaseControlButton = {
let titleKey = TUICallState.instance.isMicMute.value ? "TUICallKit.muted" : "TUICallKit.unmuted"
let muteAudioBtn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: titleKey) ?? "",
imageSize: kBtnSmallSize, buttonAction: { [weak self] sender in
guard let self = self else { return }
self.muteMicEvent(sender: sender)
})
let imageName = TUICallState.instance.isMicMute.value ? "icon_mute_on" : "icon_mute"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
muteAudioBtn.updateImage(image: image)
}
muteAudioBtn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return muteAudioBtn
}()
lazy var hangupBtn: BaseControlButton = {
let hangupBtn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: "TUICallKit.hangup") ?? "",
imageSize: kBtnSmallSize, buttonAction: { [weak self] sender in
guard let self = self else { return }
self.hangupEvent(sender: sender)
})
if let image = TUICallKitCommon.getBundleImage(name: "icon_hangup") {
hangupBtn.updateImage(image: image)
}
hangupBtn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return hangupBtn
}()
lazy var changeSpeakerBtn: BaseControlButton = {
let titleKey = (TUICallState.instance.audioDevice.value == .speakerphone) ? "TUICallKit.speakerPhone" : "TUICallKit.earpiece"
let changeSpeakerBtn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: titleKey) ?? "",
imageSize: kBtnSmallSize, buttonAction: { [weak self] sender in
guard let self = self else { return }
self.changeSpeakerEvent(sender: sender)
})
let imageName = (TUICallState.instance.audioDevice.value == .speakerphone) ? "icon_handsfree_on" : "icon_handsfree"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
changeSpeakerBtn.updateImage(image: image)
}
changeSpeakerBtn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return changeSpeakerBtn
}()
override init(frame: CGRect) {
super.init(frame: frame)
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.isMicMute.removeObserver(isMicMuteObserver)
TUICallState.instance.audioDevice.removeObserver(audioDeviceObserver)
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(muteMicBtn)
addSubview(hangupBtn)
addSubview(changeSpeakerBtn)
}
func activateConstraints() {
muteMicBtn.snp.makeConstraints { make in
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? 110.scaleWidth() : -110.scaleWidth())
make.centerY.equalTo(hangupBtn)
make.size.equalTo(kControlBtnSize)
}
hangupBtn.snp.makeConstraints { make in
make.centerX.equalTo(self)
make.bottom.equalTo(self.snp.bottom)
make.size.equalTo(kControlBtnSize)
}
changeSpeakerBtn.snp.makeConstraints { make in
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? -110.scaleWidth() : 110.scaleWidth())
make.centerY.equalTo(self.hangupBtn)
make.size.equalTo(kControlBtnSize)
}
}
// MARK: Action Event
func muteMicEvent(sender: UIButton) {
CallEngineManager.instance.muteMic()
}
func changeSpeakerEvent(sender: UIButton) {
CallEngineManager.instance.changeSpeaker()
}
func hangupEvent(sender: UIButton) {
CallEngineManager.instance.hangup()
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
TUICallState.instance.isMicMute.addObserver(isMicMuteObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateMuteAudioBtn(mute: newValue)
})
TUICallState.instance.audioDevice.addObserver(audioDeviceObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateChangeSpeakerBtn(isSpeaker: newValue == .speakerphone)
})
}
func updateMuteAudioBtn(mute: Bool) {
muteMicBtn.updateTitle(title: TUICallKitLocalize(key: mute ? "TUICallKit.muted" : "TUICallKit.unmuted") ?? "")
if let image = TUICallKitCommon.getBundleImage(name: mute ? "icon_mute_on" : "icon_mute") {
muteMicBtn.updateImage(image: image)
}
}
func updateChangeSpeakerBtn(isSpeaker: Bool) {
changeSpeakerBtn.updateTitle(title: TUICallKitLocalize(key: isSpeaker ? "TUICallKit.speakerPhone" : "TUICallKit.earpiece") ?? "")
if let image = TUICallKitCommon.getBundleImage(name: isSpeaker ? "icon_handsfree_on" : "icon_handsfree") {
changeSpeakerBtn.updateImage(image: image)
}
}
}

View File

@@ -0,0 +1,388 @@
//
// GroupCallerAndCalleeAcceptedView.swift
// TUICallKit
//
// Created by noah on 2023/11/8.
//
import Foundation
import SnapKit
protocol GroupCallerAndCalleeAcceptedViewDelegate: AnyObject {
func showAnimation()
func restoreExpansion()
func handleTransform(animationScale: CGFloat)
}
let groupFunctionAnimationDuration = 0.3
let groupFunctionBaseControlBtnHeight = 60.scaleWidth() + 5.scaleHeight() + 20
let groupFunctionBottomHeight = Bottom_SafeHeight > 1 ? Bottom_SafeHeight : 8
let groupFunctionViewHeight = 22 + groupFunctionBaseControlBtnHeight + 20.scaleHeight() + 60.scaleWidth() + groupFunctionBottomHeight
let groupSmallFunctionViewHeight = 22 + 60.scaleWidth() + groupFunctionBottomHeight
class GroupCallerAndCalleeAcceptedView: UIView {
weak var delegate: GroupCallerAndCalleeAcceptedViewDelegate?
let isCameraOpenObserver = Observer()
let showLargeViewUserIdObserver = Observer()
let isMicMuteObserver = Observer()
let audioDeviceObserver = Observer()
var isShowLittleContainerView = false
var panGestureBeganY = 0.0
lazy var containerView: UIView = {
let containerView = UIView(frame: CGRect.zero)
containerView.backgroundColor = UIColor.t_colorWithHexString(color: "#4F586B")
containerView.alpha = 0.5
containerView.isUserInteractionEnabled = true
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
containerView.addGestureRecognizer(panGesture)
return containerView
}()
lazy var muteMicBtn: BaseControlButton = {
let titleKey = TUICallState.instance.isMicMute.value ? "TUICallKit.muted" : "TUICallKit.unmuted"
let btn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: titleKey) ?? "",
imageSize: kBtnSmallSize) { [weak self] sender in
guard let self = self else { return }
self.muteMicEvent(sender: sender)
}
let imageName = TUICallState.instance.isMicMute.value ? "icon_mute_on" : "icon_mute"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
btn.updateImage(image: image)
}
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return btn
}()
lazy var closeCameraBtn: BaseControlButton = {
let titleKey = TUICallState.instance.isCameraOpen.value ? "TUICallKit.cameraOn" : "TUICallKit.cameraOff"
let btn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: titleKey) ?? "",
imageSize: kBtnSmallSize) { [weak self] sender in
guard let self = self else { return }
self.closeCameraTouchEvent(sender: sender)
}
if let image = TUICallKitCommon.getBundleImage(name: TUICallState.instance.isCameraOpen.value ? "icon_camera_on" : "icon_camera_off") {
btn.updateImage(image: image)
}
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return btn
}()
lazy var changeSpeakerBtn: BaseControlButton = {
let titleKey = (TUICallState.instance.audioDevice.value == .speakerphone) ? "TUICallKit.speakerPhone" : "TUICallKit.earpiece"
let btn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: titleKey) ?? "",
imageSize: kBtnSmallSize) { [weak self] sender in
guard let self = self else { return }
self.changeSpeakerEvent(sender: sender)
}
let imageName = (TUICallState.instance.audioDevice.value == .speakerphone) ? "icon_handsfree_on" : "icon_handsfree"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
btn.updateImage(image: image)
}
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return btn
}()
lazy var hangupBtn: BaseControlButton = {
let btn = BaseControlButton.create(frame: CGRect.zero,
title: "",
imageSize: kBtnSmallSize) { [weak self] sender in
guard let self = self else { return }
self.hangupTouchEvent(sender: sender)
}
if let image = TUICallKitCommon.getBundleImage(name: "icon_hangup") {
btn.updateImage(image: image)
}
return btn
}()
lazy var matchBtn: UIButton = {
let btn = UIButton(type: .system)
btn.addTarget(self,action:#selector(matchTouchEvent(sender:)), for: .touchUpInside)
btn.setBackgroundImage(TUICallKitCommon.getBundleImage(name: "icon_match"), for: .normal)
return btn
}()
override init(frame: CGRect) {
super.init(frame: frame)
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.isCameraOpen.removeObserver(isCameraOpenObserver)
TUICallState.instance.showLargeViewUserId.removeObserver(showLargeViewUserIdObserver)
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
setContainerViewCorner()
let isHidden: Bool = (TUICallState.instance.showLargeViewUserId.value.count <= 1)
containerView.isHidden = isHidden
matchBtn.isHidden = isHidden
isViewReady = true
}
func constructViewHierarchy() {
addSubview(containerView)
addSubview(muteMicBtn)
addSubview(changeSpeakerBtn)
addSubview(closeCameraBtn)
addSubview(hangupBtn)
addSubview(matchBtn)
}
func activateConstraints() {
let top = 22.0
containerView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: frame.size.width, height: groupFunctionViewHeight))
muteMicBtn.snp.makeConstraints { make in
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? 110.scaleWidth() : -110.scaleWidth())
make.centerY.equalTo(changeSpeakerBtn)
make.width.height.equalTo(60.scaleWidth())
}
changeSpeakerBtn.snp.makeConstraints { make in
make.top.equalTo(self).offset(top)
make.centerX.equalTo(self)
make.width.height.equalTo(60.scaleWidth())
}
closeCameraBtn.snp.makeConstraints { make in
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? -110.scaleWidth() : 110.scaleWidth())
make.centerY.equalTo(self.changeSpeakerBtn)
make.width.height.equalTo(60.scaleWidth())
}
hangupBtn.snp.makeConstraints { make in
make.top.equalTo(changeSpeakerBtn.snp.bottom).offset(5.scaleHeight() + 20 + 20.scaleHeight())
make.centerX.equalTo(self)
make.width.height.equalTo(60.scaleWidth())
}
matchBtn.snp.makeConstraints { make in
make.centerY.equalTo(hangupBtn)
make.leading.width.height.equalTo(30.scaleWidth())
}
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
TUICallState.instance.isCameraOpen.addObserver(isCameraOpenObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateCloseCameraBtn(open: newValue)
})
TUICallState.instance.showLargeViewUserId.addObserver(showLargeViewUserIdObserver) { [weak self] newValue, _ in
guard let self = self else { return }
if newValue.count > 1 {
self.showAnimation()
self.containerView.isHidden = false
self.matchBtn.isHidden = false
} else {
self.restoreExpansion()
}
}
TUICallState.instance.isMicMute.addObserver(isMicMuteObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateMuteAudioBtn(mute: newValue)
})
TUICallState.instance.audioDevice.addObserver(audioDeviceObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateChangeSpeakerBtn(isSpeaker: newValue == .speakerphone)
})
}
// MARK: Action Event
func muteMicEvent(sender: UIButton) {
CallEngineManager.instance.muteMic()
updateMuteAudioBtn(mute: TUICallState.instance.isMicMute.value == true)
}
func closeCameraTouchEvent(sender: UIButton) {
updateCloseCameraBtn(open: TUICallState.instance.isCameraOpen.value != true)
if TUICallState.instance.isCameraOpen.value == true {
CallEngineManager.instance.closeCamera()
} else {
guard let videoViewEntity = VideoFactory.instance.viewMap[TUICallState.instance.selfUser.value.id.value] else { return }
CallEngineManager.instance.openCamera(videoView: videoViewEntity.videoView)
}
}
func changeSpeakerEvent(sender: UIButton) {
CallEngineManager.instance.changeSpeaker()
updateChangeSpeakerBtn(isSpeaker: TUICallState.instance.audioDevice.value == .speakerphone)
}
@objc func hangupTouchEvent(sender: UIButton) {
CallEngineManager.instance.hangup()
}
@objc func matchTouchEvent(sender: UIButton) {
if isShowLittleContainerView {
restoreExpansion()
} else {
showAnimation()
}
}
@objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: containerView)
let scale = ((translation.y - panGestureBeganY) / 105)
if gesture.state == .began {
panGestureBeganY = translation.y
} else if gesture.state == .changed {
if scale > 0 && !isShowLittleContainerView {
// Swipe down
handleTransform(animationScale: (scale > 1) ? 1 : scale)
} else if scale < 0 && isShowLittleContainerView {
// Swipe up
handleTransform(animationScale: (scale < -1) ? 0 : (1 + scale))
}
} else if gesture.state == .ended {
if scale > 0 {
// Swipe down
if (scale > 0.5) {
showAnimation()
} else {
restoreExpansion()
}
} else {
// Swipe up
if (scale < -0.5) {
restoreExpansion()
} else {
showAnimation()
}
}
}
}
func showAnimation() {
isShowLittleContainerView = true
delegate?.showAnimation()
UIView.animate(withDuration: groupFunctionAnimationDuration) {
self.handleTransform(animationScale: 1)
}
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.toValue = NSNumber(value: Double.pi)
rotationAnimation.duration = groupFunctionAnimationDuration
rotationAnimation.fillMode = .forwards
rotationAnimation.isRemovedOnCompletion = false
self.matchBtn.layer.add(rotationAnimation, forKey: "rotationAnimation")
}
func restoreExpansion() {
isShowLittleContainerView = false
delegate?.restoreExpansion()
UIView.animate(withDuration: groupFunctionAnimationDuration, animations: {
self.muteMicBtn.titleLabel.alpha = 1
self.changeSpeakerBtn.titleLabel.alpha = 1
self.closeCameraBtn.titleLabel.alpha = 1
self.muteMicBtn.titleLabel.transform = CGAffineTransform.identity
self.changeSpeakerBtn.titleLabel.transform = CGAffineTransform.identity
self.closeCameraBtn.titleLabel.transform = CGAffineTransform.identity
self.muteMicBtn.button.transform = CGAffineTransform.identity
self.changeSpeakerBtn.button.transform = CGAffineTransform.identity
self.closeCameraBtn.button.transform = CGAffineTransform.identity
self.hangupBtn.button.transform = CGAffineTransform.identity
self.muteMicBtn.transform = CGAffineTransform.identity
self.changeSpeakerBtn.transform = CGAffineTransform.identity
self.closeCameraBtn.transform = CGAffineTransform.identity
self.hangupBtn.transform = CGAffineTransform.identity
self.matchBtn.transform = CGAffineTransform.identity
})
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.toValue = NSNumber(value: 0.0)
rotationAnimation.duration = groupFunctionAnimationDuration
rotationAnimation.fillMode = .forwards
rotationAnimation.isRemovedOnCompletion = true
self.matchBtn.layer.add(rotationAnimation, forKey: "rotationAnimation")
}
func handleTransform(animationScale: CGFloat) {
delegate?.handleTransform(animationScale: animationScale)
let alpha: CGFloat = 1 - 1 * animationScale
let scale: CGFloat = 1 - 1 / 3 * animationScale
let muteMicX = ((TUICoreDefineConvert.getIsRTL() ? -28 : 28) * animationScale).scaleWidth()
let changeSpeakerX = ((TUICoreDefineConvert.getIsRTL() ? 2 : -2) * animationScale).scaleWidth()
let closeCameraX = ((TUICoreDefineConvert.getIsRTL() ? 28 : -28) * animationScale).scaleWidth()
let hangupX = ((TUICoreDefineConvert.getIsRTL() ? -138 : 138) * animationScale).scaleWidth()
let hangupTranslationY = -(groupFunctionBaseControlBtnHeight + 20.scaleHeight()) * animationScale
let titleLabelTranslationY = -12.scaleWidth() * animationScale
self.muteMicBtn.titleLabel.alpha = alpha
self.changeSpeakerBtn.titleLabel.alpha = alpha
self.closeCameraBtn.titleLabel.alpha = alpha
self.muteMicBtn.titleLabel.transform = CGAffineTransform(translationX: 0, y: titleLabelTranslationY)
self.changeSpeakerBtn.titleLabel.transform = CGAffineTransform(translationX: 0, y: titleLabelTranslationY)
self.closeCameraBtn.titleLabel.transform = CGAffineTransform(translationX: 0, y: titleLabelTranslationY)
self.muteMicBtn.button.transform = CGAffineTransform(scaleX: scale, y: scale)
self.changeSpeakerBtn.button.transform = CGAffineTransform(scaleX: scale, y: scale)
self.closeCameraBtn.button.transform = CGAffineTransform(scaleX: scale, y: scale)
self.hangupBtn.button.transform = CGAffineTransform(scaleX: scale, y: scale)
self.muteMicBtn.transform = CGAffineTransform(translationX: muteMicX, y: 0)
self.changeSpeakerBtn.transform = CGAffineTransform(translationX: changeSpeakerX, y: 0)
self.closeCameraBtn.transform = CGAffineTransform(translationX: closeCameraX, y: 0)
self.hangupBtn.transform = CGAffineTransform(translationX: hangupX, y: hangupTranslationY)
self.matchBtn.transform = CGAffineTransform(translationX: 0, y: hangupTranslationY)
}
// MARK: Update UI
func updateMuteAudioBtn(mute: Bool) {
muteMicBtn.updateTitle(title: TUICallKitLocalize(key: mute ? "TUICallKit.muted" : "TUICallKit.unmuted") ?? "")
if let image = TUICallKitCommon.getBundleImage(name: mute ? "icon_mute_on" : "icon_mute") {
muteMicBtn.updateImage(image: image)
}
}
func updateChangeSpeakerBtn(isSpeaker: Bool) {
changeSpeakerBtn.updateTitle(title: TUICallKitLocalize(key: isSpeaker ? "TUICallKit.speakerPhone" : "TUICallKit.earpiece") ?? "")
if let image = TUICallKitCommon.getBundleImage(name: isSpeaker ? "icon_handsfree_on" : "icon_handsfree") {
changeSpeakerBtn.updateImage(image: image)
}
}
func updateCloseCameraBtn(open: Bool) {
closeCameraBtn.updateTitle(title: TUICallKitLocalize(key: open ? "TUICallKit.cameraOn" : "TUICallKit.cameraOff") ?? "")
if let image = TUICallKitCommon.getBundleImage(name: open ? "icon_camera_on" : "icon_camera_off") {
closeCameraBtn.updateImage(image: image)
}
}
func setContainerViewCorner() {
let maskLayer = CAShapeLayer()
let path = UIBezierPath(roundedRect: containerView.bounds,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: 20, height: 20))
maskLayer.path = path.cgPath
containerView.layer.mask = maskLayer
}
}

View File

@@ -0,0 +1,233 @@
//
// VideoCallerAndCalleeAcceptedView.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/14.
//
import Foundation
import SnapKit
class VideoCallerAndCalleeAcceptedView: UIView {
let isCameraOpenObserver = Observer()
let isMicMuteObserver = Observer()
let audioDeviceObserver = Observer()
lazy var muteMicBtn: BaseControlButton = {
let titleKey = TUICallState.instance.isMicMute.value ? "TUICallKit.muted" : "TUICallKit.unmuted"
let btn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: titleKey) ?? "",
imageSize: kBtnSmallSize) { [weak self] sender in
guard let self = self else { return }
self.muteMicEvent(sender: sender)
}
let imageName = TUICallState.instance.isMicMute.value ? "icon_mute_on" : "icon_mute"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
btn.updateImage(image: image)
}
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return btn
}()
lazy var closeCameraBtn: BaseControlButton = {
let titleKey = TUICallState.instance.isCameraOpen.value ? "TUICallKit.cameraOn" : "TUICallKit.cameraOff"
let btn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: titleKey) ?? "",
imageSize: kBtnSmallSize) { [weak self] sender in
guard let self = self else { return }
self.closeCameraTouchEvent(sender: sender)
}
let imageName = TUICallState.instance.isCameraOpen.value ? "icon_camera_on" : "icon_camera_off"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
btn.updateImage(image: image)
}
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return btn
}()
lazy var changeSpeakerBtn: BaseControlButton = {
let titleKey = (TUICallState.instance.audioDevice.value == .speakerphone) ? "TUICallKit.speakerPhone" : "TUICallKit.earpiece"
let btn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: titleKey) ?? "",
imageSize: kBtnSmallSize) { [weak self] sender in
guard let self = self else { return }
self.changeSpeakerEvent(sender: sender)
}
let imageName = (TUICallState.instance.audioDevice.value == .speakerphone) ? "icon_handsfree_on" : "icon_handsfree"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
btn.updateImage(image: image)
}
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return btn
}()
lazy var hangupBtn: UIButton = {
let btn = UIButton(type: .system)
btn.addTarget(self,action:#selector(hangupTouchEvent(sender:)), for: .touchUpInside)
btn.setBackgroundImage(TUICallKitCommon.getBundleImage(name: "icon_hangup"), for: .normal)
return btn
}()
lazy var switchCameraBtn: UIButton = {
let btn = UIButton(type: .system)
if let image = TUICallKitCommon.getBundleImage(name: "switch_camera") {
btn.setBackgroundImage(image, for: .normal)
}
btn.addTarget(self, action: #selector(switchCameraTouchEvent(sender: )), for: .touchUpInside)
return btn
}()
lazy var virtualBackgroundButton: UIButton = {
let btn = UIButton(type: .system)
if let image = TUICallKitCommon.getBundleImage(name: "virtual_background") {
btn.setBackgroundImage(image, for: .normal)
}
btn.addTarget(self, action: #selector(virtualBackgroundTouchEvent(sender: )), for: .touchUpInside)
return btn
}()
override init(frame: CGRect) {
super.init(frame: frame)
registerObserveState()
}
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()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(muteMicBtn)
addSubview(changeSpeakerBtn)
addSubview(closeCameraBtn)
addSubview(hangupBtn)
addSubview(switchCameraBtn)
if TUICallState.instance.showVirtualBackgroundButton {
addSubview(virtualBackgroundButton)
}
}
func activateConstraints() {
muteMicBtn.snp.makeConstraints { make in
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? 110.scaleWidth() : -110.scaleWidth())
make.centerY.equalTo(changeSpeakerBtn)
make.size.equalTo(kControlBtnSize)
}
changeSpeakerBtn.snp.makeConstraints { make in
make.centerX.equalTo(self)
make.bottom.equalTo(hangupBtn.snp.top).offset(-20.scaleHeight())
make.size.equalTo(kControlBtnSize)
}
closeCameraBtn.snp.makeConstraints { make in
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? -110.scaleWidth() : 110.scaleWidth())
make.centerY.equalTo(self.changeSpeakerBtn)
make.size.equalTo(kControlBtnSize)
}
hangupBtn.snp.makeConstraints { make in
make.centerX.equalTo(self)
make.bottom.equalTo(snp.bottom)
make.width.height.equalTo(60.scaleWidth())
}
switchCameraBtn.snp.makeConstraints { make in
make.centerY.equalTo(hangupBtn)
make.leading.equalTo(hangupBtn.snp.trailing).offset(40.scaleWidth())
make.size.equalTo(CGSize(width: 28.scaleWidth(), height: 28.scaleWidth()))
}
if TUICallState.instance.showVirtualBackgroundButton {
virtualBackgroundButton.snp.makeConstraints { make in
make.centerY.equalTo(hangupBtn)
make.trailing.equalTo(hangupBtn.snp.leading).offset(-40.scaleWidth())
make.size.equalTo(CGSize(width: 28.scaleWidth(), height: 28.scaleWidth()))
}
}
}
// MARK: Action Event
func muteMicEvent(sender: UIButton) {
CallEngineManager.instance.muteMic()
updateMuteAudioBtn(mute: TUICallState.instance.isMicMute.value == true)
}
func closeCameraTouchEvent(sender: UIButton) {
updateCloseCameraBtn(open: TUICallState.instance.isCameraOpen.value != true)
if TUICallState.instance.isCameraOpen.value == true {
CallEngineManager.instance.closeCamera()
virtualBackgroundButton.isHidden = true
switchCameraBtn.isHidden = true
} else {
guard let videoViewEntity = VideoFactory.instance.viewMap[TUICallState.instance.selfUser.value.id.value] else { return }
CallEngineManager.instance.openCamera(videoView: videoViewEntity.videoView)
virtualBackgroundButton.isHidden = false
switchCameraBtn.isHidden = false
}
}
func changeSpeakerEvent(sender: UIButton) {
CallEngineManager.instance.changeSpeaker()
updateChangeSpeakerBtn(isSpeaker: TUICallState.instance.audioDevice.value == .speakerphone)
}
@objc func hangupTouchEvent(sender: UIButton) {
CallEngineManager.instance.hangup()
}
@objc func switchCameraTouchEvent(sender: UIButton) {
CallEngineManager.instance.switchCamera()
}
@objc func virtualBackgroundTouchEvent(sender: UIButton) {
CallEngineManager.instance.setBlurBackground()
}
// MARK: Update UI
func updateMuteAudioBtn(mute: Bool) {
muteMicBtn.updateTitle(title: TUICallKitLocalize(key: mute ? "TUICallKit.muted" : "TUICallKit.unmuted") ?? "")
if let image = TUICallKitCommon.getBundleImage(name: mute ? "icon_mute_on" : "icon_mute") {
muteMicBtn.updateImage(image: image)
}
}
func updateChangeSpeakerBtn(isSpeaker: Bool) {
changeSpeakerBtn.updateTitle(title: TUICallKitLocalize(key: isSpeaker ? "TUICallKit.speakerPhone" : "TUICallKit.earpiece") ?? "")
if let image = TUICallKitCommon.getBundleImage(name: isSpeaker ? "icon_handsfree_on" : "icon_handsfree") {
changeSpeakerBtn.updateImage(image: image)
}
}
func updateCloseCameraBtn(open: Bool) {
closeCameraBtn.updateTitle(title: TUICallKitLocalize(key: open ? "TUICallKit.cameraOn" : "TUICallKit.cameraOff") ?? "")
if let image = TUICallKitCommon.getBundleImage(name: open ? "icon_camera_on" : "icon_camera_off") {
closeCameraBtn.updateImage(image: image)
}
}
func registerObserveState() {
TUICallState.instance.isCameraOpen.addObserver(isCameraOpenObserver) { [weak self] newValue, _ in
guard let self = self else { return }
self.updateCloseCameraBtn(open: TUICallState.instance.isCameraOpen.value)
}
TUICallState.instance.isMicMute.addObserver(isMicMuteObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateMuteAudioBtn(mute: newValue)
})
TUICallState.instance.audioDevice.addObserver(audioDeviceObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateChangeSpeakerBtn(isSpeaker: newValue == .speakerphone)
})
}
}

View File

@@ -0,0 +1,187 @@
//
// VideoCallerWaitingView.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/14.
//
import Foundation
class VideoCallerWaitingView: UIView {
let enableBlurBackgroundObserver = Observer()
lazy var switchCameraBtn: BaseControlButton = {
let btn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: "TUICallKit.switchCamera") ?? "",
imageSize: kBtnLargeSize) { [weak self] sender in
guard let self = self else { return }
self.switchCameraTouchEvent(sender: sender)
}
if let image = TUICallKitCommon.getBundleImage(name: "icon_big_switch_camera") {
btn.updateImage(image: image)
}
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return btn
}()
lazy var virtualBackgroundButton: BaseControlButton = {
let btn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: "TUICallKit.blurBackground") ?? "",
imageSize: kBtnLargeSize) { [weak self] sender in
guard let self = self else { return }
self.virtualBackgroundTouchEvent(sender: sender)
}
let imageName = TUICallState.instance.enableBlurBackground.value ? "icon_big_virtual_background_on" : "icon_big_virtual_background_off"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
btn.updateImage(image: image)
}
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return btn
}()
lazy var closeCameraBtn: BaseControlButton = {
let titleKey = TUICallState.instance.isCameraOpen.value ? "TUICallKit.cameraOn" : "TUICallKit.cameraOff"
let btn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: titleKey) ?? "",
imageSize: kBtnSmallSize) { [weak self] sender in
guard let self = self else { return }
self.closeCameraTouchEvent(sender: sender)
}
let imageName = TUICallState.instance.isCameraOpen.value ? "icon_camera_on" : "icon_camera_off"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
btn.updateImage(image: image)
}
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return btn
}()
lazy var hangupBtn: BaseControlButton = {
weak var weakSelf = self
let btn = BaseControlButton.create(frame: CGRect.zero,
title: TUICallKitLocalize(key: "TUICallKit.hangup") ?? "",
imageSize: kBtnLargeSize) { [weak self] sender in
guard let self = self else { return }
self.hangupTouchEvent(sender: sender)
}
if let image = TUICallKitCommon.getBundleImage(name: "icon_hangup") {
btn.updateImage(image: image)
}
btn.updateTitleColor(titleColor: UIColor.t_colorWithHexString(color: "#D5E0F2"))
return btn
}()
override init(frame: CGRect) {
super.init(frame: frame)
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
if TUICallState.instance.showVirtualBackgroundButton {
TUICallState.instance.selfUser.removeObserver(enableBlurBackgroundObserver)
}
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(switchCameraBtn)
addSubview(closeCameraBtn)
addSubview(hangupBtn)
if TUICallState.instance.showVirtualBackgroundButton {
addSubview(virtualBackgroundButton)
}
}
func activateConstraints() {
if TUICallState.instance.showVirtualBackgroundButton {
virtualBackgroundButton.snp.makeConstraints { make in
make.top.centerX.equalTo(self)
make.bottom.equalTo(hangupBtn.snp.top).offset(-8.scaleWidth())
make.size.equalTo(kControlBtnSize)
}
}
switchCameraBtn.snp.makeConstraints { make in
make.centerY.equalTo(TUICallState.instance.showVirtualBackgroundButton ? virtualBackgroundButton : hangupBtn)
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? 110.scaleWidth() : -110.scaleWidth())
make.size.equalTo(kControlBtnSize)
}
hangupBtn.snp.makeConstraints { make in
if !TUICallState.instance.showVirtualBackgroundButton {
make.top.equalTo(self)
}
make.centerX.bottom.equalTo(self)
make.size.equalTo(kControlBtnSize)
}
closeCameraBtn.snp.makeConstraints { make in
make.centerY.equalTo(TUICallState.instance.showVirtualBackgroundButton ? virtualBackgroundButton : hangupBtn)
make.centerX.equalTo(self).offset(TUICoreDefineConvert.getIsRTL() ? -110.scaleWidth() : 110.scaleWidth())
make.size.equalTo(kControlBtnSize)
}
}
// MARK: Action Event
func closeCameraTouchEvent(sender: UIButton) {
updateCloseCameraBtn(open: TUICallState.instance.isCameraOpen.value != true)
if TUICallState.instance.isCameraOpen.value == true {
CallEngineManager.instance.closeCamera()
virtualBackgroundButton.button.isEnabled = false
switchCameraBtn.button.isEnabled = false
} else {
guard let videoViewEntity = VideoFactory.instance.viewMap[TUICallState.instance.selfUser.value.id.value] else { return }
CallEngineManager.instance.openCamera(videoView: videoViewEntity.videoView)
virtualBackgroundButton.button.isEnabled = true
switchCameraBtn.button.isEnabled = true
}
}
func updateCloseCameraBtn(open: Bool) {
closeCameraBtn.updateTitle(title: TUICallKitLocalize(key: open ? "TUICallKit.cameraOn" : "TUICallKit.cameraOff") ?? "")
if let image = TUICallKitCommon.getBundleImage(name: open ? "icon_camera_on" : "icon_camera_off") {
closeCameraBtn.updateImage(image: image)
}
}
func hangupTouchEvent(sender: UIButton) {
CallEngineManager.instance.hangup()
}
func switchCameraTouchEvent(sender: UIButton ) {
CallEngineManager.instance.switchCamera()
}
func virtualBackgroundTouchEvent(sender: UIButton) {
CallEngineManager.instance.setBlurBackground()
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
if TUICallState.instance.showVirtualBackgroundButton {
TUICallState.instance.enableBlurBackground.addObserver(enableBlurBackgroundObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateVirtualBackgroundButton()
})
}
}
func updateVirtualBackgroundButton() {
let imageName = TUICallState.instance.enableBlurBackground.value ? "icon_big_virtual_background_on" : "icon_big_virtual_background_off"
if let image = TUICallKitCommon.getBundleImage(name: imageName) {
virtualBackgroundButton.updateImage(image: image)
}
}
}

View File

@@ -0,0 +1,192 @@
//
// IncomingFloatView.swift
// TUICallKit-Swift
//
// Created by noah on 2024/3/15.
//
import UIKit
import SnapKit
class IncomingFloatView: UIView {
let remoteUserListObserver = Observer()
let mediaTypeObserver = Observer()
let userHeadImageView: UIImageView = {
let userHeadImageView = UIImageView(frame: CGRect.zero)
userHeadImageView.layer.masksToBounds = true
userHeadImageView.layer.cornerRadius = 7.0
if let image = TUICallKitCommon.getBundleImage(name: "default_user_icon") {
userHeadImageView.image = image
}
return userHeadImageView
}()
let userNameLabel: UILabel = {
let userNameLabel = UILabel(frame: CGRect.zero)
userNameLabel.textColor = UIColor.t_colorWithHexString(color: "#D5E0F2")
userNameLabel.font = UIFont.boldSystemFont(ofSize: 18.0)
userNameLabel.backgroundColor = UIColor.clear
userNameLabel.textAlignment = .center
return userNameLabel
}()
let callStatusTipView: UILabel = {
let callStatusTipLabel = UILabel(frame: CGRect.zero)
callStatusTipLabel.textColor = UIColor.t_colorWithHexString(color: "#C5CCDB")
callStatusTipLabel.font = UIFont.boldSystemFont(ofSize: 12.0)
callStatusTipLabel.textAlignment = .left
return callStatusTipLabel
}()
lazy var rejectBtn: UIButton = {
let btn = UIButton(type: .system)
if let image = TUICallKitCommon.getBundleImage(name: "icon_hangup") {
btn.setBackgroundImage(image, for: .normal)
}
btn.addTarget(self, action: #selector(rejectTouchEvent(sender: )), for: .touchUpInside)
return btn
}()
lazy var acceptBtn: UIButton = {
let btn = UIButton(type: .system)
let imageStr = TUICallState.instance.mediaType.value == .video ? "icon_video_dialing" : "icon_dialing"
if let image = TUICallKitCommon.getBundleImage(name: imageStr) {
btn.setBackgroundImage(image, for: .normal)
}
btn.addTarget(self, action: #selector(acceptTouchEvent(sender: )), for: .touchUpInside)
return btn
}()
override init(frame: CGRect) {
super.init(frame: frame)
userHeadImageView.layer.masksToBounds = true
self.layer.cornerRadius = 10.0
backgroundColor = UIColor.t_colorWithHexString(color: "#22262E")
let tap = UITapGestureRecognizer(target: self, action: #selector(showCallView(sender:)))
self.addGestureRecognizer(tap)
callStatusTipView.text = getCallStatusTipText()
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.remoteUserList.removeObserver(remoteUserListObserver)
TUICallState.instance.mediaType.removeObserver(mediaTypeObserver)
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(userHeadImageView)
addSubview(userNameLabel)
addSubview(callStatusTipView)
addSubview(rejectBtn)
addSubview(acceptBtn)
}
func activateConstraints() {
self.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
userHeadImageView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16.scaleWidth())
make.centerY.equalTo(self)
make.width.height.equalTo(60.scaleWidth())
}
userNameLabel.snp.makeConstraints { make in
make.top.equalTo(userHeadImageView).offset(10.scaleWidth())
make.leading.equalTo(userHeadImageView.snp.trailing).offset(12.scaleWidth())
}
callStatusTipView.snp.makeConstraints { make in
make.leading.equalTo(userHeadImageView.snp.trailing).offset(12.scaleWidth())
make.bottom.equalTo(userHeadImageView).offset(-10.scaleWidth())
}
rejectBtn.snp.makeConstraints { make in
make.trailing.equalTo(acceptBtn.snp.leading).offset(-22.scaleWidth())
make.centerY.equalTo(self)
make.width.height.equalTo(36.scaleWidth())
}
acceptBtn.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-16.scaleWidth())
make.centerY.equalTo(self)
make.width.height.equalTo(36.scaleWidth())
}
}
// MARK: Event Action
@objc func showCallView(sender: UIButton) {
self.removeFromSuperview()
WindowManager.instance.showCallWindow(false)
}
@objc func rejectTouchEvent(sender: UIButton) {
self.removeFromSuperview()
CallEngineManager.instance.reject()
}
@objc func acceptTouchEvent(sender: UIButton) {
self.removeFromSuperview()
CallEngineManager.instance.accept()
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
remoteUserListChanged()
mediaTypeChanged()
}
func remoteUserListChanged() {
TUICallState.instance.remoteUserList.addObserver(remoteUserListObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.setUserImageAndName()
})
}
func mediaTypeChanged() {
TUICallState.instance.mediaType.addObserver(mediaTypeObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.callStatusTipView.text = self.getCallStatusTipText()
})
}
func setUserImageAndName() {
let remoteUser = TUICallState.instance.remoteUserList.value.first ?? User()
userNameLabel.text = User.getUserDisplayName(user: remoteUser)
if let url = URL(string: remoteUser.avatar.value) {
userHeadImageView.sd_setImage(with: url)
}
}
func getCallStatusTipText() -> String {
if TUICallState.instance.scene.value == .group {
return TUICallKitLocalize(key: "TUICallKit.Group.inviteToGroupCall") ?? ""
}
var tipLabelText = String()
switch TUICallState.instance.mediaType.value {
case .audio:
tipLabelText = TUICallKitLocalize(key: "TUICallKit.inviteToAudioCall") ?? ""
case .video:
tipLabelText = TUICallKitLocalize(key: "TUICallKit.inviteToVideoCall") ?? ""
case .unknown:
break
default:
break
}
return tipLabelText
}
}

View File

@@ -0,0 +1,88 @@
//
// SelectGroupMemberCell.swift
// TUICallKit
//
// Created by vincepzhang on 2023/5/12.
//
import Foundation
class SelectGroupMemberCell: UITableViewCell {
let userImageView = {
let view = UIImageView(frame: CGRect.zero)
view.layer.cornerRadius = 2
view.layer.masksToBounds = true
return view
}()
let nameLabel = {
let label = UILabel(frame: CGRect.zero)
label.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_select_group_member_name_color",
defaultHex: "#242424")
return label
}()
let selectImageView = {
let view = UIImageView(frame: CGRect.zero)
return view
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_select_group_member_cell_bg_color",
defaultHex: "#FFFFFF")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var isViewReady = false
override func didMoveToWindow() {
if isViewReady {
return
}
constructViewHierarchy()
activateConstraints()
}
func constructViewHierarchy() {
contentView.addSubview(userImageView)
contentView.addSubview(nameLabel)
contentView.addSubview(selectImageView)
}
func activateConstraints() {
selectImageView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(12)
make.width.height.equalTo(20)
make.centerY.equalToSuperview()
}
userImageView.snp.makeConstraints { make in
make.leading.equalTo(selectImageView.snp.trailing).offset(12)
make.width.height.equalTo(30)
make.centerY.equalToSuperview()
}
nameLabel.snp.makeConstraints { make in
make.leading.equalTo(self.userImageView.snp.trailing).offset(12)
make.trailing.equalToSuperview().offset(-12)
make.height.equalTo(40)
make.centerY.equalToSuperview()
}
}
func configCell(user: User, isSelect: Bool) {
backgroundColor = UIColor.clear
userImageView.sd_setImage(with: URL(string: user.avatar.value), placeholderImage: TUICallKitCommon.getBundleImage(name: "default_user_icon"))
if isSelect {
selectImageView.image = TUICallKitCommon.getBundleImage(name: "icon_check_box_group_selected")
} else {
selectImageView.image = TUICallKitCommon.getBundleImage(name: "icon_check_box_group_unselected")
}
nameLabel.text = User.getUserDisplayName(user: user)
}
}

View File

@@ -0,0 +1,191 @@
//
// SelectGroupMemberViewController.swift
// TUICallKit
//
// Created by vincepzhang on 2023/5/12.
//
import Foundation
import TUICore
class SelectGroupMemberViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let viewModel = SelectGroupMemberViewModel()
let groupMemberObserver = Observer()
lazy var navigationView: UIView = {
let navigationView = UIView()
navigationView.backgroundColor = TUICoreDefineConvert.getTUICoreDynamicColor(colorKey: "head_bg_gradient_start_color",
defaultHex: "#EBF0F6")
return navigationView
}()
lazy var leftBtn: UIButton = {
let leftBtn = UIButton(type: .custom)
leftBtn.addTarget(self, action: #selector(goBack), for: .touchUpInside)
let defaultImage = TUICallKitCommon.getBundleImage(name: "icon_nav_back") ?? UIImage()
let leftBtnImage = TUICoreDefineConvert.getTUIDynamicImage(imageKey: "icon_nav_back_image",
module: TUIThemeModule.calling,
defaultImage: defaultImage)
leftBtn.setImage(leftBtnImage, for: .normal)
return leftBtn
}()
lazy var centerLabel: UILabel = {
let centerLabel = UILabel(frame: CGRect.zero)
centerLabel.font = UIFont.boldSystemFont(ofSize: 16.0)
centerLabel.textAlignment = .center
centerLabel.text = TUICallKitLocalize(key: "TUICallKit.Recents.addUser")
centerLabel.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_nav_title_text_color",
defaultHex: "#000000")
return centerLabel
}()
lazy var rightBtn: UIButton = {
let rightBtn = UIButton(type: .system)
rightBtn.addTarget(self, action: #selector(addUser), for: .touchUpInside)
rightBtn.titleLabel?.font = UIFont.systemFont(ofSize: 17.0)
rightBtn.setTitle(TUICallKitLocalize(key: "TUICallKit.determine"), for: .normal)
rightBtn.setTitleColor(TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_nav_item_title_text_color",
defaultHex: "#000000"), for: .normal)
return rightBtn
}()
let selectTableView: UITableView = {
let selectTableView = UITableView(frame: CGRect.zero)
selectTableView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_select_group_member_bg_color",
defaultHex: "#F2F2F2")
return selectTableView
}()
// MARK: UI Specification Processing
override func viewDidLoad() {
constructViewHierarchy()
activateConstraints()
bindInteraction()
registerObserve()
}
func constructViewHierarchy() {
view.addSubview(navigationView)
view.addSubview(leftBtn)
view.addSubview(leftBtn)
view.addSubview(centerLabel)
view.addSubview(rightBtn)
view.addSubview(selectTableView)
}
func activateConstraints() {
navigationView.snp.makeConstraints({ make in
make.leading.trailing.top.equalToSuperview()
make.height.equalTo(StatusBar_Height + 44)
})
leftBtn.snp.makeConstraints({ make in
make.leading.equalToSuperview().offset(12)
make.centerY.equalTo(centerLabel)
make.width.height.equalTo(30)
})
centerLabel.snp.makeConstraints({ make in
make.top.equalToSuperview().offset(StatusBar_Height)
make.centerX.equalToSuperview()
make.width.equalTo(Screen_Width * 2 / 3)
make.height.equalTo(44)
})
rightBtn.snp.makeConstraints({ make in
make.trailing.equalToSuperview().offset(-12)
make.centerY.equalTo(centerLabel)
})
selectTableView.snp.makeConstraints({ make in
make.top.equalTo(centerLabel.snp.bottom)
make.leading.trailing.bottom.equalToSuperview()
})
}
func bindInteraction() {
selectTableView.dataSource = self
selectTableView.delegate = self
selectTableView.register(SelectGroupMemberCell.self, forCellReuseIdentifier: NSStringFromClass(SelectGroupMemberCell.self))
}
func registerObserve() {
viewModel.groupMemberList.addObserver(groupMemberObserver) {[weak self] _, _ in
guard let self = self else { return }
self.selectTableView.reloadData()
}
}
// MARK: Action
@objc func addUser() {
var userIds: [String] = []
for state in viewModel.groupMemberStateOrigin {
if state.value {
continue
}
if let isSelect = viewModel.groupMemberState[state.key] {
if isSelect {
userIds.append(state.key)
}
}
}
if userIds.count + TUICallState.instance.remoteUserList.value.count >= MAX_USER {
TUITool.makeToast(TUICallKitLocalize(key: "TUICallKit.User.Exceed.Limit"))
return
}
CallEngineManager.instance.inviteUser(userIds: userIds)
dismiss(animated: true)
}
@objc func goBack() {
dismiss(animated: true)
}
}
extension SelectGroupMemberViewController {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.groupMemberList.value.count + 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: NSStringFromClass(SelectGroupMemberCell.self),
for: indexPath) as? SelectGroupMemberCell else {
return UITableViewCell()
}
if indexPath.row == 0 {
cell.configCell(user: viewModel.selfUser.value, isSelect: true)
} else {
let user = viewModel.groupMemberList.value[indexPath.row - 1]
let isSelect = viewModel.groupMemberState[user.id.value]
cell.configCell(user: user, isSelect: isSelect ?? false)
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0 {
return
}
let user = viewModel.groupMemberList.value[indexPath.row - 1]
if viewModel.groupMemberStateOrigin[user.id.value] ?? false {
return
}
if viewModel.groupMemberState[user.id.value] ?? false {
viewModel.groupMemberState[user.id.value] = false
} else {
viewModel.groupMemberState[user.id.value] = true
}
selectTableView.reloadData()
}
}

View File

@@ -0,0 +1,102 @@
//
// InviteUserButton.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/15.
//
import Foundation
import TUICore
class InviteUserButton: UIView {
let mediaTypeObserver = Observer()
let inviteUserButton: InviteUserCustomButton = {
let inviteUserButton = InviteUserCustomButton(type: .system)
return inviteUserButton
}()
override init(frame: CGRect) {
super.init(frame: frame)
updateImage()
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.mediaType.removeObserver(mediaTypeObserver)
}
// 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(inviteUserButton)
}
func activateConstraints() {
inviteUserButton.snp.makeConstraints { make in
make.center.equalTo(self)
make.width.height.equalTo(24)
}
}
func bindInteraction() {
inviteUserButton.addTarget(self, action: #selector(clickButton(sender: )), for: .touchUpInside)
}
// MARK: Action Event
@objc func clickButton(sender: UIButton) {
let selectGroupMemberVC = SelectGroupMemberViewController()
selectGroupMemberVC.modalPresentationStyle = .fullScreen
UIWindow.getKeyWindow()?.rootViewController?.present(selectGroupMemberVC, animated: false)
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
TUICallState.instance.mediaType.addObserver(mediaTypeObserver) { [weak self] newValue, _ in
guard let self = self else { return }
self.updateImage()
}
}
func updateImage() {
if let image = TUICallKitCommon.getBundleImage(name: "icon_add_user") {
inviteUserButton.setBackgroundImage(image, for: .normal)
}
}
func tui_valueCallback(param: [AnyHashable: Any]) {
guard let selectUserList = param[TUICore_TUIContactObjectFactory_SelectGroupMemberVC_ResultUserList] as? [TUIUserModel] else { return }
if selectUserList.count > 0 {
return
}
var userIds: [String] = []
for user in selectUserList {
userIds.append(user.userId)
}
CallEngineManager.instance.inviteUser(userIds: userIds)
}
}
class InviteUserCustomButton: UIButton {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let expandedBounds = bounds.insetBy(dx: -6, dy: -6)
return expandedBounds.contains(point) ? self : nil
}
}

View File

@@ -0,0 +1,68 @@
//
// JoinInGroupCallUserCell.swift
// TUICallKit-Swift
//
// Created by noah on 2024/2/27.
//
import Foundation
class JoinInGroupCallUserCell: UICollectionViewCell {
private var user: User = User()
private let userIcon = {
let imageView = UIImageView(frame: CGRect.zero)
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = 4.0
if let image = TUICallKitCommon.getBundleImage(name: "default_user_icon") {
imageView.image = image
}
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
}
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()
isViewReady = true
}
private func constructViewHierarchy() {
contentView.addSubview(userIcon)
}
private func activateConstraints() {
userIcon.snp.makeConstraints { make in
make.edges.equalTo(self.contentView)
}
}
func setModel(user: User) {
self.user = user
setUserIcon()
}
private func setUserIcon() {
let userImage: UIImage? = TUICallKitCommon.getBundleImage(name: "default_user_icon")
if user.avatar.value == "" {
guard let image = userImage else { return }
userIcon.image = image
} else {
userIcon.sd_setImage(with: URL(string: user.avatar.value), placeholderImage: userImage)
}
}
}

View File

@@ -0,0 +1,260 @@
//
// JoinInGroupCallView.swift
// TUICallKit-Swift
//
// Created by noah on 2024/1/23.
//
import UIKit
import SnapKit
import TUICore
import TUICallEngine
let kJoinGroupCallViewDefaultHeight: CGFloat = 52.0
let kJoinGroupCallViewExpandHeight: CGFloat = 225.0
let kJoinGroupCallItemWidth: CGFloat = 50.0
let kJoinGroupCallSpacing: CGFloat = 12.0
protocol JoinInGroupCallViewDelegate: AnyObject {
func updatePageContent(isExpand: Bool)
func joinInGroupCall()
}
class JoinInGroupCallView: UIView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
weak var delegate: JoinInGroupCallViewDelegate?
var listDate = Array<User>()
lazy var bottomContainerView: UIView = {
let view = UIView()
view.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_join_group_bottom_container_bg_color",
defaultHex: "#FFFFFF")
view.layer.cornerRadius = 6.0
view.layer.masksToBounds = true
return view
}()
lazy var titleIcon: UIImageView = {
let imageView = UIImageView()
imageView.image = TUICallKitCommon.getBundleImage(name: "icon_join_group")
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel(frame: .zero)
label.font = UIFont.systemFont(ofSize: 12.0)
label.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_join_group_title_color", defaultHex: "#999999")
label.textAlignment = .left
return label
}()
lazy var expandButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(TUICallKitCommon.getBundleImage(name: "icon_join_group_expand"), for: .normal)
button.setImage(TUICallKitCommon.getBundleImage(name: "icon_join_group_zoom"), for: .selected)
button.imageView?.contentMode = .scaleAspectFit
return button
}()
lazy var expandView: UIView = {
let view = UIView()
view.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_join_group_expand_bg_color", defaultHex: "#EEF0F2")
view.isHidden = true
view.layer.cornerRadius = 6.0
view.layer.masksToBounds = true
return view
}()
lazy var collectionView: UICollectionView = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .horizontal
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.backgroundColor = .clear
return collectionView
}()
lazy var lineView: UIView = {
let view = UIView()
view.alpha = 0.1
view.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_join_group_line_color", defaultHex: "#707070")
return view
}()
lazy var joinButton: UIButton = {
let button = UIButton(type: .custom)
button.titleLabel?.font = UIFont(name: "PingFangSC-Semibold", size: 14.0)
button.setTitleColor(TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_join_group_button_color", defaultHex: "#333333"),
for: .normal)
button.setTitle(TUICallKitLocalize(key: "TUICallKit.JoinGroupView.join"), for: .normal)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_join_group_bg_color", defaultHex: "#ECF0F5")
self.frame = CGRect(x: 0, y: 0, width: Screen_Width, height: kJoinGroupCallViewDefaultHeight)
let tap = UITapGestureRecognizer(target: self, action: #selector(expandButtonClick(sender:)))
self.addGestureRecognizer(tap)
}
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(bottomContainerView)
bottomContainerView.addSubview(titleIcon)
bottomContainerView.addSubview(titleLabel)
bottomContainerView.addSubview(expandButton)
bottomContainerView.addSubview(expandView)
expandView.addSubview(collectionView)
expandView.addSubview(lineView)
expandView.addSubview(joinButton)
}
func activateConstraints() {
bottomContainerView.snp.makeConstraints { make in
make.top.equalTo(self).offset(8)
make.leading.equalTo(self).offset(16)
make.center.equalTo(self)
}
titleIcon.snp.makeConstraints { make in
make.top.equalTo(bottomContainerView).offset(8)
make.leading.equalTo(bottomContainerView).offset(16)
make.width.height.equalTo(20)
}
titleLabel.snp.makeConstraints { make in
make.centerY.equalTo(titleIcon)
make.leading.equalTo(titleIcon.snp.trailing).offset(10)
make.width.equalTo(200)
make.height.equalTo(20)
}
expandButton.snp.makeConstraints { make in
make.centerY.equalTo(titleIcon)
make.leading.equalTo(titleLabel.snp.trailing).offset(10)
make.trailing.equalTo(bottomContainerView).offset(-16)
make.width.height.equalTo(30)
}
expandView.snp.makeConstraints { make in
make.top.equalTo(bottomContainerView).offset(36)
make.leading.equalTo(bottomContainerView).offset(16)
make.trailing.equalTo(bottomContainerView).offset(-16)
make.height.equalTo(157)
}
collectionView.snp.makeConstraints { make in
make.top.equalTo(expandView).offset(37)
make.leading.equalTo(expandView).offset(10)
make.trailing.equalTo(expandView).offset(-10)
make.height.equalTo(kJoinGroupCallItemWidth)
}
lineView.snp.makeConstraints { make in
make.top.equalTo(expandView).offset(117)
make.leading.trailing.equalTo(self.expandView)
make.height.equalTo(1)
}
joinButton.snp.makeConstraints { make in
make.top.equalTo(lineView.snp.bottom).offset(5)
make.centerX.equalTo(self.expandView)
make.width.equalTo(100)
make.height.equalTo(30)
}
}
func bindInteraction() {
expandButton.addTarget(self, action: #selector(expandButtonClick(sender: )), for: .touchUpInside)
joinButton.addTarget(self, action: #selector(joinButtonClick(sender: )), for: .touchUpInside)
collectionView.register(JoinInGroupCallUserCell.self,
forCellWithReuseIdentifier: String(describing: JoinInGroupCallUserCell.self))
}
@objc func expandButtonClick(sender: UIButton) {
expandView.isHidden = expandButton.isSelected
expandButton.isSelected = !expandButton.isSelected
delegate?.updatePageContent(isExpand: expandButton.isSelected)
}
@objc func joinButtonClick(sender: UIButton) {
expandButton.isSelected = false;
delegate?.joinInGroupCall()
}
func updateView(with userList: [User], callMediaType: TUICallMediaType) {
listDate.removeAll()
userList.forEach {
obj in if obj.id.value != TUILogin.getUserID() { listDate.append(obj) } }
titleLabel.isHidden = listDate.isEmpty
titleLabel.text = String(format: TUICallKitLocalize(key: "TUICallKit.JoinGroupView.title") ?? "",
listDate.count,
getCallMediaTypeStr(with: callMediaType))
titleLabel.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
collectionView.reloadData()
}
func getCallMediaTypeStr(with callMediaType: TUICallMediaType) -> String {
var callMediaTypeStr: String = ""
if callMediaType == .audio {
callMediaTypeStr = TUICallKitLocalize(key: "TUICallKit.JoinGroupView.audioCall") ?? ""
} else if callMediaType == .video {
callMediaTypeStr = TUICallKitLocalize(key: "TUICallKit.JoinGroupView.videoCall") ?? ""
}
return callMediaTypeStr
}
}
// MARK: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout
extension JoinInGroupCallView {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return listDate.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: JoinInGroupCallUserCell.self),
for: indexPath) as! JoinInGroupCallUserCell
let model = listDate[indexPath.item]
cell.setModel(user: model)
return cell
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: kJoinGroupCallItemWidth, height: kJoinGroupCallItemWidth)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return kJoinGroupCallSpacing
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
let totalCellWidth: CGFloat = kJoinGroupCallItemWidth * CGFloat(listDate.count)
let totalSpacingWidth: CGFloat = kJoinGroupCallSpacing * (CGFloat(listDate.count - 1) < 0 ? 0 : CGFloat(listDate.count - 1))
let leftInset = (CGFloat(collectionView.bounds.size.width) - (totalCellWidth + totalSpacingWidth)) / 2
if leftInset > 0 {
let rightInset = leftInset
let sectionInset = UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)
return sectionInset
}
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
}

View File

@@ -0,0 +1,185 @@
//
// TUICallRecordCallsCell.swift
//
//
// Created by vincepzhang on 2023/8/28.
//
import Foundation
import UIKit
import TUICallEngine
class TUICallRecordCallsCell: UITableViewCell {
private let faceURLObserver = Observer()
private let titleObserver = Observer()
typealias TUICallRecordCallsCellMoreBtnClickedHandler = () -> Void
var moreBtnClickedHandler: TUICallRecordCallsCellMoreBtnClickedHandler = {}
private var isViewReady = false
private var viewModel: TUICallRecordCallsCellViewModel = TUICallRecordCallsCellViewModel(TUICallRecords())
private lazy var avatarImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = 20
imageView.clipsToBounds = true
return imageView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 1
label.font = UIFont(name: "PingFangHK-Semibold", size: 14)
label.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
return label
}()
private lazy var mediaTypeImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}()
private lazy var resultLabel: UILabel = {
let label = UILabel()
label.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_recents_cell_subtitle_color",
defaultHex: "#888888")
label.font = UIFont.systemFont(ofSize: 12)
label.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
return label
}()
private lazy var timeLabel: UILabel = {
let label = UILabel()
label.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_recents_cell_time_color",
defaultHex: "#BBBBBB")
label.font = UIFont.systemFont(ofSize: 12)
label.textAlignment = TUICoreDefineConvert.getIsRTL() ? .left : .right
return label
}()
private lazy var moreButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(TUICallKitCommon.getBundleImage(name: "ic_recents_more"), for: .normal)
button.imageView?.contentMode = .scaleAspectFit
return button
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_recents_cell_bg_color",
defaultHex: "#FFFFFF")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
unregisterObserve()
}
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady {
return
}
isViewReady = true
constructViewHierarchy()
activateConstraints()
bindInteraction()
}
override func prepareForReuse() {
super.prepareForReuse()
}
private func constructViewHierarchy() {
contentView.addSubview(avatarImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(mediaTypeImageView)
contentView.addSubview(resultLabel)
contentView.addSubview(timeLabel)
contentView.addSubview(moreButton)
}
private func activateConstraints() {
avatarImageView.snp.makeConstraints { make in
make.centerY.equalTo(contentView)
make.leading.equalTo(contentView).offset(16)
make.width.height.equalTo(40)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(contentView).offset(14)
make.leading.equalTo(avatarImageView.snp.trailing).offset(8)
make.trailing.lessThanOrEqualTo(timeLabel.snp.leading).offset(-20)
}
mediaTypeImageView.snp.makeConstraints { make in
make.bottom.equalTo(contentView).offset(-14)
make.leading.equalTo(avatarImageView.snp.trailing).offset(8)
make.width.equalTo(19)
make.height.equalTo(12)
}
resultLabel.snp.makeConstraints { make in
make.centerY.equalTo(mediaTypeImageView)
make.leading.equalTo(mediaTypeImageView.snp.trailing).offset(4)
make.width.equalTo(100)
}
timeLabel.snp.makeConstraints { make in
make.centerY.equalTo(contentView)
make.trailing.equalTo(moreButton.snp.leading).offset(-4)
make.width.equalTo(100)
}
moreButton.snp.makeConstraints { make in
make.centerY.equalTo(contentView)
make.trailing.equalTo(-8)
make.width.height.equalTo(24)
}
}
private func bindInteraction() {
moreButton.addTarget(self, action: #selector(moreButtonClick(_:)), for: .touchUpInside)
}
func configViewModel(_ viewModel: TUICallRecordCallsCellViewModel) {
self.viewModel = viewModel
registerObserve()
titleLabel.text = viewModel.titleLabelStr.value
avatarImageView.sd_setImage(with: URL(string: viewModel.faceURL.value), placeholderImage: viewModel.avatarImage)
resultLabel.text = viewModel.resultLabelStr
timeLabel.text = viewModel.timeLabelStr
mediaTypeImageView.image = TUICallKitCommon.getBundleImage(name: viewModel.mediaTypeImageStr)
if viewModel.callRecord.result == .missed {
titleLabel.textColor = UIColor.red
} else {
titleLabel.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_recents_cell_title_color",
defaultHex: "#000000")
}
}
func registerObserve() {
viewModel.faceURL.addObserver(faceURLObserver) { [weak self] _, _ in
guard let self = self else { return }
self.avatarImageView.sd_setImage(with: URL(string: self.viewModel.faceURL.value),
placeholderImage: self.viewModel.avatarImage)
}
viewModel.titleLabelStr.addObserver(titleObserver) { [weak self] _, _ in
guard let self = self else { return }
self.titleLabel.text = self.viewModel.titleLabelStr.value
}
}
func unregisterObserve() {
viewModel.faceURL.removeObserver(faceURLObserver)
viewModel.titleLabelStr.removeObserver(titleObserver)
}
@objc private func moreButtonClick(_ button: UIButton) {
moreBtnClickedHandler()
}
}

View File

@@ -0,0 +1,305 @@
//
// TUICallRecordCallsViewController.swift
//
//
// Created by vincepzhang on 2023/8/28.
//
import Foundation
import UIKit
import SnapKit
import TUICore
public class TUICallRecordCallsViewController: UIViewController {
private let viewModel = TUICallRecordCallsViewModel()
private let dataSourceObserver = Observer()
private lazy var containerView: UIView = {
let view = UIView()
if viewModel.recordCallsUIStyle == .classic {
view.backgroundColor = TUICoreDefineConvert.getTUICoreDynamicColor(colorKey: "head_bg_gradient_start_color",
defaultHex: "#EBF0F6")
} else {
view.backgroundColor = TUICoreDefineConvert.getTUICoreDynamicColor(colorKey: "callkit_recents_bg_color",
defaultHex: "#FFFFFF")
}
return view
}()
private lazy var editButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(TUICallKitCommon.getBundleImage(name: "ic_calls_edit"), for: .normal)
button.titleLabel?.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
button.imageView?.contentMode = .scaleAspectFit
return button
}()
private lazy var clearButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitle(TUICallKitLocalize(key: "TUICallKit.Recents.clear"), for: .normal)
button.titleLabel?.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
button.contentHorizontalAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
button.setTitleColor(TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_nav_title_text_color",
defaultHex: "#000000"), for: .normal)
button.isHidden = true
button.titleLabel?.sizeToFit()
button.titleLabel?.adjustsFontSizeToFitWidth = true
return button
}()
private lazy var doneButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitle(TUICallKitLocalize(key: "TUICallKit.Recents.done"), for: .normal)
button.contentHorizontalAlignment = .right
button.setTitleColor(TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_nav_item_title_text_color",
defaultHex: "#000000"), for: .normal)
button.titleLabel?.sizeToFit()
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.isHidden = true
return button
}()
private lazy var segmentedControl: UISegmentedControl = {
let items = [TUICallKitLocalize(key: "TUICallKit.Recents.all"), TUICallKitLocalize(key: "TUICallKit.Recents.missed")]
let control = UISegmentedControl(items: items as [Any])
control.selectedSegmentIndex = 0
return control
}()
private lazy var notRecordCallsLabel: UILabel = {
let label = UILabel(frame: .zero)
label.textColor = UIColor.gray
label.font = UIFont.systemFont(ofSize: 20)
label.textAlignment = .center
label.isHidden = true
return label
}()
private lazy var tableHeaderView: UIView = {
let tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 80))
let label = UILabel()
label.textColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_recents_tableHeader_title_text_color",
defaultHex: "#000000")
label.text = TUICallKitLocalize(key: "TUICallKit.Recents.calls")
label.font = UIFont(name: "PingFangHK-Semibold", size: 34)
label.textAlignment = TUICoreDefineConvert.getIsRTL() ? .right : .left
tableHeaderView.addSubview(label)
label.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.equalTo(tableHeaderView).offset(20)
}
return tableHeaderView
}()
private lazy var recordCallsList: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.tableHeaderView = tableHeaderView
tableView.backgroundColor = view.backgroundColor
return tableView
}()
init(recordCallsUIStyle: TUICallKitRecordCallsUIStyle) {
super.init(nibName: nil, bundle: nil)
viewModel.recordCallsUIStyle = recordCallsUIStyle
view.backgroundColor = TUICoreDefineConvert.getTUICallKitDynamicColor(colorKey: "callkit_recents_bg_color",
defaultHex: "#FFFFFF")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Observe
func registerObserve() {
NotificationCenter.default.addObserver(self, selector: #selector(windowDidChange),
name: UIWindow.didBecomeKeyNotification, object: nil)
viewModel.dataSource.addObserver(dataSourceObserver) {[weak self] _, _ in
guard let self = self else { return }
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.recordCallsList.reloadData()
}
}
}
func unregisterObserve() {
NotificationCenter.default.removeObserver(self, name: UIWindow.didBecomeKeyNotification, object: nil)
viewModel.dataSource.removeObserver(dataSourceObserver)
}
// Register the UI data refresh callback when the page appears, close it when the page disappears, to prevent the UI refresh from running in a non-main thread.
public override func viewWillAppear(_ animated: Bool) {
registerObserve()
viewModel.queryRecentCalls()
}
public override func viewWillDisappear(_ animated: Bool) {
unregisterObserve()
}
// MARK: UI Specification Processing
public override func viewDidLoad() {
super.viewDidLoad()
constructViewHierarchy()
activateConstraints()
bindInteraction()
viewModel.queryRecentCalls()
}
func constructViewHierarchy() {
view.addSubview(containerView)
containerView.addSubview(segmentedControl)
containerView.addSubview(editButton)
containerView.addSubview(clearButton)
containerView.addSubview(doneButton)
view.addSubview(notRecordCallsLabel)
view.addSubview(recordCallsList)
}
func activateConstraints() {
containerView.snp.makeConstraints { make in
make.top.centerX.width.equalTo(view)
make.height.equalTo(StatusBar_Height + 44)
}
segmentedControl.snp.makeConstraints { make in
make.top.equalTo(containerView).offset(StatusBar_Height + 6.0)
make.centerX.equalTo(containerView)
make.width.equalTo(180)
make.height.equalTo(32)
}
editButton.snp.makeConstraints { make in
make.centerY.equalTo(segmentedControl)
make.trailing.equalTo(containerView).offset(-20)
make.width.height.equalTo(32)
}
clearButton.snp.makeConstraints { make in
make.centerY.equalTo(segmentedControl)
make.leading.equalTo(containerView).offset(20)
make.height.equalTo(32)
}
doneButton.snp.makeConstraints { make in
make.centerY.equalTo(segmentedControl)
make.trailing.equalTo(containerView).offset(-20)
make.height.equalTo(32)
}
recordCallsList.snp.makeConstraints { make in
make.top.equalTo(view).offset(StatusBar_Height + 44)
make.centerX.width.bottom.equalTo(view)
}
notRecordCallsLabel.snp.makeConstraints { make in
make.center.width.equalTo(view)
}
}
func bindInteraction() {
recordCallsList.delegate = self
recordCallsList.dataSource = self
recordCallsList.register(TUICallRecordCallsCell.self, forCellReuseIdentifier: NSStringFromClass(TUICallRecordCallsCell.self))
clearButton.addTarget(self, action: #selector(clearButtonClick(_:)), for: .touchUpInside)
editButton.addTarget(self, action: #selector(editButtonClick(_:)), for: .touchUpInside)
doneButton.addTarget(self, action: #selector(doneButtonClick(_:)), for: .touchUpInside)
segmentedControl.addTarget(self, action: #selector(segmentSelectItem(_:)), for: .valueChanged)
}
@objc func windowDidChange() {
viewModel.queryRecentCalls()
}
@objc func clearButtonClick(_ button: UIButton) {
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let clearAction = UIAlertAction(title: TUICallKitLocalize(key: "TUICallKit.Recents.clear.all"),
style: .destructive) { [weak self] _ in
guard let self = self else { return }
self.recordCallsList.setEditing(false, animated: true)
self.setShowEditButton(true)
self.viewModel.deleteAllRecordCalls()
}
let cancelAction = UIAlertAction(title: TUICallKitLocalize(key: "TUICallKit.Recents.clear.cancel"), style: .cancel)
alertController.addAction(clearAction)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
@objc func editButtonClick(_ button: UIButton) {
recordCallsList.setEditing(true, animated: true)
setShowEditButton(false)
}
@objc func doneButtonClick(_ button: UIButton) {
recordCallsList.setEditing(false, animated: true)
setShowEditButton(true)
}
@objc func segmentSelectItem(_ sender: UISegmentedControl) {
var type: TUICallRecordCallsType = .all
if sender.selectedSegmentIndex == 1 {
type = .missed
}
viewModel.switchRecordCallsType(type)
}
func setShowEditButton(_ isShow: Bool) {
editButton.isHidden = !isShow
clearButton.isHidden = isShow
doneButton.isHidden = isShow
}
}
extension TUICallRecordCallsViewController: UITableViewDataSource, UITableViewDelegate {
public func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
public func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .delete
}
@available(iOS 11.0, *)
public func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteRowAction = UIContextualAction(style: .normal,
title: TUICallKitLocalize(key: "TUICallKit.Recents.delete"))
{ [weak self] action, sourceView, completionHandler in
guard let self = self else { return }
self.viewModel.deleteRecordCall(indexPath)
}
deleteRowAction.backgroundColor = .red
let config = UISwipeActionsConfiguration(actions: [deleteRowAction])
config.performsFirstActionWithFullSwipe = false
self.setShowEditButton(false)
return config
}
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 64.0
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.dataSource.value.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: NSStringFromClass(TUICallRecordCallsCell.self),
for: indexPath) as? TUICallRecordCallsCell else { return UITableViewCell() }
cell.moreBtnClickedHandler = { [weak self] in
guard let self = self else { return }
guard let navigationController = self.navigationController else { return }
self.viewModel.jumpUserInfoController(indexPath: indexPath, navigationController: navigationController)
}
cell.configViewModel(viewModel.dataSource.value[indexPath.row])
return cell
}
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
viewModel.repeatCall(indexPath)
}
}

View File

@@ -0,0 +1,74 @@
//
// TimerView.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/15.
//
import Foundation
import TUICallEngine
class TimerView: UIView {
let timeCountObserver = Observer()
lazy var timerLabel: UILabel = {
let timerLabel = UILabel(frame: CGRect.zero)
timerLabel.font = UIFont.boldSystemFont(ofSize: 15.0)
timerLabel.backgroundColor = UIColor.clear
timerLabel.textAlignment = .center
timerLabel.textColor = UIColor.t_colorWithHexString(color: "#D5E0F2")
timerLabel.text = GCDTimer.secondToHMSString(second: TUICallState.instance.timeCount.value)
return timerLabel
}()
override init(frame: CGRect) {
super.init(frame: frame)
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.timeCount.removeObserver(timeCountObserver)
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(timerLabel)
}
func activateConstraints() {
timerLabel.snp.makeConstraints { make in
make.edges.equalTo(self)
}
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
callTimeChange()
}
func callTimeChange() {
TUICallState.instance.timeCount.addObserver(timeCountObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
DispatchCallKitMainAsyncSafe {
self.timerLabel.text = GCDTimer.secondToHMSString(second: newValue)
}
})
}
}

View File

@@ -0,0 +1,83 @@
//
// GroupBackgroundView.swift
// TUICallKit
//
// Created by noah on 2023/11/8.
//
import Foundation
class GroupBackgroundView: UIView {
let selfUserObserver = Observer()
let userHeadImageView: UIImageView = {
let userHeadImageView = UIImageView(frame: CGRect.zero)
userHeadImageView.contentMode = .scaleAspectFill
return userHeadImageView
}()
let blurEffectView: UIVisualEffectView = {
let blurEffect = UIBlurEffect(style: .light)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.alpha = 0.65
return blurEffectView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setUserImage()
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.selfUser.removeObserver(selfUserObserver)
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(userHeadImageView)
addSubview(blurEffectView)
}
func activateConstraints() {
self.userHeadImageView.snp.makeConstraints { make in
make.edges.equalTo(self)
}
self.blurEffectView.snp.makeConstraints { make in
make.edges.equalTo(self)
}
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
selfUserChanged()
}
func selfUserChanged() {
TUICallState.instance.selfUser.addObserver(selfUserObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.setUserImage()
})
}
// MARK: Update UI
func setUserImage() {
let selfUser = TUICallState.instance.selfUser.value
userHeadImageView.sd_setImage(with: URL(string: selfUser.avatar.value),
placeholderImage: TUICallKitCommon.getBundleImage(name: "default_user_icon"))
}
}

View File

@@ -0,0 +1,109 @@
//
// GroupCallerUserInfoView.swift
// TUICallKit
//
// Created by vincepzhang on 2023/3/7.
//
import Foundation
class GroupCallerUserInfoView: UIView {
let remoteUserListObserver = Observer()
let userHeadImageView: UIImageView = {
let userHeadImageView = UIImageView(frame: CGRect.zero)
userHeadImageView.layer.masksToBounds = true
userHeadImageView.layer.cornerRadius = 6.0
if let image = TUICallKitCommon.getBundleImage(name: "default_user_icon") {
userHeadImageView.image = image
}
return userHeadImageView
}()
let userNameLabel: UILabel = {
let userNameLabel = UILabel(frame: CGRect.zero)
userNameLabel.textColor = UIColor.t_colorWithHexString(color: "#D5E0F2")
userNameLabel.font = UIFont.boldSystemFont(ofSize: 24.0)
userNameLabel.backgroundColor = UIColor.clear
userNameLabel.textAlignment = .center
return userNameLabel
}()
let waitingInviteLabel: UILabel = {
let waitingInviteLabel = UILabel(frame: CGRect.zero)
waitingInviteLabel.textColor = UIColor.t_colorWithHexString(color: "#8F9AB2")
waitingInviteLabel.font = UIFont.boldSystemFont(ofSize: 14.0)
waitingInviteLabel.backgroundColor = UIColor.clear
waitingInviteLabel.text = TUICallKitLocalize(key: "TUICallKit.Group.inviteToGroupCall") ?? ""
waitingInviteLabel.textAlignment = .center
return waitingInviteLabel
}()
override init(frame: CGRect) {
super.init(frame: frame)
setUserImageAndName()
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.remoteUserList.removeObserver(remoteUserListObserver)
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(userHeadImageView)
addSubview(userNameLabel)
addSubview(waitingInviteLabel)
}
func activateConstraints() {
userHeadImageView.snp.makeConstraints { make in
make.top.centerX.equalTo(self)
make.size.equalTo(CGSize(width: 100.scaleWidth(), height: 100.scaleWidth()))
}
userNameLabel.snp.makeConstraints { make in
make.top.equalTo(userHeadImageView.snp.bottom).offset(10.scaleHeight())
make.centerX.equalTo(self)
make.width.equalTo(self)
make.height.equalTo(30)
}
waitingInviteLabel.snp.makeConstraints { make in
make.top.equalTo(userNameLabel.snp.bottom).offset(20.scaleHeight())
make.centerX.equalTo(self)
make.width.equalTo(self)
make.height.equalTo(20)
}
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
TUICallState.instance.remoteUserList.addObserver(remoteUserListObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.setUserImageAndName()
})
}
// MARK: Update UI
func setUserImageAndName() {
let remoteUser = TUICallState.instance.remoteUserList.value.first ?? User()
userNameLabel.text = User.getUserDisplayName(user: remoteUser)
if let url = URL(string: remoteUser.avatar.value) {
userHeadImageView.sd_setImage(with: url)
}
}
}

View File

@@ -0,0 +1,67 @@
//
// InviteeAvatarCell.swift
// TUICallKit
//
// Created by vincepzhang on 2023/3/2.
//
import Foundation
class InviteeAvatarCell: UICollectionViewCell {
private var user: User = User()
private let userIcon = {
let imageView = UIImageView(frame: CGRect.zero)
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = 2.0
if let image = TUICallKitCommon.getBundleImage(name: "default_user_icon") {
imageView.image = image
}
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
}
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()
isViewReady = true
}
private func constructViewHierarchy() {
contentView.addSubview(userIcon)
}
private func activateConstraints() {
userIcon.snp.makeConstraints { make in
make.edges.equalTo(self.contentView)
}
}
func initCell(user: User) {
self.user = user
setUserIcon()
}
private func setUserIcon() {
let userImage: UIImage? = TUICallKitCommon.getBundleImage(name: "default_user_icon")
if user.avatar.value == "" {
guard let image = userImage else { return }
userIcon.image = image
} else {
userIcon.sd_setImage(with: URL(string: user.avatar.value), placeholderImage: userImage)
}
}
}

View File

@@ -0,0 +1,157 @@
//
// InviteeAvatarListView.swift
// TUICallKit
//
// Created by vincepzhang on 2023/3/2.
//
import Foundation
private let kItemWidth = 32.scaleWidth()
private let kSpacing = 5.scaleWidth()
class InviteeAvatarListView: UIView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
let remoteUserListObserver = Observer()
let dataSource: Observable<[User]> = Observable(Array())
lazy var describeLabel: UILabel = {
let describeLabel = UILabel()
describeLabel.font = UIFont.systemFont(ofSize: 12.0)
describeLabel.textColor = UIColor.t_colorWithHexString(color: "#D5E0F2")
describeLabel.textAlignment = .center
describeLabel.isUserInteractionEnabled = false
describeLabel.text = TUICallKitLocalize(key: "TUICallKit.calleeTip") ?? ""
return describeLabel
}()
lazy var calleeCollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let calleeCollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
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)
var dataList = TUICallState.instance.remoteUserList.value
dataList.append(TUICallState.instance.selfUser.value)
dataSource.value = removeCallUser(remoteUserList: dataList)
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.remoteUserList.removeObserver(remoteUserListObserver)
}
func removeCallUser(remoteUserList: [User]) -> [User] {
let userList = remoteUserList.filter { $0.callRole.value != .call }
return userList
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
updateDescribeLabel()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(describeLabel)
addSubview(calleeCollectionView)
}
func activateConstraints() {
describeLabel.snp.makeConstraints { make in
make.bottom.equalTo(calleeCollectionView.snp.top).offset(-5.scaleWidth())
make.centerX.equalTo(self)
make.width.equalTo(Screen_Width)
make.height.equalTo(20)
}
calleeCollectionView.snp.makeConstraints { make in
make.centerX.equalTo(self)
make.width.equalTo(Screen_Width)
make.height.equalTo(40)
}
}
func bindInteraction() {
calleeCollectionView.register(InviteeAvatarCell.self, forCellWithReuseIdentifier: "InviteeAvatarCell")
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
remoteUserChanged()
}
func remoteUserChanged() {
TUICallState.instance.remoteUserList.addObserver(remoteUserListObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
var dataList = newValue
dataList.append(TUICallState.instance.selfUser.value)
self.dataSource.value = self.removeCallUser(remoteUserList: dataList)
self.updateDescribeLabel()
self.calleeCollectionView.reloadData()
self.calleeCollectionView.layoutIfNeeded()
})
}
func updateDescribeLabel() {
let count = dataSource.value.count
if count >= 1 {
describeLabel.isHidden = false
} else {
describeLabel.isHidden = true
}
}
}
// MARK: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout
extension InviteeAvatarListView {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSource.value.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "InviteeAvatarCell", for: indexPath) as! InviteeAvatarCell
cell.initCell(user: dataSource.value[indexPath.row])
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: kItemWidth, height: kItemWidth)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return kSpacing
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return kSpacing
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let cellCount = collectionView.numberOfItems(inSection: section)
var inset = (collectionView.bounds.size.width - ((CGFloat(cellCount)) * kItemWidth) - ((CGFloat(cellCount) - 1) * kSpacing)) * 0.5
inset = max(inset, 0.0)
return UIEdgeInsets(top: 0.0, left: inset, bottom: 0.0, right: 0.0)
}
}

View File

@@ -0,0 +1,85 @@
//
// AvatarBackgroundView.swift
// TUICallKit
//
// Created by noah on 2023/11/2.
//
import Foundation
class BackgroundView: UIView {
let remoteUserListObserver = Observer()
let userHeadImageView: UIImageView = {
let userHeadImageView = UIImageView(frame: CGRect.zero)
userHeadImageView.contentMode = .scaleAspectFill
if let image = TUICallKitCommon.getBundleImage(name: "default_user_icon") {
userHeadImageView.image = image
}
return userHeadImageView
}()
let blurEffectView: UIVisualEffectView = {
let blurEffect = UIBlurEffect(style: .light)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.alpha = 0.65
return blurEffectView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setUserImage()
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.remoteUserList.removeObserver(remoteUserListObserver)
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(userHeadImageView)
addSubview(blurEffectView)
}
func activateConstraints() {
self.userHeadImageView.snp.makeConstraints { make in
make.edges.equalTo(self)
}
self.blurEffectView.snp.makeConstraints { make in
make.edges.equalTo(self)
}
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
remoteUserListChanged()
}
func remoteUserListChanged() {
TUICallState.instance.remoteUserList.addObserver(remoteUserListObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.setUserImage()
})
}
// MARK: Update UI
func setUserImage() {
let remoteUser = TUICallState.instance.remoteUserList.value.first ?? User()
userHeadImageView.sd_setImage(with: URL(string: remoteUser.avatar.value))
}
}

View File

@@ -0,0 +1,139 @@
//
// CallStatusView.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/15.
//
import Foundation
class CallStatusTipView: UIView {
let selfCallStatusObserver = Observer()
let networkQualityObserver = Observer()
private var isFirstShowAccept: Bool = true
let callStatusLabel: UILabel = {
let callStatusLabel = UILabel(frame: CGRect.zero)
callStatusLabel.textColor = UIColor.t_colorWithHexString(color: "#FFFFFF")
callStatusLabel.font = UIFont.systemFont(ofSize: 15.0)
callStatusLabel.backgroundColor = UIColor.clear
callStatusLabel.textAlignment = .center
return callStatusLabel
}()
override init(frame: CGRect) {
super.init(frame: frame)
isFirstShowAccept = (TUICallState.instance.selfUser.value.callStatus.value == .accept) ? false : true
updateStatusText()
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.selfUser.value.callStatus.removeObserver(selfCallStatusObserver)
TUICallState.instance.networkQualityReminder.removeObserver(networkQualityObserver)
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(callStatusLabel)
}
func activateConstraints() {
self.callStatusLabel.snp.makeConstraints { make in
make.edges.equalTo(self)
}
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
callStatusChange()
networkQualityChange()
}
func callStatusChange() {
TUICallState.instance.selfUser.value.callStatus.addObserver(selfCallStatusObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateStatusText()
})
}
func networkQualityChange() {
TUICallState.instance.networkQualityReminder.addObserver(networkQualityObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.updateNetworkQualityText()
})
}
func updateNetworkQualityText() {
switch TUICallState.instance.networkQualityReminder.value {
case .Local:
self.callStatusLabel.text = TUICallKitLocalize(key: "TUICallKit.Self.NetworkLowQuality") ?? ""
break
case .Remote:
self.callStatusLabel.text = TUICallKitLocalize(key: "TUICallKit.OtherParty.NetworkLowQuality") ?? ""
break
case .None:
updateStatusText()
break
}
}
func updateStatusText() {
switch TUICallState.instance.selfUser.value.callStatus.value {
case .waiting:
self.callStatusLabel.text = self.getCurrentWaitingText()
break
case .accept:
if isFirstShowAccept {
self.callStatusLabel.text = TUICallKitLocalize(key: "TUICallKit.accept") ?? ""
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isFirstShowAccept = false
}
} else {
self.callStatusLabel.text = ""
}
break
case .none:
break
default:
break
}
}
func getCurrentWaitingText() -> String {
var waitingText = String()
switch TUICallState.instance.mediaType.value {
case .audio:
if TUICallState.instance.selfUser.value.callRole.value == .call {
waitingText = TUICallKitLocalize(key: "TUICallKit.waitAccept") ?? ""
} else {
waitingText = TUICallKitLocalize(key: "TUICallKit.inviteToAudioCall") ?? ""
}
case .video:
if TUICallState.instance.selfUser.value.callRole.value == .call {
waitingText = TUICallKitLocalize(key: "TUICallKit.waitAccept") ?? ""
} else {
waitingText = TUICallKitLocalize(key: "TUICallKit.inviteToVideoCall") ?? ""
}
case .unknown:
break
default:
break
}
return waitingText
}
}

View File

@@ -0,0 +1,96 @@
//
// CallUserInfoView.swift
// TUICallKit
//
// Created by vincepzhang on 2023/2/15.
//
import Foundation
class CallUserInfoView: UIView {
let remoteUserListObserver = Observer()
let userHeadImageView: UIImageView = {
let userHeadImageView = UIImageView(frame: CGRect.zero)
userHeadImageView.layer.masksToBounds = true
userHeadImageView.layer.cornerRadius = 6.0
if let image = TUICallKitCommon.getBundleImage(name: "default_user_icon") {
userHeadImageView.image = image
}
return userHeadImageView
}()
let userNameLabel: UILabel = {
let userNameLabel = UILabel(frame: CGRect.zero)
userNameLabel.textColor = UIColor.t_colorWithHexString(color: "#D5E0F2")
userNameLabel.font = UIFont.boldSystemFont(ofSize: 18.0)
userNameLabel.backgroundColor = UIColor.clear
userNameLabel.textAlignment = .center
return userNameLabel
}()
override init(frame: CGRect) {
super.init(frame: frame)
setUserImageAndName()
registerObserveState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
TUICallState.instance.remoteUserList.removeObserver(remoteUserListObserver)
}
// MARK: UI Specification Processing
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
if isViewReady { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(userHeadImageView)
addSubview(userNameLabel)
}
func activateConstraints() {
self.userHeadImageView.snp.makeConstraints { make in
make.top.centerX.equalTo(self)
make.size.equalTo(CGSize(width: 100.scaleWidth(), height: 100.scaleWidth()))
}
self.userNameLabel.snp.makeConstraints { make in
make.top.equalTo(userHeadImageView.snp.bottom).offset(10.scaleHeight())
make.centerX.equalTo(self)
make.width.equalTo(self)
make.height.equalTo(30)
}
}
// MARK: Register TUICallState Observer && Update UI
func registerObserveState() {
remoteUserListChanged()
}
func remoteUserListChanged() {
TUICallState.instance.remoteUserList.addObserver(remoteUserListObserver, closure: { [weak self] newValue, _ in
guard let self = self else { return }
self.setUserImageAndName()
})
}
// MARK: Update UI
func setUserImageAndName() {
let remoteUser = TUICallState.instance.remoteUserList.value.first ?? User()
userNameLabel.text = User.getUserDisplayName(user: remoteUser)
if let url = URL(string: remoteUser.avatar.value) {
userHeadImageView.sd_setImage(with: url)
}
}
}

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)
}
}
}