diff --git a/Mastodon/Extension/UIApplication.swift b/Mastodon/Extension/UIApplication.swift index 38080fdab..74019b0ae 100644 --- a/Mastodon/Extension/UIApplication.swift +++ b/Mastodon/Extension/UIApplication.swift @@ -22,5 +22,13 @@ extension UIApplication { return version == build ? "v\(version)" : "v\(version) (\(build))" } + + func getKeyWindow() -> UIWindow? { + return UIApplication + .shared + .connectedScenes + .flatMap { ($0 as? UIWindowScene)?.windows ?? [] } + .first { $0.isKeyWindow } + } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index ff90775ff..8ad798165 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -122,6 +122,10 @@ extension HomeTimelineViewController { identifier: nil, options: [], children: [ + UIAction(title: "Toggle Visible Touches", image: UIImage(systemName: "hand.tap"), attributes: []) { _ in + guard let window = UIApplication.shared.getKeyWindow() as? TouchesVisibleWindow else { return } + window.touchesVisible = !window.touchesVisible + }, UIAction(title: "Toggle EmptyView", image: UIImage(systemName: "clear"), attributes: []) { [weak self] action in guard let self = self else { return } if self.emptyView.superview != nil { diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 002f494d5..e2f045a7b 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -11,6 +11,7 @@ import Combine import CoreDataStack import MastodonCore import MastodonExtension +import MastodonUI #if PROFILE import FPSIndicator @@ -35,8 +36,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = scene as? UIWindowScene else { return } + #if DEBUG + let window = TouchesVisibleWindow(windowScene: windowScene) + self.window = window + #else let window = UIWindow(windowScene: windowScene) self.window = window + #endif // set tint color window.tintColor = UIColor.label diff --git a/MastodonSDK/Sources/MastodonUI/View/Window/TouchesVisibleWindow.swift b/MastodonSDK/Sources/MastodonUI/View/Window/TouchesVisibleWindow.swift new file mode 100644 index 000000000..8779e1ce7 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Window/TouchesVisibleWindow.swift @@ -0,0 +1,135 @@ +// +// TouchesVisibleWindow.swift +// +// +// Created by Chase Carroll on 12/5/22. +// + +#if DEBUG + +import UIKit + +/// View that represents a single touch from the user. +private final class TouchView: UIView { + + private let blurView = UIVisualEffectView(effect: nil) + + override var frame: CGRect { + didSet { + layer.cornerRadius = frame.height / 2.0 + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + let isLightMode = traitCollection.userInterfaceStyle == .light + + backgroundColor = .clear + layer.masksToBounds = true + layer.cornerCurve = .circular + layer.borderColor = isLightMode ? UIColor.gray.cgColor : UIColor.white.cgColor + layer.borderWidth = 2.0 + + let blurEffect = isLightMode ? + UIBlurEffect(style: .systemUltraThinMaterialDark) : + UIBlurEffect(style: .systemUltraThinMaterialLight) + blurView.effect = blurEffect + addSubview(blurView) + } + + @available(iOS, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + blurView.frame = bounds + } + +} + + +/// `UIWindow` subclass that renders visual representations of the user's touches. +public final class TouchesVisibleWindow: UIWindow { + + public var touchesVisible = false { + didSet { + if !touchesVisible { + cleanUpAllTouches() + } + } + } + + private var touchViews: [UITouch : TouchView] = [:] + + private func newTouchView() -> TouchView { + let touchSize = 44.0 + return TouchView(frame: CGRect( + origin: .zero, + size: CGSize( + width: touchSize, + height: touchSize + ) + )) + } + + private func cleanupTouch(_ touch: UITouch) { + guard let touchView = touchViews[touch] else { + return + } + + touchView.removeFromSuperview() + touchViews.removeValue(forKey: touch) + } + + private func cleanUpAllTouches() { + for (_, touchView) in touchViews { + touchView.removeFromSuperview() + } + + touchViews.removeAll() + } + + public override func sendEvent(_ event: UIEvent) { + if touchesVisible { + let touches = event.allTouches + + guard + let touches = touches, + touches.count > 0 + else { + cleanUpAllTouches() + super.sendEvent(event) + return + } + + for touch in touches { + let touchLocation = touch.location(in: self) + switch touch.phase { + case .began: + let touchView = newTouchView() + touchView.center = touchLocation + addSubview(touchView) + touchViews[touch] = touchView + + case .moved: + if let touchView = touchViews[touch] { + touchView.center = touchLocation + } + + case .ended, .cancelled: + cleanupTouch(touch) + + default: + break + } + } + } + + super.sendEvent(event) + } +} + +#endif