diff --git a/application/RTCube/AppDelegate.swift b/application/RTCube/AppDelegate.swift index 30c200c2..c0db1e40 100644 --- a/application/RTCube/AppDelegate.swift +++ b/application/RTCube/AppDelegate.swift @@ -14,6 +14,7 @@ import TCMediaX import TEBeautyKitWrapper #endif import TUICore +import TUIRoomKit import TXLiteAVSDK_Professional import UIKit import UserNotifications @@ -87,6 +88,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { + if let topVC = window?.rootViewController?.topMostViewController(), + let roomVC = topVC as? RoomMainViewController { + return roomVC.isLandscapeMode ? .landscape : .portrait + } return AppDelegate.allowedOrientations } diff --git a/room/Resources/TUIRoomKit.xcassets/room_switch_landscape.imageset/Contents.json b/room/Resources/TUIRoomKit.xcassets/room_switch_landscape.imageset/Contents.json new file mode 100644 index 00000000..990bf4ef --- /dev/null +++ b/room/Resources/TUIRoomKit.xcassets/room_switch_landscape.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "room_switch_landscape@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/room/Resources/TUIRoomKit.xcassets/room_switch_landscape.imageset/room_switch_landscape@3x.png b/room/Resources/TUIRoomKit.xcassets/room_switch_landscape.imageset/room_switch_landscape@3x.png new file mode 100644 index 00000000..ab3cc42c Binary files /dev/null and b/room/Resources/TUIRoomKit.xcassets/room_switch_landscape.imageset/room_switch_landscape@3x.png differ diff --git a/room/Resources/TUIRoomKit.xcassets/room_switch_portrait.imageset/Contents.json b/room/Resources/TUIRoomKit.xcassets/room_switch_portrait.imageset/Contents.json new file mode 100644 index 00000000..ff7cb255 --- /dev/null +++ b/room/Resources/TUIRoomKit.xcassets/room_switch_portrait.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "room_switch_portrait@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/room/Resources/TUIRoomKit.xcassets/room_switch_portrait.imageset/room_switch_portrait@3x.png b/room/Resources/TUIRoomKit.xcassets/room_switch_portrait.imageset/room_switch_portrait@3x.png new file mode 100644 index 00000000..449308e4 Binary files /dev/null and b/room/Resources/TUIRoomKit.xcassets/room_switch_portrait.imageset/room_switch_portrait@3x.png differ diff --git a/room/Source/RoomMainViewController.swift b/room/Source/RoomMainViewController.swift index 0db11cf2..d6a11df4 100644 --- a/room/Source/RoomMainViewController.swift +++ b/room/Source/RoomMainViewController.swift @@ -12,6 +12,18 @@ import SnapKit public class RoomMainViewController: UIViewController, RouterContext { // MARK: - Properties private let rootView: RoomMainView + + /// When `true`, the controller only allows landscape orientations; + /// when `false` (default) only portrait is allowed. + public var isLandscapeMode: Bool = false { + didSet { + if #available(iOS 16.0, *) { + setNeedsUpdateOfSupportedInterfaceOrientations() + } else { + UIViewController.attemptRotationToDeviceOrientation() + } + } + } // MARK: - Lifecycle @@ -41,18 +53,30 @@ public class RoomMainViewController: UIViewController, RouterContext { public override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) UIApplication.shared.isIdleTimerDisabled = false + + // Reset to portrait when leaving the room + isLandscapeMode = false + if #available(iOS 16.0, *) { + setNeedsUpdateOfSupportedInterfaceOrientations() + if let windowScene = view.window?.windowScene { + let geometry = UIWindowScene.GeometryPreferences.iOS(interfaceOrientations: .portrait) + windowScene.requestGeometryUpdate(geometry) + } + } else { + UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") + UIViewController.attemptRotationToDeviceOrientation() + } } public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return .portrait + return isLandscapeMode ? .landscape : .portrait } - - + public override var shouldAutorotate: Bool { return false } - + public override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { - return .portrait + return isLandscapeMode ? .landscapeRight : .portrait } } diff --git a/room/Source/View/Main/RoomView.swift b/room/Source/View/Main/RoomView.swift index b745699e..9eb36127 100644 --- a/room/Source/View/Main/RoomView.swift +++ b/room/Source/View/Main/RoomView.swift @@ -76,6 +76,11 @@ public class RoomView: UIView, BaseView { webinarRoomView.delegate = self } } + + public func setScrollEnabled(_ enabled: Bool) { + guard roomType == .standard else { return } + standardRoomView.setScrollEnabled(enabled) + } } extension RoomView: WebinarRoomViewDelegate { diff --git a/room/Source/View/Main/RoomView/StandardRoomView.swift b/room/Source/View/Main/RoomView/StandardRoomView.swift index 70796056..f477c2ac 100644 --- a/room/Source/View/Main/RoomView/StandardRoomView.swift +++ b/room/Source/View/Main/RoomView/StandardRoomView.swift @@ -121,6 +121,10 @@ class StandardRoomView: UIView, BaseView { backgroundColor = .clear } + public func setScrollEnabled(_ enabled: Bool) { + collectionView.isScrollEnabled = enabled + } + public func setupBindings() { // MARK: - Real Data Binding roomParticipantStore.state diff --git a/room/Source/View/RoomMainView.swift b/room/Source/View/RoomMainView.swift index 64031fe6..7d330218 100644 --- a/room/Source/View/RoomMainView.swift +++ b/room/Source/View/RoomMainView.swift @@ -52,6 +52,8 @@ public class RoomMainView: UIView, BaseView { private var roomType: RoomType = .standard private var localParticipant: RoomParticipant? private let deviceOperator = DeviceOperator() + private var currentScreenSharerID: String? + private var isCurrentlyLandscape: Bool = false // MARK: - UI Components private lazy var topBarView: RoomTopBarView = { @@ -93,6 +95,17 @@ public class RoomMainView: UIView, BaseView { return view }() + private lazy var orientationSwitchButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(ResourceLoader.loadImage("room_switch_landscape"), for: .normal) + button.imageView?.contentMode = .scaleAspectFit + button.contentHorizontalAlignment = .fill + button.contentVerticalAlignment = .fill + button.addTarget(self, action: #selector(onOrientationSwitchButtonTapped), for: .touchUpInside) + button.isHidden = true + return button + }() + private lazy var listView: ParticipantListView = { let listView = ParticipantListView(roomID: roomID, roomType: roomType) return listView @@ -151,6 +164,7 @@ public class RoomMainView: UIView, BaseView { addSubview(topBarView) addSubview(roomView) roomView.addSubview(screenShareOverlay) + roomView.addSubview(orientationSwitchButton) addSubview(barrageStreamView) addSubview(barrageInputView) @@ -201,6 +215,12 @@ public class RoomMainView: UIView, BaseView { make.top.equalTo(topBarView.snp.bottom).offset(12) make.leading.equalToSuperview().offset(16) } + + orientationSwitchButton.snp.makeConstraints { make in + make.width.height.equalTo(32) + make.trailing.equalToSuperview().offset(-12) + make.bottom.equalToSuperview().offset(-12) + } } public func setupStyles() { @@ -240,6 +260,16 @@ public class RoomMainView: UIView, BaseView { } .store(in: &cancellableSet) + participantStore.state.subscribe(StatePublisherSelector(keyPath: \.participantWithScreen)) + .map { $0?.userID } + .removeDuplicates() + .receive(on: RunLoop.main) + .sink { [weak self] userID in + guard let self = self else { return } + onScreenSharerChanged(newSharerID: userID) + } + .store(in: &cancellableSet) + participantStore.participantEventPublisher .receive(on: RunLoop.main) .sink { [weak self] event in @@ -302,6 +332,104 @@ public class RoomMainView: UIView, BaseView { .store(in: &cancellableSet) } + + public override func layoutSubviews() { + super.layoutSubviews() + let landscape = resolveIsLandscape() + if landscape != isCurrentlyLandscape { + isCurrentlyLandscape = landscape + applyOrientationLayout(isLandscape: landscape) + updateOrientationSwitchButtonImage(isLandscape: landscape) + } + } +} + +// MARK: - Orientation +extension RoomMainView { + @objc private func onOrientationSwitchButtonTapped() { + switchOrientation() + } + + private func switchOrientation() { + guard let viewController = routerContext as? UIViewController else { return } + + let isCurrentlyLandscape = bounds.width > bounds.height + let targetLandscape = !isCurrentlyLandscape + + if let roomVC = viewController as? RoomMainViewController { + roomVC.isLandscapeMode = targetLandscape + } + + if #available(iOS 16.0, *) { + viewController.setNeedsUpdateOfSupportedInterfaceOrientations() + if let windowScene = viewController.view.window?.windowScene { + let mask: UIInterfaceOrientationMask = targetLandscape ? .landscape : .portrait + let geometry = UIWindowScene.GeometryPreferences.iOS(interfaceOrientations: mask) + windowScene.requestGeometryUpdate(geometry) + } + } else { + let targetOrientation: UIInterfaceOrientation = targetLandscape ? .landscapeRight : .portrait + UIDevice.current.setValue(targetOrientation.rawValue, forKey: "orientation") + UIViewController.attemptRotationToDeviceOrientation() + } + } + + private func forcePortraitIfLandscape() { + guard let viewController = routerContext as? RoomMainViewController else { return } + if viewController.isLandscapeMode { + viewController.isLandscapeMode = false + } + } + + private func resolveIsLandscape() -> Bool { + guard let viewController = routerContext as? RoomMainViewController else { return false } + return viewController.isLandscapeMode + } + + private func onScreenSharerChanged(newSharerID: String?) { + if currentScreenSharerID == newSharerID { return } + currentScreenSharerID = newSharerID + updateOrientationSwitchButtonVisibility() + if newSharerID == nil || newSharerID?.isEmpty == true { + forcePortraitIfLandscape() + } + } + + private func updateOrientationSwitchButtonVisibility() { + let localUserID = LoginStore.shared.state.value.loginUserInfo?.userID + let hasRemoteSharer = currentScreenSharerID != nil + && !(currentScreenSharerID?.isEmpty ?? true) + && currentScreenSharerID != localUserID + let shouldShow = roomType != .webinar && hasRemoteSharer + orientationSwitchButton.isHidden = !shouldShow + } + + private func updateOrientationSwitchButtonImage(isLandscape: Bool) { + let name = isLandscape ? "room_switch_portrait" : "room_switch_landscape" + orientationSwitchButton.setImage(ResourceLoader.loadImage(name), for: .normal) + } + + private func applyOrientationLayout(isLandscape: Bool) { + topBarView.isHidden = isLandscape + bottomBarView.isHidden = isLandscape + subtitleView?.isHidden = isLandscape + if roomType == .webinar { + barrageInputView.isHidden = isLandscape + barrageStreamView.isHidden = isLandscape + } + recordingFloatingView.alpha = isLandscape ? 0 : 1 + roomView.setScrollEnabled(!isLandscape) + + roomView.snp.remakeConstraints { make in + if isLandscape { + make.edges.equalTo(safeAreaLayoutGuide) + } else { + make.left.right.equalToSuperview() + make.top.equalTo(topBarView.snp.bottom) + make.bottom.equalTo(bottomBarView.snp.top).offset(-10) + } + } + } } extension RoomMainView {