// // ConferenceMainView.swift // TUIRoomKit // // Created by aby on 2022/12/27. // Copyright © 2022 Tencent. All rights reserved. // The main conference interface is responsible for arranging and managing the top bar, bottom bar, video interface, etc. // import Foundation import RTCRoomEngine import Factory protocol ConferenceMainViewFactory { func makeBottomView() -> BottomView func makeTopView() -> TopView func makeVideoSeatView() -> UIView func makeRaiseHandNoticeView() -> UIView func makeLocalAudioView() -> UIView func makeWaterMarkLayer() -> WaterMarkLayer func makeFloatChatButton() -> FloatChatButton func makeFloatChatDisplayView() -> FloatChatDisplayView func makeRaiseHandApplicationNotificationView() -> RaiseHandApplicationNotificationView func makeConferencePasswordView() -> ConferencePasswordView } struct ConferenceMainViewLayout { //Layout changes when switching between horizontal and vertical screens let bottomViewLandscapeSpace: Float = 0 let bottomViewPortraitSpace: Float = 34.0 let topViewLandscapeHight: Float = 75.0 let topViewPortraitHight: Float = 105.0 let videoSeatViewPortraitSpace: Float = 73.0 let videoSeatViewLandscapeSpace: Float = 82.0 } class ConferenceMainView: UIView { let viewModel: ConferenceMainViewModel let viewFactory: ConferenceMainViewFactory let layout: ConferenceMainViewLayout = ConferenceMainViewLayout() @Injected(\.navigation) private var route init(viewModel: ConferenceMainViewModel, viewFactory: ConferenceMainViewFactory) { self.viewModel = viewModel self.viewFactory = viewFactory super.init(frame: .zero) viewModel.viewResponder = self subscribeUIEvent() } private var currentLandscape: Bool = isLandscape private let firstDelayDisappearanceTime = 6.0 private let delayDisappearanceTime = 3.0 required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } lazy var topView: TopView = { return viewFactory.makeTopView() }() lazy var videoSeatView: UIView = { return viewFactory.makeVideoSeatView() }() lazy var bottomView: BottomView = { return viewFactory.makeBottomView() }() lazy var raiseHandNoticeView: UIView = { return viewFactory.makeRaiseHandNoticeView() }() lazy var localAudioView: UIView = { return viewFactory.makeLocalAudioView() }() lazy var waterMarkLayer: CALayer = { return viewFactory.makeWaterMarkLayer() }() lazy var floatChatDisplayView: FloatChatDisplayView = { return viewFactory.makeFloatChatDisplayView() }() lazy var floatChatButton: FloatChatButton = { return viewFactory.makeFloatChatButton() }() lazy var raiseHandApplicationNotificationView: RaiseHandApplicationNotificationView = { let applicationNotificationView = viewFactory.makeRaiseHandApplicationNotificationView() return applicationNotificationView }() lazy var conferencePasswordView: ConferencePasswordView = { return viewFactory.makeConferencePasswordView() }() // MARK: - view layout private var isViewReady: Bool = false override func didMoveToWindow() { super.didMoveToWindow() guard !isViewReady else { return } backgroundColor = UIColor(0x0F1014) constructViewHierarchy() activateConstraints() bindInteraction() isViewReady = true } override func layoutSubviews() { super.layoutSubviews() guard currentLandscape != isLandscape else { return } setupRootViewOrientation(isLandscape: isLandscape) currentLandscape = isLandscape } func constructViewHierarchy() { addSubview(videoSeatView) if viewModel.isShownWaterMark { layer.addSublayer(waterMarkLayer) } addSubview(topView) addSubview(floatChatDisplayView) addSubview(floatChatButton) addSubview(bottomView) addSubview(localAudioView) addSubview(raiseHandNoticeView) addSubview(raiseHandApplicationNotificationView) addSubview(conferencePasswordView) } func activateConstraints() { setupRootViewOrientation(isLandscape: isLandscape) raiseHandNoticeView.snp.makeConstraints { make in make.bottom.equalTo(bottomView.snp.top).offset(-15) make.centerX.equalToSuperview() make.height.equalTo(40) make.width.equalTo(300) } localAudioView.snp.makeConstraints { make in make.centerX.equalToSuperview() make.width.height.equalTo(40.scale375()) make.bottom.equalToSuperview().offset(-40.scale375Height()) } floatChatButton.snp.makeConstraints { make in make.bottom.equalTo(localAudioView.snp.top).offset(-18) make.height.equalTo(30) make.leading.equalTo(videoSeatView.snp.leading) } floatChatDisplayView.snp.makeConstraints { make in make.bottom.equalTo(floatChatButton.snp.top).offset(-8) make.height.equalTo(128) make.leading.equalToSuperview().offset(5) make.width.equalTo(313) } raiseHandApplicationNotificationView.snp.makeConstraints { make in make.top.equalTo(topView.snp.bottom) make.width.equalTo(359.scale375()) make.centerX.equalToSuperview() make.height.equalTo(40.scale375Height()) } conferencePasswordView.snp.makeConstraints { make in make.edges.equalToSuperview() } } private func bindInteraction() { perform(#selector(hideToolBar),with: nil,afterDelay: firstDelayDisappearanceTime) } func setupRootViewOrientation(isLandscape: Bool) { videoSeatView.snp.remakeConstraints { make in if isLandscape { make.leading.equalTo(layout.videoSeatViewLandscapeSpace) make.trailing.equalTo(-layout.videoSeatViewLandscapeSpace) make.top.bottom.equalToSuperview() } else { make.leading.trailing.equalToSuperview() make.top.equalTo(layout.videoSeatViewPortraitSpace) make.bottom.equalTo(-layout.videoSeatViewPortraitSpace) } } topView.snp.remakeConstraints() { make in make.top.equalToSuperview() make.leading.equalTo(safeAreaLayoutGuide.snp.leading) make.trailing.equalTo(safeAreaLayoutGuide.snp.trailing) if isLandscape { make.height.equalTo(layout.topViewLandscapeHight) } else { make.height.equalTo(layout.topViewPortraitHight) } } bottomView.snp.remakeConstraints { make in make.leading.equalTo(safeAreaLayoutGuide.snp.leading) make.trailing.equalTo(safeAreaLayoutGuide.snp.trailing) make.height.equalTo(bottomView.isUnfold ? bottomView.unfoldHeight : bottomView.packUpHeight) if isLandscape { make.bottom.equalToSuperview().offset(-layout.bottomViewLandscapeSpace) } else { make.bottom.equalToSuperview().offset(-layout.bottomViewPortraitSpace) } } topView.updateRootViewOrientation(isLandscape: isLandscape) setupWaterMarkLayerOrientation(isLandscape: isLandscape) } private func setupWaterMarkLayerOrientation(isLandscape: Bool) { guard viewModel.isShownWaterMark else { return } let widthSpace = isLandscape ? CGFloat(layout.videoSeatViewLandscapeSpace) : 0 let heightSpace = isLandscape ? 0 : CGFloat(layout.videoSeatViewPortraitSpace) waterMarkLayer.frame = CGRect(x: widthSpace, y: heightSpace, width: kScreenWidth - widthSpace * 2, height: kScreenHeight - heightSpace * 2) waterMarkLayer.setNeedsDisplay() } private func subscribeUIEvent() { EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_ShowFloatChatView, responder: self) } private func unsubscribeEvent() { EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_ShowFloatChatView, responder: self) } deinit { NSObject.cancelPreviousPerformRequests(withTarget: self) unsubscribeEvent() debugPrint("deinit \(self)") } } extension ConferenceMainView: ConferenceMainViewResponder { func hidePasswordView() { conferencePasswordView.hide() } func showPasswordView(roomId: String) { conferencePasswordView.show(roomId: roomId) } func showExitRoomView() { let view = ExitRoomView(viewModel: ExitRoomViewModel()) view.show(rootView: self) } func showAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?) { RoomRouter.presentAlert(title: title, message: message, sureTitle: sureTitle, declineTitle: declineTitle, sureBlock: sureBlock, declineBlock: declineBlock) } func showAlertWithAutoConfirm(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?, autoConfirmSeconds: Int?) { RoomRouter.presentAlert(title: title, message: message, sureTitle: sureTitle, declineTitle: declineTitle, sureBlock: sureBlock, declineBlock: declineBlock, autoConfirmSeconds: autoConfirmSeconds) } func makeToast(text: String) { RoomRouter.makeToastInCenter(toast: text, duration: 1) } func showRaiseHandNoticeView() { raiseHandNoticeView.isHidden = false } func updateRoomInfo(roomInfo: TUIRoomInfo) { floatChatButton.updateRoomId(roomId: roomInfo.roomId) } private func showToolBar() { topView.alpha = 1 bottomView.alpha = 1 topView.isHidden = false bottomView.isHidden = false viewModel.hideLocalAudioView() } @objc private func hideToolBar() { topView.alpha = 0 bottomView.alpha = 0 topView.isHidden = true bottomView.isHidden = true viewModel.showLocalAudioView() } func changeToolBarHiddenState() { NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideToolBar), object: nil) if topView.isHidden { showToolBar() perform(#selector(hideToolBar),with: nil,afterDelay: delayDisappearanceTime) } else if !bottomView.isUnfold { hideToolBar() } } func setToolBarDelayHidden(isDelay: Bool) { NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideToolBar), object: nil) guard !bottomView.isUnfold, isDelay else { return } perform(#selector(hideToolBar),with: nil,afterDelay: delayDisappearanceTime) } func showRepeatJoinRoomAlert() { let sureAction = UIAlertAction(title: .repeatJoinRoomSureText, style: .default) { _ in } let alertState = AlertState(title: .repeatJoinRoomTitle, message: .repeatJoinRoomMessage, sureAction: sureAction, declineAction: nil) route.present(route: .alert(state: alertState)) } } extension ConferenceMainView: RoomKitUIEventResponder { func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) { switch key { case .TUIRoomKitService_ShowFloatChatView: guard let shouldShow = info?["shouldShow"] as? Bool else { return } floatChatButton.isHidden = !shouldShow floatChatDisplayView.isHidden = !shouldShow default: break } } } private extension String { static let repeatJoinRoomTitle = localized("Currently in the room") static let repeatJoinRoomMessage = localized("Please exit before joining a new room") static let repeatJoinRoomSureText = localized("I see") }