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 
updatePropsnull25   func updateProps() {
26     reactRootView?.appProperties = initialProps()
27   }
28 
29   // MARK: UIViewController
30 
viewDidLoadnull31   override func viewDidLoad() {
32     super.viewDidLoad()
33     maybeRebuildRootView()
34   }
35 
viewWillLayoutSubviewsnull36   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 
viewWillAppearnull41   override func viewWillAppear(_ animated: Bool) {
42     super.viewWillAppear(animated)
43     forceRootViewToRenderHack()
44     reactRootView?.becomeFirstResponder()
45   }
46 
47   override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
48     get {
49       return UIInterfaceOrientationMask.all
50     }
51   }
52 
53   @available(iOS 12.0, *)
54   override var overrideUserInterfaceStyle: UIUserInterfaceStyle {
55     get {
56       return manager.userInterfaceStyle
57     }
58     set {}
59   }
60 
61   // MARK: private
62 
initialPropsnull63   private func initialProps() -> [String: Any] {
64     let isSimulator = TARGET_IPHONE_SIMULATOR > 0
65 
66     return [
67       "showOnboardingView": manager.shouldShowOnboarding(),
68       "appInfo": manager.getAppInfo(),
69       "devSettings": manager.getDevSettings(),
70       "menuPreferences": DevMenuPreferences.serialize(),
71       "uuid": UUID.init().uuidString,
72       "isDevice": !isSimulator,
73       "registeredCallbacks": manager.registeredCallbacks.map { $0.name }
74     ]
75   }
76 
77   // RCTRootView assumes it is created on a loading bridge.
78   // in our case, the bridge has usually already loaded. so we need to prod the view.
forceRootViewToRenderHacknull79   private func forceRootViewToRenderHack() {
80     if !hasCalledJSLoadedNotification, let bridge = manager.appInstance.bridge {
81       let notification = Notification(name: DevMenuViewController.JavaScriptDidLoadNotification, object: nil, userInfo: ["bridge": bridge])
82 
83       reactRootView?.javaScriptDidLoad(notification)
84       hasCalledJSLoadedNotification = true
85     }
86   }
87 
maybeRebuildRootViewnull88   private func maybeRebuildRootView() {
89     guard let bridge = manager.appInstance.bridge else {
90       return
91     }
92     if reactRootView?.bridge != bridge {
93       if reactRootView != nil {
94         reactRootView?.removeFromSuperview()
95         reactRootView = nil
96       }
97       hasCalledJSLoadedNotification = false
98       reactRootView = DevMenuRootView(bridge: bridge, moduleName: "main", initialProperties: initialProps())
99       reactRootView?.frame = view.bounds
100       reactRootView?.backgroundColor = UIColor.clear
101 
102       if isViewLoaded, let reactRootView = reactRootView {
103         view.addSubview(reactRootView)
104         view.setNeedsLayout()
105       }
106     } else {
107       updateProps()
108     }
109   }
110 }
111