1 // Copyright 2015-present 650 Industries. All rights reserved.
2 
3 import UIKit
4 
5 class DevMenuViewController: UIViewController {
6   static let JavaScriptDidLoadNotification = Notification.Name("RCTJavaScriptDidLoadNotification")
7   static let ContentDidAppearNotification = Notification.Name("RCTContentDidAppearNotification")
8 
9   private let manager: DevMenuManager
10   private var reactRootView: DevMenuRootView?
11   private var hasCalledJSLoadedNotification: Bool = false
12 
13   init(manager: DevMenuManager) {
14     self.manager = manager
15 
16     super.init(nibName: nil, bundle: nil)
17     edgesForExtendedLayout = UIRectEdge.init(rawValue: 0)
18     extendedLayoutIncludesOpaqueBars = true
19   }
20 
21   required init?(coder: NSCoder) {
22     fatalError("init(coder:) has not been implemented")
23   }
24 
25   func updateProps() {
26     reactRootView?.appProperties = initialProps()
27   }
28 
29   // MARK: UIViewController
30 
31   override func viewDidLoad() {
32     super.viewDidLoad()
33     maybeRebuildRootView()
34   }
35 
36   override func viewWillLayoutSubviews() {
37     super.viewWillLayoutSubviews()
38     reactRootView?.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height)
39   }
40 
41   override func viewWillAppear(_ animated: Bool) {
42     super.viewWillAppear(animated)
43     forceRootViewToRenderHack()
44     reactRootView?.becomeFirstResponder()
45   }
46 
47   override var shouldAutorotate: Bool {
48     get {
49       return true
50     }
51   }
52 
53   override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
54     get {
55       return UIInterfaceOrientationMask.portrait
56     }
57   }
58 
59   override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
60     get {
61       return UIInterfaceOrientation.portrait
62     }
63   }
64 
65   @available(iOS 12.0, *)
66   override var overrideUserInterfaceStyle: UIUserInterfaceStyle {
67     get {
68       return manager.userInterfaceStyle
69     }
70     set {}
71   }
72 
73   // MARK: private
74 
75   private func initialProps() -> [String: Any] {
76     return [
77       "enableDevelopmentTools": true,
78       "showOnboardingView": manager.shouldShowOnboarding(),
79       "devMenuItems": manager.serializedDevMenuItems(),
80       "devMenuScreens": manager.serializedDevMenuScreens(),
81       "uuid": UUID.init().uuidString,
82     ]
83   }
84 
85   // RCTRootView assumes it is created on a loading bridge.
86   // in our case, the bridge has usually already loaded. so we need to prod the view.
87   private func forceRootViewToRenderHack() {
88     if !hasCalledJSLoadedNotification, let bridge = manager.appInstance.bridge {
89       let notification = Notification(name: DevMenuViewController.JavaScriptDidLoadNotification, object: nil, userInfo: ["bridge": bridge])
90 
91       reactRootView?.javaScriptDidLoad(notification)
92       hasCalledJSLoadedNotification = true
93     }
94   }
95 
96   private func maybeRebuildRootView() {
97     guard let bridge = manager.appInstance.bridge else {
98       return
99     }
100     if reactRootView?.bridge != bridge {
101       if reactRootView != nil {
102         reactRootView?.removeFromSuperview()
103         reactRootView = nil
104       }
105       hasCalledJSLoadedNotification = false
106       reactRootView = DevMenuRootView(bridge: bridge, moduleName: "main", initialProperties: initialProps())
107       reactRootView?.frame = view.bounds
108       reactRootView?.backgroundColor = UIColor.clear
109 
110       if isViewLoaded, let reactRootView = reactRootView {
111         view.addSubview(reactRootView)
112         view.setNeedsLayout()
113       }
114     } else {
115       updateProps()
116     }
117   }
118 }
119