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       "appInfo": manager.session?.appInfo ?? [:],
82       "uuid": UUID.init().uuidString,
83       "openScreen": manager.session?.openScreen ?? NSNull()
84     ]
85   }
86 
87   // RCTRootView assumes it is created on a loading bridge.
88   // in our case, the bridge has usually already loaded. so we need to prod the view.
89   private func forceRootViewToRenderHack() {
90     if !hasCalledJSLoadedNotification, let bridge = manager.appInstance.bridge {
91       let notification = Notification(name: DevMenuViewController.JavaScriptDidLoadNotification, object: nil, userInfo: ["bridge": bridge])
92 
93       reactRootView?.javaScriptDidLoad(notification)
94       hasCalledJSLoadedNotification = true
95     }
96   }
97 
98   private func maybeRebuildRootView() {
99     guard let bridge = manager.appInstance.bridge else {
100       return
101     }
102     if reactRootView?.bridge != bridge {
103       if reactRootView != nil {
104         reactRootView?.removeFromSuperview()
105         reactRootView = nil
106       }
107       hasCalledJSLoadedNotification = false
108       reactRootView = DevMenuRootView(bridge: bridge, moduleName: "main", initialProperties: initialProps())
109       reactRootView?.frame = view.bounds
110       reactRootView?.backgroundColor = UIColor.clear
111 
112       if isViewLoaded, let reactRootView = reactRootView {
113         view.addSubview(reactRootView)
114         view.setNeedsLayout()
115       }
116     } else {
117       updateProps()
118     }
119   }
120 }
121