1 // Copyright 2021-present 650 Industries. All rights reserved.
2 
3 import React
4 
5 /**
6  Custom component data extending `RCTComponentData`. Its main purpose is to handle event-based props (callbacks),
7  but it also simplifies capturing the view config so we can omit some reflections that React Native executes.
8  */
9 @objc(EXComponentData)
10 public final class ComponentData: RCTComponentDataSwiftAdapter {
11   /**
12    Weak pointer to the holder of a module that the component data was created for.
13    */
14   weak var moduleHolder: ModuleHolder?
15 
16   /**
17    Initializer that additionally takes the original view module to have access to its definition.
18    */
19   @objc
20   public init(viewModule: ViewModuleWrapper, managerClass: ViewModuleWrapper.Type, bridge: RCTBridge) {
21     self.moduleHolder = viewModule.wrappedModuleHolder
22     super.init(managerClass: managerClass, bridge: bridge, eventDispatcher: bridge.eventDispatcher())
23   }
24 
25   // MARK: RCTComponentData
26 
27   /**
28    Creates a setter for the specific prop. For non-event props we just let React Native do its job.
29    Events are handled differently to conveniently use them in Swift.
30    */
31   public override func createPropBlock(_ propName: String, isShadowView: Bool) -> RCTPropBlockAlias {
32     // Expo Modules Core doesn't support shadow views yet, so fall back to the default implementation.
33     if isShadowView {
34       return super.createPropBlock(propName, isShadowView: isShadowView)
35     }
36 
37     // If the prop is defined as an event, create our own event setter.
38     if moduleHolder?.viewManager?.eventNames.contains(propName) == true {
39       return createEventSetter(eventName: propName, bridge: self.manager?.bridge)
40     }
41 
42     // Otherwise also fall back to the default implementation.
43     return super.createPropBlock(propName, isShadowView: isShadowView)
44   }
45 
46   public override func setProps(_ props: [String: Any], forView view: RCTComponent) {
47     guard let view = view as? UIView else {
48       log.warn("Given view is not an UIView")
49       return
50     }
51     guard let viewManager = moduleHolder?.viewManager else {
52       log.warn("View manager '\(self.name)' not found")
53       return
54     }
55     guard let appContext = moduleHolder?.appContext else {
56       log.warn("App context has been lost")
57       return
58     }
59     let propsDict = viewManager.propsDict()
60     var remainingProps = props
61 
62     for (key, prop) in propsDict {
63       if props.index(forKey: key) == nil {
64         continue
65       }
66 
67       let newValue = props[key] as Any
68 
69       // TODO: @tsapeta: Figure out better way to rethrow errors from here.
70       try? prop.set(value: Conversions.fromNSObject(newValue), onView: view, appContext: appContext)
71 
72       remainingProps.removeValue(forKey: key)
73     }
74 
75     // Let the base class `RCTComponentData` handle all remaining props.
76     super.setProps(remainingProps, forView: view)
77 
78     viewManager.callLifecycleMethods(withType: .didUpdateProps, forView: view)
79   }
80 
81   /**
82    The base `RCTComponentData` class does some Objective-C dynamic calls in this function, but we don't
83    need to do these slow operations since the Sweet API gives us necessary details without reflections.
84    */
85   public override func viewConfig() -> [String: Any] {
86     var propTypes: [String: Any] = [:]
87     var directEvents: [String] = []
88     let superClass: AnyClass? = managerClass.superclass()
89 
90     if let viewManager = moduleHolder?.viewManager {
91       for prop in viewManager.props {
92         // `id` allows every type to be passed in
93         propTypes[prop.name] = "id"
94       }
95       for eventName in viewManager.eventNames {
96         directEvents.append(RCTNormalizeInputEventName(eventName))
97         propTypes[eventName] = "BOOL"
98       }
99     }
100 
101     return [
102       "propTypes": propTypes,
103       "directEvents": directEvents,
104       "bubblingEvents": [String](),
105       "baseModuleName": superClass?.moduleName() as Any
106     ]
107   }
108 }
109 
110 /**
111  Creates a setter for the event prop. Used only by Paper.
112  */
113 private func createEventSetter(eventName: String, bridge: RCTBridge?) -> RCTPropBlockAlias {
114   return { [weak bridge] (target: RCTComponent, value: Any) in
115     installEventDispatcher(forEvent: eventName, onView: target) { [weak target] (body: [String: Any]) in
116       if let target = target {
117         let componentEvent = RCTComponentEvent(name: eventName, viewTag: target.reactTag, body: body)
118         bridge?.eventDispatcher()?.send(componentEvent)
119       }
120     }
121   }
122 }
123