Tips and tricks for using JavaScript in Swift

Tips and tricks for using JavaScript in Swift

The author of this article, Nate Cook, is an independent web and mobile application developer. He is the main maintainer of NSHipster after Mattt, a well-known and active Swift blogger, and the creator of SwiftDoc.org, a website that supports automatic generation of Swift online documentation. In this article, he introduces the methods and techniques of using JavaScript in Swift, which is very practical for iOS and web application engineers. The following is the translation:

In the January 2015 programming language rankings released by RedMonk, Swift's adoption rate ranking soared rapidly, jumping from 68th when it was first released to 22nd, Objective-C still ranked ***0th, and JavaScript became the most popular programming language of the year with its native experience advantage on the iOS platform.

[[132047]]

As early as 2013, Apple released OS X Mavericks and iOS 7, which both added JavaScriptCore framework, allowing developers to easily, quickly and safely write applications using JavaScript. Regardless of praise or criticism, JavaScript's dominance has become a fact. Developers flock to it, and JS tool resources are emerging in an endless stream for OS

High-speed virtual machines such as X and iOS have also flourished.

JSContext/JSValue

JSContext is the running environment of JavaScript code. A Context is an environment in which JavaScript code is executed, also called a scope. When running JavaScript code in a browser, JSContext is equivalent to a window that can easily execute JavaScript code to create variables, perform calculations, and even define functions:

  1. //Objective-C  
  2. JSContext *context = [[JSContext alloc] init];
  3. [context evaluateScript:@ "var num = 5 + 5" ];
  4. [context evaluateScript:@ "var names = ['Grace', 'Ada', 'Margaret']" ];
  5. [context evaluateScript:@ "var triple = function(value) { return value * 3 }" ];
  6. JSValue *tripleNum = [context evaluateScript:@ "triple(num)" ];
  1. //Swift  
  2. let context = JSContext()
  3. context.evaluateScript( "var num = 5 + 5" )
  4. context.evaluateScript( "var names = ['Grace', 'Ada', 'Margaret']" )
  5. context.evaluateScript( "var triple = function(value) { return value * 3 }" )
  6. let tripleNum: JSValue = context.evaluateScript( "triple(num)" )

Dynamic languages ​​like JavaScript require a dynamic type, so as shown in the first line of the code, different values ​​in JSContext are encapsulated in JSValue objects, including strings, numbers, arrays, functions, and even Error, null, and undefined.

JSValue contains a series of methods for obtaining Underlying Value, as shown in the following table:

To retrieve the tripleNum value in the above example, just use the corresponding method:

  1. //Objective-C  
  2. NSLog(@ "Tripled: %d" , [tripleNum toInt32]);
  3. // Tripled: 30
  1. //Swift  
  2. println( "Tripled: \(tripleNum.toInt32())" )
  3. // Tripled: 30  

Subscripting Values

By using subscript notation in JSContext and JSValue instances, you can easily get the existing value in the context. Among them, JSContext can only put string subscripts into objects and arrays, while JSValue can be string or integer subscripts.

  1. //Objective-C  
  2. JSValue *names = context[@ "names" ];
  3. JSValue *initialName = names[ 0 ];
  4. NSLog(@ "The first name: %@" , [initialName toString]);
  5. // The first name: Grace
  1. //Swift  
  2. let names = context.objectForKeyedSubscript( "names" )
  3. let initialName = names.objectAtIndexedSubscript( 0 )
  4. println( "The first name: \(initialName.toString())" )
  5. // The first name: Grace  

After all, the Swift language was born not long ago, so it cannot use subscript symbols as freely as Objective-C. Currently, Swift methods can only implement subscripts such as objectAtKeyedSubscript() and objectAtIndexedSubscript().

Calling Functions

We can use the Foundation class as a parameter and directly call the JavaScript function encapsulated in JSValue from Objective-C/Swift code. Here, JavaScriptCore plays a connecting role again.

  1. //Objective-C  
  2. JSValue *tripleFunction = context[@ "triple" ];
  3. JSValue *result = [tripleFunction callWithArguments:@[ @5 ] ];
  4. NSLog(@ "Five tripled: %d" , [result toInt32]);
  1. //Swift  
  2. let tripleFunction = context.objectForKeyedSubscript( "triple" )
  3. let result = tripleFunction.callWithArguments([ 5 ])
  4. println( "Five tripled: \(result.toInt32())" )

Exception Handling

JSContext also has a unique skill, which is to check and record syntax, type, and runtime errors by setting the exceptionHandler property in the context. ExceptionHandler is a callback handler that mainly receives the reference of JSContext and handles exceptions.

  1. //Objective-C  
  2. context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
  3. NSLog(@ "JS Error: %@" , exception);
  4. };
  5. [context evaluateScript:@ "function multiply(value1, value2) { return value1 * value2 " ];
  6. // JS Error: SyntaxError: Unexpected end of script  
  1. //Swift  
  2. context.exceptionHandler = { context, exception in
  3. println( "JS Error: \(exception)" )
  4. }
  5. context.evaluateScript( "function multiply(value1, value2) { return value1 * value2 " )
  6. // JS Error: SyntaxError: Unexpected end of script  

JavaScript function calls

Now that we know how to get different values ​​and call functions from the JavaScript environment, how do we get custom objects and methods defined in Objective-C or Swift in the JavaScript environment? There are two main ways to get local client code from JSContext, namely Blocks and JSExport protocols.

Blocks

In JSContext, if the Objective-C code block is assigned to an identifier, JavaScriptCore will automatically encapsulate it in a JavaScript function, making it easier to use Foundation and Cocoa classes on JavaScript - this once again proves the powerful connection of JavaScriptCore. Now CFStringTransform can also be used on JavaScript, as shown below:

  1. //Objective-C  
  2. context[@ "simplifyString" ] = ^(NSString *input) {
  3. NSMutableString *mutableString = [input mutableCopy];
  4. CFStringTransform((__bridge CFMutableStringRef)mutableString, NULL, kCFStringTransformToLatin, NO);
  5. CFStringTransform((__bridge CFMutableStringRef)mutableString, NULL, kCFStringTransformStripCombiningMarks, NO);
  6. return mutableString;
  7. };
  8. NSLog(@ "%@" , [context evaluateScript:@ "simplifyString('?????!')" ]);
  1. //Swift  
  2. let simplifyString: @objc_block String -> String = { input in
  3. var mutableString = NSMutableString(string: input) as CFMutableStringRef
  4. CFStringTransform(mutableString, nil, kCFStringTransformToLatin, Boolean( 0 ))
  5. CFStringTransform(mutableString, nil, kCFStringTransformStripCombiningMarks, Boolean( 0 ))
  6. return mutableString
  7. }
  8. context.setObject(unsafeBitCast(simplifyString, AnyObject.self), forKeyedSubscript: "simplifyString" )
  9. println(context.evaluateScript( "simplifyString('?????!')" ))
  10. // annyeonghasaeyo!  

It should be noted that Swift's speedbump only applies to Objective-C blocks and is useless for Swift closures. To use a closure in a JSContext, there are two steps: first, declare it with @objc_block, and second, convert Swift's knuckle-whitening unsafeBitCast() function to AnyObject.

Memory Management

Code blocks can capture variable references, and strong references to all JSContext variables are retained in JSContext, so be careful to avoid circular strong reference problems. In addition, do not capture JSContext or any JSValues ​​in code blocks. It is recommended to use [JSContext currentContext] to obtain the current Context object and pass the value as a parameter to the block according to specific needs.

JSExport Protocol

With the help of JSExport protocol, you can also use custom objects on JavaScript. Instance methods and class methods declared in JSExport protocol can automatically interact with JavaScript regardless of their attributes. The specific practice process will be introduced later in this article.

JavaScriptCore Practice

We can better understand how to use the above techniques through some examples. First, define a Person model that conforms to the JSExport subprotocol PersonJSExport, and then use JavaScript to create and fill in the instance in JSON. What is the use of NSJSONSerialization when there is an entire JVM?

PersonJSExports and Person

The PersonJSExports protocol implemented by the Person class specifies the available JavaScript properties. , class methods are required at creation time because JavaScriptCore does not apply initialization conversions and we cannot use var person = new Person() as we would with native JavaScript types.

  1. //Objective-C  
  2. // in Person.h ------------------  
  3. @class Person;
  4. @protocol PersonJSExports @property (nonatomic, copy) NSString *firstName;
  5. @property (nonatomic, copy) NSString *lastName;
  6. @property NSInteger ageToday;
  7. - (NSString *)getFullName;
  8. // create and return a new Person instance with `firstName` and `lastName`  
  9. + (instancetype)createWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;
  10. @end  
  11. @interface Person: NSObject @property (nonatomic, copy) NSString *firstName;
  12. @property (nonatomic, copy) NSString *lastName;
  13. @property NSInteger ageToday;
  14. @end  
  15. // in Person.m -----------------  
  16. @implementation Person
  17. - (NSString *)getFullName {
  18. return [NSString stringWithFormat:@ "%@ %@" , self.firstName, self.lastName];
  19. }
  20. + (instancetype) createWithFirstName:(NSString *)firstName lastName:(NSString *)lastName {
  21. Person *person = [[Person alloc] init];
  22. person.firstName = firstName;
  23. person.lastName = lastName;
  24. return person;
  25. }
  26. @end  
  1. //Swift  
  2. // Custom protocol must be declared with `@objc`  
  3. @objc protocol PersonJSExports : JSExport {
  4. var firstName: String { get set }
  5. var lastName: String { get set }
  6. var birthYear: NSNumber? { get set }
  7. func getFullName() -> String
  8. /// create and return a new Person instance with `firstName` and `lastName`  
  9. class func createWithFirstName(firstName: String, lastName: String) -> Person
  10. }
  11. // Custom class must inherit from `NSObject`  
  12. @objc   class Person : NSObject, PersonJSExports {
  13. // properties must be declared as `dynamic`  
  14. dynamic var firstName: String
  15. dynamic var lastName: String
  16. dynamic var birthYear: NSNumber?
  17. init(firstName: String, lastName: String) {
  18. self.firstName = firstName
  19. self.lastName = lastName
  20. }
  21. class func createWithFirstName(firstName: String, lastName: String) -> Person {
  22. return Person(firstName: firstName, lastName: lastName)
  23. }
  24. func getFullName() -> String {
  25. return   "\(firstName) \(lastName)"  
  26. }
  27. }

Configuring JSContext

After creating the Person class, you need to export it to the JavaScript environment and import the Mustache JS library to apply templates to the Person object.

  1. //Objective-C  
  2. // export Person class  
  3. context[@ "Person" ] = [Person class ];
  4. // load Mustache.js  
  5. NSString *mustacheJSString = [NSString stringWithContentsOfFile:... encoding:NSUTF8StringEncoding error:nil];
  6. [context evaluateScript:mustacheJSString];
  1. //Swift  
  2. // export Person class  
  3. context.setObject(Person.self, forKeyedSubscript: "Person" )
  4. // load Mustache.js  
  5. if let mustacheJSString = String(contentsOfFile:..., encoding:NSUTF8StringEncoding, error:nil) {
  6. context.evaluateScript(mustacheJSString)
  7. }

JavaScript Data & Processing

The following is a simple JSON example and how to create a new Person instance using JSON.

Note: JavaScriptCore implements the interaction between Objective-C/Swift method names and JavaScript code. Because JavaScript does not have named parameters, any additional parameter names are Camel-Case and appended to the function name. In this example, the Objective-C method createWithFirstName:lastName: becomes createWithFirstNameLastName() in JavaScript.

  1. //JSON  
  2. [
  3. { "first" : "Grace" , "last" : "Hopper" , "year" : 1906 },
  4. { "first" : "Ada" , "last" : "Lovelace" , "year" : 1815 },
  5. { "first" : "Margaret" , "last" : "Hamilton" , "year" : 1936 }
  6. ]
  1. //JavaScript  
  2. var loadPeopleFromJSON = function(jsonString) {
  3. var data = JSON.parse(jsonString);
  4. var people = [];
  5. for (i = 0 ; i < data.length; i++) {
  6. var person = Person.createWithFirstNameLastName(data[i].first, data[i].last);
  7. person.birthYear = data[i].year;
  8. people.push(person);
  9. }
  10. return people;
  11. }

Try it yourself

Now you just need to load the JSON data, call it in JSContext, parse it into an array of Person objects, and render it with a Mustache template:

  1. //Objective-C  
  2. // get JSON string  
  3. NSString *peopleJSON = [NSString stringWithContentsOfFile:... encoding:NSUTF8StringEncoding error:nil];
  4. // get load function  
  5. JSValue *load = context[@ "loadPeopleFromJSON" ];
  6. // call with JSON and convert to an NSArray  
  7. JSValue *loadResult = [load callWithArguments:@[peopleJSON]];
  8. NSArray *people = [loadResult toArray];
  9. // get rendering function and create template  
  10. JSValue *mustacheRender = context[@ "Mustache" ][@ "render" ];
  11. NSString *template = @ "{{getFullName}}, born {{birthYear}}" ;
  12. // loop through people and render Person object as string  
  13. for (Person *person in people) {
  14. NSLog(@ "%@" , [mustacheRender callWithArguments:@[template, person]]);
  15. }
  16. // Output:  
  17. // Grace Hopper, born 1906  
  18. // Ada Lovelace, born 1815  
  19. // Margaret Hamilton, born 1936  
  1. //Swift  
  2. // get JSON string  
  3. if let peopleJSON = NSString(contentsOfFile:..., encoding: NSUTF8StringEncoding, error: nil) {
  4. // get load function  
  5. let load = context.objectForKeyedSubscript( "loadPeopleFromJSON" )
  6. // call with JSON and convert to an array of `Person`  
  7. if let people = load.callWithArguments([peopleJSON]).toArray() as? [Person] {
  8. // get rendering function and create template  
  9. let mustacheRender = context.objectForKeyedSubscript( "Mustache" ).objectForKeyedSubscript( "render" )
  10. let template = "{{getFullName}}, born {{birthYear}}"  
  11. // loop through people and render Person object as string  
  12. for person in people
  13. println(mustacheRender.callWithArguments([template, person]))
  14. }
  15. }
  16. }
  17. // Output:  
  18. // Grace Hopper, born 1906  
  19. // Ada Lovelace, born 1815  
  20. // Margaret Hamilton, born 1936  

<<:  Swift TIP: objc and dynamic

>>:  Quickly understand Swift's classes and structures based on OC

Recommend

Three Management Axes - Alibaba's Cadre Training Tool

Three Management Axes - Introduction to Alibaba&#...

French car owner sues Mercedes-Benz for diesel emissions cheating?

According to Reuters, three French Mercedes-Benz ...

How to operate new media? Share in 4 dimensions!

When it comes to new media , everyone will think ...

IPv6 addresses are running out, and operators are launching IPv6 pilot projects

As the global IPv4 addresses are about to run out...

Maoyan vs Tao Piaopiao competitive product analysis!

In modern society, people's leisure and enter...

How to do foreign trade promotion? Here are 4 effective promotion channels

Most of our friends can tell you a thing or two a...

Event operation and promotion data analysis formula!

This article will focus on operational activities...

Google confesses to providing employee data to the FBI

On December 23, 2014, Christmas Eve, Google infor...

How much bandwidth is needed to rent a server for APP with 1 million users?

In the 5G smart era, I believe that everyone’s sm...

Some thoughts on Android APP performance optimization

When it comes to Android phones, most people have...