Created
March 10, 2014 00:43
-
-
Save keicoder/9457498 to your computer and use it in GitHub Desktop.
objective-c : Custom UITextView with JavaScript Core
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Custom UITextView with JavaScript Core | |
//1. Setting up the console | |
//ConsoleTextView.h | |
@interface ConsoleTextView : UITextView | |
- (void)setText:(NSString *)text concatenate:(BOOL)concatenate; | |
- (void)clear; | |
@end | |
//ConsoleTextView.m | |
@interface ConsoleTextView() <UITextViewDelegate> | |
@end | |
@implementation ConsoleTextView | |
- (instancetype)initWithCoder:(NSCoder *)aDecoder { | |
self = [super initWithCoder:aDecoder]; | |
if (self) { | |
//Initialization code | |
self.delegate = self; | |
[self configureTextView]; | |
} | |
return self; | |
} | |
- (void)configureTextView { | |
self.textContainerInset = UIEdgeInsetsMake(0, 20, 0, 20); //top, left, bottom, right | |
self.textColor = [UIColor whiteColor]; | |
self.font = [UIFont fontWithName:@"Courier" size:20]; | |
} | |
- (void)setText:(NSString *)text concatenate:(BOOL)concatenate { | |
if (concatenate) { | |
text = [NSString stringWithFormat:@"\n%@", text]; | |
self.text = [self.text stringByAppendingString:text]; | |
} | |
else { | |
self.text = text; | |
} | |
self.scrollEnabled = NO; | |
[self.delegate textViewDidChange:self]; | |
} | |
- (void)clear { | |
self.text = @""; | |
} | |
#pragma mark - UITextViewDelegate | |
- (void)textViewDidChange:(UITextView *)textView { | |
self.scrollEnabled = YES; | |
float margin = 0; | |
if (self.contentSize.height + margin > self.frame.size.height) { | |
CGPoint offset = CGPointMake(0, self.contentSize.height - self.frame.size.height + margin); | |
[self setContentOffset:offset animated:YES]; //sets the offset from the content view’s origin that corresponds to the receiver’s origin. | |
} | |
} | |
@end | |
//XorkViewController.h | |
@interface XorkViewController : UIViewController | |
@end | |
//XorkViewController.m | |
#import "XorkViewController.h" | |
#import "ConsoleTextView.h" | |
@interface XorkViewController () <UITextFieldDelegate> | |
@property (strong, nonatomic) IBOutlet ConsoleTextView *outputTextView; | |
@property (strong, nonatomic) IBOutlet UITextField *inputTextField; | |
@property (strong, nonatomic) IBOutlet UINavigationBar *navigationBar; | |
@end | |
@implementation XorkViewController | |
- (void)viewWillAppear:(BOOL)animated { | |
[super viewWillAppear:animated]; | |
self.inputTextField.delegate = self; | |
[self.inputTextField becomeFirstResponder]; | |
UIFont *navBarFont = [UIFont fontWithName:@"Courier" size:23]; | |
NSDictionary *attributes = @{NSFontAttributeName : navBarFont}; | |
[self.navigationBar setTitleTextAttributes:attributes]; | |
} | |
#pragma mark - UITextFielDelegate methods | |
- (BOOL)textFieldShouldReturn:(UITextField *)textField { | |
//1 | |
NSString* inputString = textField.text; | |
[inputString lowercaseString]; | |
//2 | |
if ([inputString isEqualToString:@"clear"]) { | |
[self.outputTextView clear]; | |
//3 | |
} else { | |
[self.outputTextView setText:inputString concatenate:YES]; //by default, the text view overwrites everything, Passing YES into setText:concatenate: preserves the text that was showing before | |
[self.inputTextField setText:@""]; | |
} | |
return YES; | |
} | |
@end | |
//2. Printing to the console from JavaScript | |
//create a new JavaScript file in Xcode | |
//File > New > File.... > iOS > Other > Empty template > Name the file hello.js > Create | |
//in this case don't add hello.js to the Xork target | |
//project file > Build Phases > Copy Bundle Resources > add a file hello.js | |
//write JavaScript function | |
//hello.js | |
//JavaScript print() function will be defined with Objective-C code! | |
function startGame() | |
{ | |
print("Hello World"); | |
} | |
//XorkViewController.m | |
@import JavaScriptCore; | |
@interface XorkViewController () <UITextFieldDelegate> | |
//... | |
@property (strong, nonatomic) JSContext *context; //JSContext object as an execution environment to run JavaScript | |
@end | |
- (void)viewDidLoad { | |
[super viewDidLoad]; | |
//below code runs the startGame method in hello.js, which will then in-turn call the print method you defined in an Objective-C block | |
//1. Grab the contents of hello.js and store it as a string. | |
NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"hello" | |
ofType:@"js"]; | |
NSString *scriptString = [NSString stringWithContentsOfFile:scriptPath | |
encoding:NSUTF8StringEncoding | |
error:nil]; | |
//2. Initialize a JSContext instance to evaluate and run the string containing the script | |
self.context = [[JSContext alloc] init]; | |
[self.context evaluateScript:scriptString]; | |
//3. secret sauce. define the JavaScript function print() inside the JSContext | |
__weak XorkViewController *weakSelf = self; //weak reference to self to avoid a retain cycle | |
self.context[@"print"] = ^(NSString* text) { | |
text = [NSString stringWithFormat:@"%@\n", text]; | |
[weakSelf.outputTextView setText:text concatenate:YES]; | |
}; | |
//4. Get a reference to the startGame function defined in hello.js | |
JSValue *function = self.context[@"startGame"]; | |
[function callWithArguments:@[]]; //call it with an empty argument array since it doesn’t take any arguments | |
} | |
//3. Building Game Logic | |
//replace viewDidLoad method with the following | |
- (void)viewDidLoad { | |
[super viewDidLoad]; | |
//below code runs the startGame method in hello.js, which will then in-turn call the print method you defined in an Objective-C block | |
//1. Grab the contents of .js, .json files and store it as a string, array. | |
NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"xork" | |
ofType:@"js"]; | |
NSString *scriptString = [NSString stringWithContentsOfFile:scriptPath | |
encoding:NSUTF8StringEncoding | |
error:nil]; | |
NSString *dataPath = [[NSBundle mainBundle] pathForResource:@"data" | |
ofType:@"json"]; | |
NSString *dataString = [NSString stringWithContentsOfFile:dataPath | |
encoding:NSUTF8StringEncoding | |
error:nil]; | |
NSData *jsonData = [dataString dataUsingEncoding:NSUTF8StringEncoding]; | |
NSError *error; | |
NSArray *jsonArray = [NSJSONSerialization JSONObjectWithData:jsonData | |
options:0 | |
error:&error]; | |
if (error) { | |
NSLog(@"%@", @"NSJSONSerialization error"); | |
return; | |
} | |
//2. Initialize a JSContext instance to evaluate and run the string containing the script | |
self.context = [[JSContext alloc] init]; | |
[self.context evaluateScript:scriptString]; | |
//3. secret sauce. define the JavaScript function print() inside the JSContext | |
__weak XorkViewController *weakSelf = self; //weak reference to self to avoid a retain cycle | |
self.context[@"print"] = ^(NSString* text) { | |
text = [NSString stringWithFormat:@"%@\n", text]; | |
[weakSelf.outputTextView setText:text concatenate:YES]; | |
}; | |
//4. Get a reference to the startGame() function defined in xork.js | |
//startGame() takes an array of rooms. | |
//Convert the NSArray representation to a JavaScript object using valueWithObject:inContext: | |
//finally, call startGame() with callWithArguments: to start the game. | |
JSValue *function = self.context[@"startGame"]; | |
JSValue *dataValue = [JSValue valueWithObject:jsonArray | |
inContext:self.context]; | |
[function callWithArguments:@[dataValue]]; //call it with an argument array | |
} | |
#pragma mark - UITextFielDelegate methods | |
//replace [self.outputTextView setText:inputString concatenate:YES]; to the following code | |
- (BOOL)textFieldShouldReturn:(UITextField *)textField | |
{ | |
//... | |
//3 | |
} else { | |
[self processUserInput:inputString]; //app forwards the text input to the JavaScript function processUserInput() | |
//... | |
} | |
return YES; | |
} | |
//app forwards the text input to the JavaScript function processUserInput() | |
- (void)processUserInput:(NSString *)input | |
{ | |
JSValue *function = self.context[@"processUserInput"]; | |
JSValue *value = [JSValue valueWithObject:input inContext:self.context]; | |
[function callWithArguments:@[value]]; | |
} | |
//4. Extending the game logic | |
//xork.js | |
//function processCommand(action, object) | |
//add the following else-if statement immediately before the closing else statement | |
function processCommand(action, object) { | |
//... | |
else if (action == "take") { | |
take(object); | |
} | |
//... | |
} | |
//A global array map contains all the rooms passed into startGame(). A global index currentRoom tracks your position in the map array as you navigate the game. The function getCurrentRoom() simply returns the Room object you’re in according to the currentRoom index. | |
//The Room object contains a hasItem() function that indicates whether or not a particular item is in the room. If it is, add the Item object to the global inventory array and remove it from the Room. | |
//Alert the user if the item they want to take isn't in the room or doesn't exist. | |
function take(itemName) { | |
//1 | |
var room = getCurrentRoom(); | |
//2 | |
if (room.hasItem(itemName)) { | |
item = room.itemForName(itemName); | |
inventory.addItem(item); | |
room.removeItem(itemName); | |
print("You picked it up. Woot!"); | |
} | |
//3 | |
else { | |
print("You can't pick that up."); | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment