Files
crossdesk/apps/ios/CrossDeskMobile/CrossDeskMobileApp.swift
T

113 lines
4.1 KiB
Swift

import SwiftUI
import UIKit
final class CrossDeskAppDelegate: NSObject, UIApplicationDelegate {
static var supportedOrientations: UIInterfaceOrientationMask =
UIDevice.current.userInterfaceIdiom == .pad ? .all : .portrait
func application(_ application: UIApplication,
supportedInterfaceOrientationsFor window: UIWindow?)
-> UIInterfaceOrientationMask {
Self.supportedOrientations
}
}
@MainActor
enum AppOrientation {
nonisolated static let autoLandscapeKey = "crossdesk.mobile.auto-landscape"
private static var sessionActive = false
private static var waitingForLandscape = false
private static var deviceObserver: NSObjectProtocol?
nonisolated static func beginSession(autoLandscape: Bool) {
DispatchQueue.main.async { beginSessionOnMain(autoLandscape: autoLandscape) }
}
private static func beginSessionOnMain(autoLandscape: Bool) {
// Repeated transport-ready callbacks must not lock an existing session again.
guard !sessionActive else { return }
sessionActive = true
waitingForLandscape = autoLandscape && UIDevice.current.userInterfaceIdiom == .phone
if waitingForLandscape {
CrossDeskAppDelegate.supportedOrientations = .landscape
resumeObserving()
} else {
apply(to: .allButUpsideDown)
}
}
nonisolated static func endSession() {
DispatchQueue.main.async {
sessionActive = false
waitingForLandscape = false
stopObserving()
apply(to: .portrait)
}
}
nonisolated static func resume() {
DispatchQueue.main.async { resumeObserving() }
}
private static func resumeObserving() {
guard sessionActive, waitingForLandscape, deviceObserver == nil,
UIApplication.shared.applicationState == .active else { return }
apply(to: .landscape)
deviceObserver = NotificationCenter.default.addObserver(
forName: UIDevice.orientationDidChangeNotification, object: nil, queue: .main
) { _ in
MainActor.assumeIsolated { deviceDidRotate() }
}
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
deviceDidRotate()
}
nonisolated static func suspend() {
DispatchQueue.main.async { stopObserving() }
}
private static func stopObserving() {
guard let observer = deviceObserver else { return }
NotificationCenter.default.removeObserver(observer)
deviceObserver = nil
UIDevice.current.endGeneratingDeviceOrientationNotifications()
}
private static func deviceDidRotate() {
// UIDevice reports physical posture; window geometry also changes when
// we request landscape and must never release the initial lock itself.
guard sessionActive, waitingForLandscape, deviceObserver != nil,
UIApplication.shared.applicationState == .active,
UIDevice.current.orientation.isLandscape else { return }
waitingForLandscape = false
stopObserving()
apply(to: .allButUpsideDown)
}
private static func apply(to orientations: UIInterfaceOrientationMask) {
// iPad keeps every orientation available, including in resizable windows.
guard UIDevice.current.userInterfaceIdiom != .pad else { return }
CrossDeskAppDelegate.supportedOrientations = orientations
for case let scene as UIWindowScene in UIApplication.shared.connectedScenes {
scene.windows.forEach {
$0.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
}
scene.requestGeometryUpdate(.iOS(interfaceOrientations: orientations)) { error in
NSLog("CrossDesk orientation update failed: %@", error.localizedDescription)
}
}
}
}
@main
struct CrossDeskMobileApp: App {
@UIApplicationDelegateAdaptor(CrossDeskAppDelegate.self) private var appDelegate
@StateObject private var session = RemoteSessionModel()
var body: some Scene {
WindowGroup {
ContentView(session: session)
}
}
}