1 // Copyright 2015-present 650 Industries. All rights reserved.
2
3 import UIKit
4
firstSubview<T: UIView>null5 private func firstSubview<T: UIView>(_ rootView: UIView, ofType type: T.Type) -> T? {
6 var resultView: T?
7 for view in rootView.subviews {
8 if let view = view as? T {
9 resultView = view
10 break
11 }
12
13 if let foundView = firstSubview(view, ofType: T.self) {
14 resultView = foundView
15 break
16 }
17 }
18 return resultView
19 }
20
21 class DevMenuWindow: UIWindow, OverlayContainerViewControllerDelegate {
22 private let manager: DevMenuManager
23
24 private let bottomSheetController: OverlayContainerViewController
25 private let devMenuViewController: DevMenuViewController
26
27 init(manager: DevMenuManager) {
28 self.manager = manager
29 bottomSheetController = OverlayContainerViewController(style: .flexibleHeight)
30 devMenuViewController = DevMenuViewController(manager: manager)
31
32 super.init(frame: UIScreen.main.bounds)
33
34 bottomSheetController.delegate = self
35 bottomSheetController.viewControllers = [devMenuViewController]
36
37 self.rootViewController = bottomSheetController
38 self.backgroundColor = UIColor(white: 0, alpha: 0.4)
39 self.bounds = UIScreen.main.bounds
40 self.windowLevel = .statusBar
41 self.isHidden = true
42 }
43
44 @available(*, unavailable)
45 required init?(coder: NSCoder) {
46 fatalError("init(coder:) has not been implemented")
47 }
48
becomeKeynull49 override func becomeKey() {
50 // We set up the background of the RN root view to mask all artifacts caused by Yoga when the bottom sheet is dragged.
51 devMenuViewController.view.backgroundColor = UIColor(red: 0.97, green: 0.97, blue: 0.98, alpha: 1)
52
53 devMenuViewController.updateProps()
54 bottomSheetController.moveOverlay(toNotchAt: OverlayNotch.open.rawValue, animated: true)
55
56 setDrivingScrollView()
57 }
58
59 // In order to create a smooth interplay between a mobile bottom sheet and scrolling through its contents,
60 // the 'drivingScrollView' property must be established. However, it may not be immediately accessible
61 // when the menu is first opened. As a result, we schedule a task that periodically verifies the availability of the scroll view.
62 // TODO(@lukmccall): find a better way how to detect if the scroll view is available.
setDrivingScrollViewnull63 private func setDrivingScrollView() {
64 let scrollView = firstSubview(devMenuViewController.view, ofType: UIScrollView.self)
65 if scrollView == nil {
66 DispatchQueue.main.async {
67 self.setDrivingScrollView()
68 }
69 } else {
70 bottomSheetController.drivingScrollView = scrollView
71 }
72 }
73
hitTestnull74 override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
75 let view = super.hitTest(point, with: event)
76 if view == self {
77 bottomSheetController.moveOverlay(toNotchAt: OverlayNotch.hidden.rawValue, animated: true)
78 }
79
80 return view == self ? nil : view
81 }
82
83 enum OverlayNotch: Int, CaseIterable {
84 case hidden, open, fullscreen
85 }
86
numberOfNotchesnull87 func numberOfNotches(in containerViewController: OverlayContainerViewController) -> Int {
88 return OverlayNotch.allCases.count
89 }
90
91 func overlayContainerViewController(
92 _ containerViewController: OverlayContainerViewController,
93 heightForNotchAt index: Int,
94 availableSpace: CGFloat
95 ) -> CGFloat {
96 switch OverlayNotch.allCases[index] {
97 case .fullscreen:
98 // Before the dev menu is opened for the first time the availableSpace equals zero (correct value is loaded while opening the dev menu).
99 // In order to avoid crashing the app because of returning a negative value make sure that the returned value is >= 0.
100 return max(availableSpace - 45, 0)
101 case .open:
102 return availableSpace * 0.6
103 case .hidden:
104 return 0
105 }
106 }
107
108 func overlayContainerViewController(
109 _ containerViewController: OverlayContainerViewController,
110 didMoveOverlay overlayViewController: UIViewController,
111 toNotchAt index: Int
112 ) {
113 if index == OverlayNotch.hidden.rawValue {
114 manager.hideMenu()
115 }
116 }
117
118 func closeBottomSheet(completion: (() -> Void)? = nil) {
119 bottomSheetController.moveOverlay(toNotchAt: OverlayNotch.hidden.rawValue, animated: true, completion: completion)
120 }
121 }
122