1 // Copyright 2015-present 650 Industries. All rights reserved. 2 3 import UIKit 4 5 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 49 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. 63 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 74 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 87 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