Objective-C Interoperability
Several features exist to enable seamless interoperability between Objective-C and ObjectiveScript code.
Global Class Resolution
It's extremely easy to access an Objective-C class in ObjectiveScript. Just access the class name in the global scope. E.g. NSString
(or global.NSString
) will yield the NSString
class.
Objects
Objective-C objects and classes are wrapped in JXObject
when being passed to ObjectiveScript code. This wrapper adds a few bits of functionality.
Method Calling
Methods can be called on JXObject
using Objective-C like syntax. E.g. [tableView reloadData]
or [NSString new]
.
Dot Syntax
Just like in Objective-C, methods can also be called using dot syntax, for example NSString.new
(note the lack of parentheses).
In addition, properties can be read and written to using dot syntax as well.
if (!window.rootViewController) {
window.rootViewController = [UIViewController new];
}
Boxing
When passing a pure JavaScript type to [Objective-]C code that expects an id
, the following automatic boxing occurs
null
: converted tonil
string
: converted toNSString
number
,boolean
: converted toNSNumber
Date
: converted toNSDate
Array
: deep-converted toNSArray
(i.e. elements are also boxed)Object
: deep-converted toNSDictionary
(only enumerable keys)
It's possible to manually box a JavaScript type into its Objective-C equivalent using @(<value>)
. It's also possible to use @<literal>
in the case of string, numeric, boolean, array, and object literals.
Unboxing
The ObjectiveScript runtime attempts to respect the type system's requests to convert Objective-C objects to strings and numbers via Symbol.toPrimitive
, by returning the type's description
. Additionally, it is possible to manually unbox an Objective-C object into a high fidelity JavaScript counterpart using unbox
.
function unbox(object: JXObject): any
The following rules are applied:
NSString
: converted tostring
NSNumber
: converted tonumber
orboolean
NSDate
: converted toDate
NSArray
: deep-converted toArray
NSDictionary
: deep-converted toObject
Other classes are simply passed through.
Blocks
It's possible to declare a block in ObjectiveScript using an Objective-C like block syntax.
^<return-type>(params) { /* code */ };
For example
[UIAlertAction actionWithTitle:"Confirm" style:1 handler:^(id action) {
// do stuff
}];
Defining Classes
One can define classes purely in ObjectiveScript. The syntax for this is
@class <class name> : <superclass name>
// methods
@end
It's also possible to register instance variables. These are internally implemented as associated objects at the moment, so they must be id
s for now.
@class <class name> : <superclass name> {
id <name>;
...
}
// methods
@end
Furthermore, a class can declare conformance to protocols if necessary, using Objective-C like syntax. For example
@class MyViewController : UIViewController <UITableViewDelegate, UITableViewDataSource> {
id _objects;
}
// methods
@end
Defining Methods
Methods are defined inside classes, using Objective-C like syntax.
- (<return type>)methodWithNoArgs { /* code */ }
- (<return type>)methodWithOneArg:(<type>)argOne { /* code */ }
- (<return type>)methodWithFirstArg:(<type>)argOne
secondArg:(<type>)argTwo /* and so on */ { /* code */ }
During a method call, the runtime gives the code access to the callee JXObject
via self
. It's also possible to call superclass methods using the super
keyword. For example,
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// do stuff
}
A gotcha of the way self
is implemented is that it's set globally upon entering a user-defined method, and unset upon exiting it. Therefore asynchronous blocks executed in the method won't be able to access self
even though it looks like it's inside the correct scope. To work around this, save self
to a local variable, which can later be accessed inside the block.
Hooking Classes
One can swizzle methods of an existing class using syntax similar to that used to create a class.
Similar to Logos, one first sets up a %hook
for the class.
%hook <class name> /* { id _associatedObj; } */
// methods
%end
Any methods inside the hook matching an existing method on the class will be replaced. Methods that do not match an existing one will be added to the class.
%orig
It's possible to call %orig
from within a user-defined method hook to invoke the original method. When %orig
is called without parentheses (%orig;
), all arguments are passed through to the method. This doesn't mean that %orig;
disallows changing the arguments' values though; if you change an argument variable inside the method, it'll reflect in the value that %orig;
passes through. For example, the following snippet will make it so that -[UILabel setText:]
always receives the string "hi".
%hook UILabel
- (void)setText:(id)text {
text = "hi";
%orig;
}
%end
If you wish to pass values other than arguments
to the original method, invoke %orig
with parentheses, passing in the desired values. For example, the following is functionally equivalent to the previous example.
%hook UILabel
- (void)setText:(id)text {
%orig("hi");
}
%end
It's also possible to not call %orig
at all, which makes it so that the original method is never called.
orig
orig
is the function which %orig
uses as its underlying implementation. The difference between the two is that orig
expects an array of arguments, while %orig
expects them to be supplied in a variadic manner.
function orig(args: any[]): any
In asynchronous code, %orig
faces a similar issue to self
. In such a situation, save orig
to a local variable and pass it an array of the desired arguments inside the block. To replicate the behavior of %orig;
, use orig(arguments)
.