Skip to content

Instantly share code, notes, and snippets.

@jspeth
Forked from interstateone/iOS7JSCTest.m
Created February 23, 2019 14:16
Show Gist options
  • Save jspeth/060091525fd89dd0f3d7409ab2513d55 to your computer and use it in GitHub Desktop.
Save jspeth/060091525fd89dd0f3d7409ab2513d55 to your computer and use it in GitHub Desktop.
A documented example of how a user might script a UIKit UI.
//
// ViewController.m
// PencilCaseJSDemo
//
// Created by Brandon on 12/3/2013.
// Copyright (c) 2013 Robots and Pencils. All rights reserved.
//
#import "ViewController.h"
@import JavaScriptCore;
@import ObjectiveC;
@protocol UIViewExport <JSExport>
@property (nonatomic) CGPoint center;
@property (nonatomic) CGRect frame;
@property (nonatomic, copy) UIColor *backgroundColor;
- (void)addSubview:(UIView *)view;
// Support for passing JS functions as method parameters to bound methods that expect blocks was removed from JSC
// That means methods like this won't work in JS
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations;
@end
// Based on https://github.com/steamclock/jscalc/tree/master/iOS/JSCalc
// Might be able to have more than one handler with some runtime magic
// Initial support for just touch up inside is probably fine though
@implementation UIControl (JSAction)
static const char *BlockKey = "SCBlocks";
- (void)addEventHandler:(JSValue *)handler forControlEvents:(UIControlEvents)controlEvents {
objc_setAssociatedObject(self, &BlockKey, handler, OBJC_ASSOCIATION_RETAIN);
[self addTarget:self action:@selector(handleControlEventWithSender:event:) forControlEvents:controlEvents];
}
- (void)setTouchUpInsideHandler:(JSValue *)handler {
[self addEventHandler:handler forControlEvents:UIControlEventTouchUpInside];
}
- (void)handleControlEventWithSender:(id)sender event:(UIControlEvents)event {
JSValue *handler;
handler = objc_getAssociatedObject(self, &BlockKey);
if (handler) {
[handler callWithArguments:@[]];
}
}
@end
@protocol UIControlExport <JSExport>
- (void)addEventHandler:(JSValue *)handler forControlEvents:(UIControlEvents)controlEvents;
- (void)setTouchUpInsideHandler:(JSValue *)handler;
@end
@protocol UIButtonExport <JSExport>
@property (nonatomic, retain) UIColor *tintColor;
- (void)setTitle:(NSString *)title forState:(UIControlState)state;
@end
@protocol UIColorExport <JSExport>
+ (UIColor *)redColor;
+ (UIColor *)blueColor;
+ (UIColor *)blackColor;
+ (UIColor *)whiteColor;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
JSContext *context = [[JSContext alloc] init];
// Add Lo-Dash
NSString *lodashPath = [[NSBundle mainBundle] pathForResource:@"lodash" ofType:@"js"];
NSString *lodash = [NSString stringWithContentsOfFile:lodashPath encoding:NSUTF8StringEncoding error:NULL];
[context evaluateScript:lodash];
// Add EventEmitter
NSString *eventEmitterPath = [[NSBundle mainBundle] pathForResource:@"EventEmitter" ofType:@"js"];
NSString *eventEmitter = [NSString stringWithContentsOfFile:eventEmitterPath encoding:NSUTF8StringEncoding error:NULL];
[context evaluateScript:eventEmitter];
// Automatic protocol creation doesn't work yet because of a bug (https://bugs.webkit.org/show_bug.cgi?id=122501)
//[self exportClass:[UIButton class] toContext:context];
//[self exportClass:[UIControl class] toContext:context];
//[self exportClass:[UIColor class] toContext:context];
// Export some classes
class_addProtocol([UIView class], @protocol(UIViewExport));
class_addProtocol([UIControl class], @protocol(UIControlExport));
class_addProtocol([UIButton class], @protocol(UIButtonExport));
class_addProtocol([UIColor class], @protocol(UIColorExport));
context[@"UIView"] = [UIView class];
context[@"UIControl"] = [UIControl class];
context[@"UIButton"] = [UIButton class];
context[@"UIColor"] = [UIColor class];
// Add EventEmitter to classes
[context evaluateScript:@"var event = new EventEmitter();"];
// We can expose functions like this
// Note that in this particular case we can simply use an object like { x: 0, y: 0, width: 100, height: 100 } in JS
// We get support for conversion of CGPoint, NSRange, CGRect and CGSize for free, but it's not hard to do
context[@"CGRectMake"] = (JSValue *) ^(CGFloat x, CGFloat y, CGFloat width, CGFloat height){
return CGRectMake(x, y, width, height);
};
// We will either need to write some categories for UIKit to avoid needing constants like this
// e.g. - setTitle:(NSString *)title;
// Or we can manually export all of the constants
context[@"UIControlStateNormal"] = @(UIControlStateNormal);
context[@"UIControlEventTouchUpInside"] = @(UIControlEventTouchUpInside);
context[@"UIButton"] = [UIButton class];
context[@"UIControl"] = [UIControl class];
context[@"UIColor"] = [UIColor class];
context[@"log"] = ^(NSString *message) {
NSLog(@"%@", message);
};
// Constructors from Objective-C won't work unless we build JavaScriptCore ourselves since the fix isn't in iOS7 yet
// This is how it *should* work though
// [context evaluateScript:@"button = UIButton.new(); button.frame = { x: 0, y: 0, width: 100, height: 44 }; button.setTitleForState('WTFJS', UIControlStateNormal); view.addSubview(button);"];
// Here's an example workaround:
context[@"createUIButton"] = (UIButton *)^{
return [UIButton buttonWithType:UIButtonTypeRoundedRect];
};
/* -----------------------------------------
Simulate deserializing a slide from JSON
-------------------------------------- */
// The parent view
context[@"view"] = self.view;
// Create a button
// Note that no frame is set yet
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button setTitle:@"Test" forState:UIControlStateNormal];
[self.view addSubview:button];
NSString *buttonInstanceName = @"button";
context[buttonInstanceName] = button;
// Run the button's script property from the JSON
// The script is wrapped in a self-executing function and scoped to the UI element itself
// Scripts should be evaluated top-down in the view hierarchy, and shouldn't have access to subviews
NSString *buttonScript = @"this.frame = {x: 0, y: 0, width: 100, height: 44}; this.center = view.center; this.tintColor = UIColor.redColor();";
[self evaluateScript:buttonScript withInstanceName:buttonInstanceName inContext:context];
// Let's add a listener on the parent view
// Note that the scope of the event listener is the EventEmitter object itself
NSString *viewScript = @"var black = false; event.on('makeItBlack', function() { view.backgroundColor = black ? UIColor.whiteColor() : UIColor.blackColor(); black = !black; });";
[self evaluateScript:viewScript withInstanceName:@"view" inContext:context];
// Let's create another button in JS (with our constructor workaround) that will trigger that event
NSString *newButtonScript = @"var newButton = createUIButton(); newButton.frame = {x: 0, y: 0, width: 100, height: 44}; newButton.center = { x: view.center.x, y: view.center.y + 52 }; newButton.tintColor = UIColor.blueColor(); newButton.setTitleForState('Make it black', UIControlStateNormal); this.addSubview(newButton); newButton.addEventHandlerForControlEvents(function() { event.trigger('makeItBlack'); }, UIControlEventTouchUpInside);";
[self evaluateScript:newButtonScript withInstanceName:@"view" inContext:context];
}
- (void)evaluateScript:(NSString *)script withInstanceName:(NSString *)instanceName inContext:(JSContext *)context {
NSString *wrappedScript = [NSString stringWithFormat:@"(function() { %@ }).call(%@);", script, instanceName];
[context evaluateScript:wrappedScript];
}
- (void)exportClass:(Class)class toContext:(JSContext *)context {
// Create a protocol that inherits from JSExport and with all the public methods and properties of the class
const char *protocolName = class_getName(class);
Protocol *protocol = objc_allocateProtocol(protocolName);
protocol_addProtocol(protocol, objc_getProtocol("JSExport"));
// Add the public methods of the class to the protocol
NSUInteger methodCount, classMethodCount, propertyCount;
Method *methods, *classMethods;
objc_property_t *properties;
methods = class_copyMethodList(class, &methodCount);
for (NSUInteger methodIndex = 0; methodIndex < methodCount; ++methodIndex) {
Method method = methods[methodIndex];
protocol_addMethodDescription(protocol, method_getName(method), method_getTypeEncoding(method), YES, YES);
}
classMethods = class_copyMethodList(object_getClass(class), &classMethodCount);
for (NSUInteger methodIndex = 0; methodIndex < classMethodCount; ++methodIndex) {
Method method = classMethods[methodIndex];
protocol_addMethodDescription(protocol, method_getName(method), method_getTypeEncoding(method), YES, NO);
}
properties = class_copyPropertyList(class, &propertyCount);
for (NSUInteger propertyIndex = 0; propertyIndex < propertyCount; ++propertyIndex) {
objc_property_t property = properties[propertyIndex];
NSUInteger attributeCount;
objc_property_attribute_t *attributes = property_copyAttributeList(property, &attributeCount);
protocol_addProperty(protocol, property_getName(property), attributes, attributeCount, YES, YES);
free(attributes);
}
free(methods);
free(classMethods);
free(properties);
// Add the new protocol to the class
objc_registerProtocol(protocol);
class_addProtocol(class, protocol);
NSString *className = [NSString stringWithCString:class_getName(class) encoding:NSUTF8StringEncoding];
context[className] = class;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment