Files
letro-ios/ElementX/Sources/Application/Windowing/WindowManager.swift
2024-01-18 13:24:15 +00:00

110 lines
4.0 KiB
Swift

//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
class WindowManager: WindowManagerProtocol {
private let appDelegate: AppDelegate
weak var windowScene: UIWindowScene?
weak var delegate: WindowManagerDelegate?
private(set) var mainWindow: UIWindow!
private(set) var overlayWindow: UIWindow!
private(set) var alternateWindow: UIWindow!
var windows: [UIWindow] {
[mainWindow, overlayWindow, alternateWindow]
}
// periphery:ignore - auto cancels when reassigned
/// The task used to switch windows, so that we don't get stuck in the wrong state with a quick switch.
@CancellableTask private var switchTask: Task<Void, Error>?
/// A duration that allows window switching to wait a couple of frames to avoid a transition through black.
private let windowHideDelay = Duration.milliseconds(33)
init(appDelegate: AppDelegate) {
self.appDelegate = appDelegate
}
func configure(with windowScene: UIWindowScene) {
self.windowScene = windowScene
mainWindow = windowScene.keyWindow
mainWindow.tintColor = .compound.textActionPrimary
overlayWindow = PassthroughWindow(windowScene: windowScene)
overlayWindow.tintColor = .compound.textActionPrimary
overlayWindow.backgroundColor = .clear
overlayWindow.isHidden = false
alternateWindow = UIWindow(windowScene: windowScene)
alternateWindow.tintColor = .compound.textActionPrimary
delegate?.windowManagerDidConfigureWindows(self)
}
func switchToMain() {
mainWindow.isHidden = false
overlayWindow.isHidden = false
switchTask = Task {
// Delay hiding to make sure the main windows are visible.
try await Task.sleep(for: windowHideDelay)
alternateWindow.isHidden = true
}
}
func switchToAlternate() {
alternateWindow.isHidden = false
// We don't know what route the app will use when returning back
// to the main window, so end any editing operation now to avoid
// e.g. the keyboard being displayed on top of a call sheet.
mainWindow.endEditing(true)
// alternateWindow.isHidden = false cannot got inside the Task otherwise the timing
// is poor when you lock the phone - you briefly see the main window for a few
// frames after you've unlocked the phone and then the placeholder animates in.
switchTask = Task {
// Delay hiding to make sure the alternate window is visible.
try await Task.sleep(for: windowHideDelay)
overlayWindow.isHidden = true
mainWindow.isHidden = true
}
}
func setOrientation(_ orientation: UIInterfaceOrientationMask) {
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientation))
}
func lockOrientation(_ orientation: UIInterfaceOrientationMask) {
appDelegate.orientationLock = orientation
}
}
private class PassthroughWindow: UIWindow {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let hitView = super.hitTest(point, with: event) else {
return nil
}
// If the returned view is the `UIHostingController`'s view, ignore.
return rootViewController?.view == hitView ? nil : hitView
}
}