ComponentKit/Core/Action/CKAction.mm (441 lines of code) (raw):

/* * Copyright (c) 2014-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * */ #import "CKAction.h" #import <unordered_map> #import <vector> #import <array> #import <ComponentKit/CKInternalHelpers.h> #import <RenderCore/RCAssert.h> #import <ComponentKit/RCAssociatedObject.h> #import <ComponentKit/CKCollection.h> #import <ComponentKit/CKGlobalConfig.h> #import <ComponentKit/CKMutex.h> #import "CKComponent+UIView.h" #import "CKComponent.h" #import "CKComponentInternal.h" void CKActionTypeVectorBuild(std::vector<const char *> &typeVector, const CKActionTypelist<> &list) noexcept { } void CKConfigureInvocationWithArguments(NSInvocation *invocation, NSInteger index) noexcept { } #pragma mark - CKActionBase bool CKActionBase::operator==(const CKActionBase& rhs) const { return (_variant == rhs._variant && RCObjectIsEqual(_target, rhs._target) // If we are using a scoped action, we are only concerned that the selector and the // responder unique identifier matches. && _scopedResponderAndKey.responder == rhs._scopedResponderAndKey.responder && _selectorOrIdentifier == rhs._selectorOrIdentifier && ((CKReadGlobalConfig().actionShouldCompareCustomIdentifier && _variant == CKActionVariant::BlockWithIdentifier) || _block == rhs._block)); } CKActionSendBehavior CKActionBase::defaultBehavior() const { return (_variant == CKActionVariant::RawSelector ? CKActionSendBehaviorStartAtSenderNextResponder : CKActionSendBehaviorStartAtSender); } id CKActionBase::initialTarget(CKComponent *sender) const { switch (_variant) { case CKActionVariant::RawSelector: return sender; case CKActionVariant::TargetSelector: return _target; case CKActionVariant::Responder: return [_scopedResponderAndKey.responder responderForKey:_scopedResponderAndKey.key]; case CKActionVariant::Block: RCCFailAssert(@"Should not be asking for target for block action."); return nil; case CKActionVariant::BlockWithIdentifier: RCCFailAssert(@"Should not be asking for target for block action."); return nil; } } CKActionBase::CKActionBase() noexcept : _target(nil), _scopedResponderAndKey({}), _block(NULL), _variant(CKActionVariant::RawSelector), _selectorOrIdentifier(nullptr) {} CKActionBase::CKActionBase(const CKActionBase&) = default; CKActionBase::CKActionBase(id target, SEL selector) noexcept : _target(target), _scopedResponderAndKey({}), _block(NULL), _variant(CKActionVariant::TargetSelector), _selectorOrIdentifier(selector) {} CKActionBase::CKActionBase(const CKComponentScope &scope, SEL selector) noexcept : CKActionBase(selector, scope.node()) { } CKActionBase::CKActionBase(SEL selector, CKTreeNode *node) noexcept : _target(nil), _scopedResponderAndKey{ .responder = node.scopeHandle.scopedResponder, .key = [node.scopeHandle.scopedResponder keyForHandle:node.scopeHandle] }, _block(NULL), _variant(CKActionVariant::Responder), _selectorOrIdentifier(selector) { RCCAssertNotNil(node.scopeHandle, @"You are creating an action that will not fire because you have an invalid scope handle."); } CKActionBase::CKActionBase(SEL selector) noexcept : _target(nil), _scopedResponderAndKey({}), _block(NULL), _variant(CKActionVariant::RawSelector), _selectorOrIdentifier(selector) {} CKActionBase::CKActionBase(dispatch_block_t block) noexcept : _target(nil), _scopedResponderAndKey({}), _block(block), _variant(CKActionVariant::Block), _selectorOrIdentifier(NULL) {} CKActionBase::CKActionBase(dispatch_block_t block, void *functionPointer, CKScopedResponder *responder, CKScopedResponderKey key) noexcept : _target(nil), _scopedResponderAndKey(CKActionBase::ScopedResponderAndKey{responder, key}), _block(block), _variant(CKActionVariant::BlockWithIdentifier), _selectorOrIdentifier(functionPointer) {}; CKActionBase::operator bool() const noexcept { return _block != NULL || _selectorOrIdentifier != NULL || _scopedResponderAndKey.responder != nil; } CKActionBase::~CKActionBase() {} SEL CKActionBase::selector() const noexcept { if (_variant == CKActionVariant::BlockWithIdentifier || _variant == CKActionVariant::Block) { return nil; } return (SEL)_selectorOrIdentifier; } std::string CKActionBase::identifier() const noexcept { switch (_variant) { case CKActionVariant::RawSelector: return std::string(sel_getName((SEL)_selectorOrIdentifier)) + "-Selector"; case CKActionVariant::TargetSelector: return std::string(sel_getName((SEL)_selectorOrIdentifier)) + "-TargetSelector-" + std::to_string((long)_target); case CKActionVariant::Responder: return std::string(sel_getName((SEL)_selectorOrIdentifier)) + "-Responder-" + std::to_string((long)_scopedResponderAndKey.responder); case CKActionVariant::Block: return "Block-" + std::to_string((long)_block); case CKActionVariant::BlockWithIdentifier: if (CKReadGlobalConfig().actionShouldUseCustomIdentifierInIdentifierString) { return std::string("BlockWithIdentifier-") + std::to_string((long)_scopedResponderAndKey.responder) + "-" + std::to_string((long)_selectorOrIdentifier); } else { return std::string("BlockWithIdentifier-") + std::to_string((long)_scopedResponderAndKey.responder) + "-" + std::to_string((long)_block); } } } dispatch_block_t CKActionBase::block() const noexcept { return _block; } CKTreeNode *CKActionBase::nodeFromContext(const CK::BaseSpecContext &context) noexcept { // Requires CKComponentInternal.h which shouldn't be imported publicly. return componentFromContext(context).treeNode; } CKComponent *CKActionBase::componentFromContext(const CK::BaseSpecContext &context) noexcept { const auto component = context._component; #if DEBUG RCCAssertNotNil(component, @"BaseSpecContext contains nil component"); #endif return ((CKComponent *)component); } #pragma mark - Sending CKActionInfo CKActionFind(SEL selector, id target) noexcept { // If we don't have a selector or target, we bail early. if (!selector || !target) { return {}; } id responder = ([target respondsToSelector:@selector(targetForAction:withSender:)] ? [target targetForAction:selector withSender:target] : target); RCCAssert(![responder isProxy], @"NSProxy can't be a responder for target-selector CKAction. Please use a block action instead."); IMP imp = [responder methodForSelector:selector]; while (!imp) { // From https://www.mikeash.com/pyblog/friday-qa-2009-03-27-objective-c-message-forwarding.html // 1. Lazy method resolution if ( [[responder class] resolveInstanceMethod:selector]) { imp = [responder methodForSelector:selector]; // The responder resolved its instance method, we now have a valid responder/signature break; } // 2. Fast-forwarding path id forwardingTarget = [responder forwardingTargetForSelector:selector]; if (!forwardingTarget || forwardingTarget == responder) { // Bail, the object they're asking us to message will just crash if the method is invoked on them RCCFailAssertWithCategory(NSStringFromSelector(selector), @"Forwarding target failed for action: %@ %@", NSStringFromSelector(selector), target); return {}; } responder = forwardingTarget; RCCAssert(![responder isProxy], @"NSProxy can't be a responder for target-selector CKAction. Please use a block action instead."); imp = [responder methodForSelector:selector]; } RCCAssert(imp != nil, @"IMP not found for selector => SEL: %@ | target: %@", NSStringFromSelector(selector), [target class]); return {imp, responder}; } #pragma mark - Legacy Send Functions void CKActionSend(const CKAction<> &action, CKComponent *sender) { action.send(sender); } void CKActionSend(const CKAction<> &action, CKComponent *sender, CKActionSendBehavior behavior) { action.send(sender, behavior); } void CKActionSend(const CKAction<id> &action, CKComponent *sender, id context) { action.send(sender, action.defaultBehavior(), context); } void CKActionSend(const CKAction<id> &action, CKComponent *sender, id context, CKActionSendBehavior behavior) { action.send(sender, behavior, context); } #pragma mark - Control Actions @interface CKComponentActionControlForwarder : NSObject - (instancetype)initWithControlEvents:(UIControlEvents)controlEvents; - (void)handleControlEventFromSender:(UIControl *)sender withEvent:(UIEvent *)event; @end /** Stashed as an associated object on UIControl instances; contains a list of CKComponentActions. */ @interface CKComponentActionList : NSObject { @public std::unordered_map<UIControlEvents, std::vector<CKAction<UIEvent *>>> _actions; std::unordered_set<UIControlEvents> _registeredForwarders; } @end @implementation CKComponentActionList @end static void *ck_actionListKey = &ck_actionListKey; typedef std::unordered_map<UIControlEvents, CKComponentActionControlForwarder *> ForwarderMap; CKComponentViewAttributeValue CKComponentActionAttribute(const CKAction<UIEvent *> action, UIControlEvents controlEvents) noexcept { if (!action) { return { {"CKComponentActionAttribute-no-op", ^(UIControl *control, id value) {}, ^(UIControl *control, id value) {}}, // Use a bogus value for the attribute's "value". All the information is encoded in the attribute itself. @YES }; } static ForwarderMap *map = new ForwarderMap(); // access on main thread only; never destructed to avoid static destruction fiasco return { { std::string("CKComponentActionAttribute-") + action.identifier() + "-" + std::to_string(controlEvents), ^(UIControl *control, id value){ CKComponentActionList *list = RCGetAssociatedObject_MainThreadAffined(control, ck_actionListKey); if (list == nil) { list = [CKComponentActionList new]; RCSetAssociatedObject_MainThreadAffined(control, ck_actionListKey, list); } if (list->_registeredForwarders.insert(controlEvents).second) { // Since this is the first time we've seen this {control, events} pair, add a Forwarder as a target. const auto it = map->find(controlEvents); CKComponentActionControlForwarder *const forwarder = (it == map->end()) ? map->insert({controlEvents, [[CKComponentActionControlForwarder alloc] initWithControlEvents:controlEvents]}).first->second : it->second; [control addTarget:forwarder action:@selector(handleControlEventFromSender:withEvent:) forControlEvents:controlEvents]; } list->_actions[controlEvents].push_back(action); }, ^(UIControl *control, id value){ CKComponentActionList *const list = RCGetAssociatedObject_MainThreadAffined(control, ck_actionListKey); RCCAssertNotNil(list, @"Unapplicator should always find an action list installed by applicator"); auto &actionList = list->_actions[controlEvents]; auto it = CK::find(actionList, action); if (it == actionList.end()) { RCCFailAssert(@"Unapplicator should always find item in action list"); return; } actionList.erase(it); // Don't bother unsetting the action list or removing the forwarder as a target; both are harmless. } }, // Use a bogus value for the attribute's "value". All the information is encoded in the attribute itself. @YES }; } @implementation CKComponentActionControlForwarder { UIControlEvents _controlEvents; } - (instancetype)initWithControlEvents:(UIControlEvents)controlEvents { if (self = [super init]) { _controlEvents = controlEvents; } return self; } - (void)handleControlEventFromSender:(UIControl *)sender withEvent:(UIEvent *)event { CKComponentActionList *const list = RCGetAssociatedObject_MainThreadAffined(sender, ck_actionListKey); RCCAssertNotNil(list, @"Forwarder should always find an action list installed by applicator"); // Protect against mutation-during-enumeration by copying the list of actions to send: const std::vector<CKAction<UIEvent *>> copiedActions = list->_actions[_controlEvents]; CKComponent *const sendingComponent = CKMountedComponentForView(sender); for (const auto &action : copiedActions) { // If the action can be handled by the sender itself, send it there instead of looking up the chain. action.send(sendingComponent, CKActionSendBehaviorStartAtSender, event); } } @end #pragma mark - Debug Helpers std::unordered_map<UIControlEvents, std::vector<CKAction<UIEvent *>>> _CKComponentDebugControlActionsForComponent(CKComponent *const component) { #if DEBUG CKComponentActionList *const list = RCGetAssociatedObject_MainThreadAffined(component.viewContext.view, ck_actionListKey); if (list == nil) { return {}; } return list->_actions; #else return {}; #endif } BOOL checkMethodSignatureAgainstTypeEncodings(SEL selector, Method method, const std::vector<const char *> &typeEncodings) { if (selector == NULL) { return NO; } if (typeEncodings.size() + 3 < method_getNumberOfArguments(method)) { RCCFailAssert(@"Expected action method %@ to take less than %llu arguments, but it supports %llu", NSStringFromSelector(selector), (unsigned long long)typeEncodings.size(), (unsigned long long)method_getNumberOfArguments(method) - 3); return NO; } char *return_type = method_copyReturnType(method); if (return_type == NULL) { return NO; } const bool has_return_type = strcmp(return_type, "v") != 0; // "v" is void free(return_type); if (has_return_type) { RCCFailAssert(@"Component action methods should not have any return value. Any objects returned from this method will be leaked."); return NO; } // Skipping self, _cmd, and sender (the component). for (int i = 0; i + 3 < method_getNumberOfArguments(method) && i < typeEncodings.size(); i++) { char *cp_argType = method_copyArgumentType(method, i + 3); // freed later - DON'T early exit! char *methodEncoding = cp_argType; // a pointer we can move around const char *typeEncoding = typeEncodings[i]; // Type Encoding: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html // ref types get '^' prefixed to them. Since C++ would implicitly // use pass-by-ref or pass-by-value based the called function, we // treat a mismatch between ref & copy as valid. if (methodEncoding != NULL && *methodEncoding == '^') { methodEncoding++; } if (typeEncoding != NULL && *typeEncoding == '^') { typeEncoding++; } BOOL doEncodingsMatch = NO; if (methodEncoding == NULL || typeEncoding == NULL) { // nothing to compare doEncodingsMatch = YES; } else if (*methodEncoding == '{' && *typeEncoding == '{') { // types are structures. Due to an issue with c++ types not always being // encoded the same even thought they are basically the same, we only // compare the structure name. (see T23131874) const char *nameEnd = strchr(methodEncoding, '='); const size_t nameSize = nameEnd - methodEncoding; doEncodingsMatch = (nameEnd && strlen(typeEncoding) >= nameSize && strncmp(methodEncoding, typeEncoding, nameSize) == 0); } else { doEncodingsMatch = strcmp(methodEncoding, typeEncoding) == 0; } NSString *safe_methodEncoding = [NSString stringWithFormat:@"%s", methodEncoding]; free(cp_argType); if (!doEncodingsMatch) { RCCFailAssert(@"Implementation of %@ does not match expected types.\nExpected type %s, got %@", NSStringFromSelector(selector), typeEncoding, safe_methodEncoding); return NO; } safe_methodEncoding = nil; // avoids -Wunused-variable } if (method_getNumberOfArguments(method) >= 3) { char *const unasfe_methodEncoding = method_copyArgumentType(method, 2); NSString *methodEncoding = [NSString stringWithFormat:@"%s", unasfe_methodEncoding ?: ""]; free(unasfe_methodEncoding); if (methodEncoding != nil && [methodEncoding isEqualToString:@"@"] == NO) { RCCFailAssert(@"Sender of %@ is not an object.\nGot %@ instead. Please add the component as the first argument when sending an action", NSStringFromSelector(selector), methodEncoding); return NO; } } return YES; } #if DEBUG void _CKTypedComponentDebugCheckComponentScope(const CKComponentScope &scope, SEL selector, const std::vector<const char *> &typeEncodings) noexcept { _CKTypedComponentDebugCheckComponentNode(scope.node(), selector, typeEncodings); } void _CKTypedComponentDebugCheckComponentNode(CKTreeNode *node, SEL selector, const std::vector<const char *> &typeEncodings) noexcept { const auto handle = node.scopeHandle; if (handle == nil) { return; } // In DEBUG mode, we want to do the minimum of type-checking for the action that's possible in Objective-C. We // can't do exact type checking, but we can ensure that you're passing the right type of primitives to the right // argument indices. const Class klass = objc_getClass(handle.componentTypeName); RCCAssertWithCategory(klass != nil || [handle.acquiredComponent.class coalescingMode] != RCComponentCoalescingModeNone, [NSString stringWithUTF8String:handle.componentTypeName], @"Creating an action from a scope should always yield a class"); if (klass != nil) { _CKTypedComponentDebugCheckComponent(klass, selector, typeEncodings); } } void _CKTypedComponentDebugCheckTargetSelector(id target, SEL selector, const std::vector<const char *> &typeEncodings) noexcept { // In DEBUG mode, we want to do the minimum of type-checking for the action that's possible in Objective-C. We // can't do exact type checking, but we can ensure that you're passing the right type of primitives to the right // argument indices. if (selector == NULL) { return; } // If the target is `Class<CKComponentProtocol>`, we pass it to the `_CKTypedComponentDebugCheckComponent` function. if ([[target class] respondsToSelector:@selector(controllerClass)]) { _CKTypedComponentDebugCheckComponent([target class], selector, typeEncodings); return; } RCCAssert([target respondsToSelector:selector], @"Target does not respond to selector for component action. -[%@ %@]", [target class], NSStringFromSelector(selector)); Method method = class_getInstanceMethod([target class], selector); checkMethodSignatureAgainstTypeEncodings(selector, method, typeEncodings); } void _CKTypedComponentDebugCheckComponent(Class<CKComponentProtocol> klass, SEL selector, const std::vector<const char *> &typeEncodings) noexcept { // We allow component actions to be implemented either in the component, or its controller. const Class componentKlass = klass; const Class controllerKlass = [klass controllerClass]; if (selector == NULL) { return; } RCCAssert([componentKlass instancesRespondToSelector:selector] || [controllerKlass instancesRespondToSelector:selector], @"Target does not respond to selector for component action. -[%@ %@]", componentKlass, NSStringFromSelector(selector)); // Type encoding with NSMethodSignatue isn't working well for C++, so we use class_getInstanceMethod() Method method = class_getInstanceMethod(componentKlass, selector) ?: class_getInstanceMethod(controllerKlass, selector); checkMethodSignatureAgainstTypeEncodings(selector, method, typeEncodings); } #endif // This method returns a friendly-print of a responder chain. Used for debug purposes. NSString *_CKComponentResponderChainDebugResponderChain(id responder) noexcept { return (responder ? [NSString stringWithFormat:@"%@ -> %@", responder, _CKComponentResponderChainDebugResponderChain([responder nextResponder])] : @"nil"); } #pragma mark - Accessibility Actions @interface CKComponentAccessibilityCustomAction : UIAccessibilityCustomAction - (instancetype)initWithName:(NSString *)name action:(const CKAction<> &)action view:(UIView *)view; @end @implementation CKComponentAccessibilityCustomAction { __weak UIView *_ck_view; CKAction<> _ck_action; } - (instancetype)initWithName:(NSString *)name action:(const CKAction<> &)action view:(UIView *)view { if (self = [super initWithName:name target:self selector:@selector(ck_send)]) { _ck_view = view; _ck_action = action; } return self; } - (BOOL)ck_send { _ck_action.send(CKMountedComponentForView(_ck_view), CKActionSendBehaviorStartAtSender); return YES; } @end CKComponentViewAttributeValue CKComponentAccessibilityCustomActionsAttribute(const std::vector<std::pair<NSString *, CKAction<>>> &passedActions) noexcept { auto const actions = passedActions; return { { std::string(sel_getName(@selector(setAccessibilityCustomActions:))), ^(UIView *view, id value){ NSMutableArray<CKComponentAccessibilityCustomAction *> *accessibilityCustomActions = [NSMutableArray new]; for (auto const& action : actions) { if (action.first && action.second) { [accessibilityCustomActions addObject:[[CKComponentAccessibilityCustomAction alloc] initWithName:action.first action:action.second view:view]]; } } view.accessibilityCustomActions = accessibilityCustomActions; }, ^(UIView *view, id value){ view.accessibilityCustomActions = nil; } }, // Use a bogus value for the attribute's "value". All the information is encoded in the attribute itself. @YES }; } #pragma mark - Template instantiations template class CKAction<>; template class CKAction<id>;