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