This website is based on Docsify, hosted on the GitHub, pull requests are welcome.
Create powerful addins for JSBox with JavaScript, ES6 is supported, and we provide tons of APIs to interact with iOS directly
JSBox 2.0 provides Node.js runtime, you can also use Node APIs. For details, please refer to: https://cyanzhong.github.io/jsbox-nodejs/#/en/
> There's a simple code editor inside JSBox, provides auto completion and syntax highlighting, it will be better in the future
> For example: jsbox://import?url=url&name=name&icon=icon
> - Parameters:
> - url
: Code file url
> - name
: The script name
> - icon
: JSBox icon set name, refer: https://github.com/cyanzhong/xTeko/tree/master/extension-icons
> - Note:
> - Parameters should be URL Encoded
> - Please use ASCII characters to naming a URL
> 1. Open editor settings in JSBox, turn on Debug Mode
> 2. Back to home screen and restart JSBox, switch to the 4th tab to check Host
> 3. Search & install JSBox
extension in VSCode extension market
> 4. Open a JavaScript file with VSCode, set host on the menu
> 5. Now you can write code, and sync it to your iPhone automatically
> You can share a js file with AirDrop to JSBox, but you need to import those files manually. > For example you can do that with this script: https://github.com/cyanzhong/xTeko/blob/master/extension-demos/sync-inbox.js
> Here's an example: https://github.com/cyanzhong/xTeko/blob/master/extension-demos/addin-gallery.js
In order to provide more demo scripts, we created a open source project on GitHub: https://github.com/cyanzhong/xTeko
Welcome to improve this project together, you can contribute by New issue or New pull request.
Thank you!
If you have any question or suggestion about JSBox, you can find us by:
I'm ready, let's start >
This document is still under construction, please note changes in the future
Quick Start
Foundation
Built-in Functions
Data Structure
Build UI
Components
Safari Extension
Home Screen Widget (iOS 14)
Today Widget (Deprecated)
Action Extension
Keyboard Extension
File System
SQLite
Addin Related
Media
Native SDK
Networking
Extended APIs
Shortcuts
SSH
Object Properties
Promise
Package
Runtime
Debug
About
JSBox is an IDE for learning JavaScript on iOS, it aims to provide a safe sandbox for JavaScript executing, for educational purpose only.
This privacy policy applies to the iOS main app itself, its embedded frameworks and app extensions, and official examples we provided, but obviously not to scripts that you, the user, write yourself. Especially when using networking APIs, please be aware that other terms (of the API service provider) may apply.
JSBox is not designed to be a "platform" that allows you run arbitrary code, but rather as a learning environment for your own programs. In order to prevent malicious scripts, you are strongly encouraged to always check the code before running.
Data Collected by JSBox
In short, the JSBox app itself does not collect any data from you. Yes, we don't do any data tracking technology for "Growth Hacking" purpose, we don't even count how many users we have. We just want to provide a safe programming environment, and do not care how "success" we are, or how the app is being used by users.
But you should be careful about using some APIs that may request your data, such as location, calendar, and reminders. Especially when you are running a script that someone gave you, double-check it before running.
Other than that, please be confident about our service, all secrets stay on your device, we don't have control over it.
Cookies
Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.
JSBox does not use these "cookies" explicitly. However, the app may use third-party code or libraries that may collect cookies to improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device.
Changes to This Privacy Policy
We may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately after they are posted on this page.
Contact Us
If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact us at log.e@qq.com.
We can manage our scripts with $addin APIs
Get all installed addins (scripts):
const addins = $addin.list;
Data Structure:
Prop | Type | Read/Write | Description |
---|---|---|---|
name | string | rw | name |
category | string | rw | category |
url | string | rw | url |
data | $data | rw | file |
id | string | rw | id |
version | string | rw | version number |
icon | string | rw | icon name |
iconImage | image | r | icon image |
module | bool | rw | whether a module |
author | string | rw | author name |
website | string | rw | author website |
Only name
and data
are necessary, other fields are optional.
You can modify the addins
, such as reordering, and save it like this:
$addin.list = addins;
Returns all categories as a string array:
const categories = $addin.categories;
You can modify it, such as reordering or adding new category, and save it like this:
$addin.categories = categories;
Get current running addin:
const current = $addin.current;
Install a new addin:
$addin.save({
name: "New Script",
data: $data({string: "$ui.alert('Hey!')"}),
handler: function(success) {
}
})
JSBox will install a new script named New Script
, the data structure just like we mentioned before.
Delete an installed addin:
$addin.delete("New Script")
A script named New Script
will be deleted.
Run a script with name:
$addin.run("New Script")
A script named New Script
will be started.
Since v1.15.0, you can also put an extra parameter here:
$addin.run({
name: "New Script",
query: {
"a": "b"
}
})
It will be passed to $context.query
.
Restart current running script.
Replay the current running UI script.
Convert scripts to JSBox syntax:
const result = $addin.compile("$ui.alert('Hey')");
// result => JSBox.ui.alert('Hey')
Similar to eval()
, but convert script to JSBox syntax first:
$addin.eval("$ui.alert('Hey')");
In order to understand $addin APIs are very important, we created two examples
For the purpose of safety, we don't provide any solution to install a script by user.
You should understand the meaning of script before you run it.
blur
is designed to add a blur effect:
{
type: "blur",
props: {
style: 1 // 0 ~ 20
},
layout: $layout.fill
}
style
0 ~ 20 stands for different blur styles, reference.
Prop | Type | Read/Write | Description |
---|---|---|---|
style | $blurStyle | w | effect style |
button
is designed to create a button to handle tap action:
{
type: "button",
props: {
title: "Click"
},
layout: function(make, view) {
make.center.equalTo(view.super)
}
}
It shows a Click
button.
Similar to image view, it supports configure the image content with a src
.
In addition, button also supports JSBox icon set, refer.
Prop | Type | Read/Write | Description |
---|---|---|---|
title | string | rw | title |
titleColor | $color | rw | title color |
font | $font | rw | font |
src | string | rw | image url |
source | object | rw | image loading info |
symbol | string | rw | SF symbols id |
image | image | rw | icon image |
icon | $icon | w | builtin icon |
type | $btnType | r | button type |
menu | object | w | Pull-Down menu |
contentEdgeInsets | $insets | rw | content edge insets |
titleEdgeInsets | $insets | rw | title edge insets |
imageEdgeInsets | $insets | rw | image edge insets |
After v1.55.0, the image can be specified with source
for more detailed information:
source: {
url: url,
placeholder: image,
header: {
"key1": "value1",
"key2": "value2",
}
}
To support tap event on buttons, implement tapped
:
events: {
tapped: function(sender) {
}
}
button
supports Pull-Down Menus, refer to Pull-Down Menus for usage.
canvas
provides drawing ability to JSBox, in most cases you could create UI with views, but sometimes you need to draw something:
{
type: "canvas",
layout: $layout.fill,
events: {
draw: function(view, ctx) {
var centerX = view.frame.width * 0.5
var centerY = view.frame.height * 0.3
var radius = 50.0
ctx.fillColor = $color("red")
ctx.moveToPoint(centerX, centerY - radius)
for (var i=1; i<5; ++i) {
var x = radius * Math.sin(i * Math.PI * 0.8)
var y = radius * Math.cos(i * Math.PI * 0.8)
ctx.addLineToPoint(x + centerX, centerY - y)
}
ctx.fillPath()
}
}
}
It creates a red pentagram, the radius is 50pt.
canvas
is very complicated, and is still under construction, please refer CoreGraphics to learn more.
ctx
means a context in drawing operations, to understand that please read Apple Developer Docs first.
Here are some interfaces we implemented now.
fillColor
is the color to fill a rect.
strokeColor
is the color to draw a line.
font
is the font to draw texts.
fontSize
is the font size when draw texts.
allowsAntialiasing
allows antialiasing.
Save current state.
Restore state.
Scale CTM.
Translate CTM.
Rotate CTM.
Set the line width of context.
Set the line cap of context: https://developer.apple.com/documentation/coregraphics/cglinecap
Set the line join of context: https://developer.apple.com/documentation/coregraphics/cglinejoin
Set the miter limit of context: https://developer.apple.com/documentation/coregraphics/cgcontext/1456499-setmiterlimit
Set the alpha value of context.
Begin a path.
Move a point to (x, y).
Add a line to point (x, y).
Add a curve to point (x, y), the curvature is controlled by (cp1x, cp1y)
and (cp2x, cp2y)
.
Add a curve to point (x, y), the curvature is controlled by (cpx, cpy)
.
Close the path.
Add a rect.
Add an arc, centered at (x, y)
, starts from startAngle
, ends with endAngle
.
Add an arch between (x1, y1)
and (x2, y2)
.
Fill the rect.
Stroke the rect.
Clear the rect.
Fille the path.
Stroke the path.
Draw the context's path using drawing mode:
0: kCGPathFill,
1: kCGPathEOFill,
2: kCGPathStroke,
3: kCGPathFillStroke,
4: kCGPathEOFillStroke,
Reference: https://developer.apple.com/documentation/coregraphics/1455195-cgcontextdrawpath
Draw an image to rect.
Draw a text to rect:
ctx.drawText($rect(0, 0, 100, 100), "Hey!", {
color: $color("red"),
font: $font(30)
});
chart
displays chart for data visualization:
{
type: "chart",
props: {
options: {
"legend": {
"data": ["Chart"]
},
"xAxis": {
"data": [
"A",
"B",
"C",
"D"
]
},
"yAxis": {},
"series": [
{
"name": "foo",
"type": "bar",
"data": [5, 20, 36, 10]
}
]
}
},
layout: $layout.fill
}
It shows you a columnar chart, options are exactly the same as echarts.
Sometimes we need to generate the data dynamically, it can be done with JavaScript functions, in that case you need to render the chart with template string:
$ui.render({
views: [
{
type: "chart",
layout: $layout.fill,
events: {
ready: chart => {
let options = `
options = {
tooltip: {},
backgroundColor: "#fff",
visualMap: {
show: false,
dimension: 2,
min: -1,
max: 1,
inRange: {
color: [
"#313695",
"#4575b4",
"#74add1",
"#abd9e9",
"#e0f3f8",
"#ffffbf",
"#fee090",
"#fdae61",
"#f46d43",
"#d73027",
"#a50026"
]
}
},
xAxis3D: {
type: "value"
},
yAxis3D: {
type: "value"
},
zAxis3D: {
type: "value"
},
grid3D: {
viewControl: {
// projection: 'orthographic'
}
},
series: [
{
type: "surface",
wireframe: {
// show: false
},
equation: {
x: {
step: 0.05
},
y: {
step: 0.05
},
z: function(x, y) {
if (Math.abs(x) < 0.1 && Math.abs(y) < 0.1) {
return "-";
}
return Math.sin(x * Math.PI) * Math.sin(y * Math.PI);
}
}
}
]
};`;
chart.render(options);
}
}
}
]
});
Define the options with options =
, you can use functions inside the string.
Trigger actions:
chart.dispatchAction({
type: "dataZoom",
start: 20,
end: 30
});
Get width of the chart:
let width = await chart.getWidth();
Get height of the chart:
let height = await chart.getHeight();
Get options of the chart:
let options = await chart.getOption();
Resize the chart:
chart.resize($size(100, 100));
Shows loading animation:
chart.showLoading();
Hides loading animation:
chart.hideLoading();
Clear current chart:
chart.clear();
rendered
will be called after rendered:
events: {
rendered: () => {
}
}
finished
will be called when finish:
events: {
finished: () => {
}
}
Chart view is an ECharts wrapper that uses WebView, so you can use all features in WebView, such as JavaScript injection and $notify, please refer to WebView for details.
More examples: https://github.com/cyanzhong/xTeko/tree/master/extension-demos/charts
Text view that provides syntax highlighting, and code editing, supports many commonly used languages and themes:
$ui.render({
views: [
{
type: "code",
props: {
text: "const value = 100"
},
layout: $layout.fill
}
]
});
You can control its behavior with parameters like below:
Prop | Type | Default | Description |
---|---|---|---|
language | string | javascript | programming language |
theme | string | nord | editor theme |
darkKeyboard | bool | true | uses dark mode |
adjustInsets | bool | true | adjust insets automatically |
lineNumbers | bool | false | shows line numbers |
invisibles | bool | false | show invisible characters |
linePadding | number | null | line padding |
keys | [string] | null | toolbar keys |
Note that, those parameters have to be defined when creating a code view, cannot be overridden later.
Besides, code view inherits from text view, it behaves exactly the same as type: text
.
For instance, disable editing with editable: false
. Or, control its content with the text
property:
const view = $("codeView");
const code = view.text;
view.text = "console.log([1, 2, 3])";
Of course, all events are supported:
$ui.render({
views: [
{
type: "code",
props: {
text: "const value = 100"
},
layout: $layout.fill,
events: {
changed: sender => {
console.log("code changed");
}
}
}
]
});
code
view is based on highlightjs, you can find all supported languages here: https://github.com/highlightjs/highlight.js/tree/master/src/languages
For example, syntax highlighting for Python:
props: {
language: "python"
}
code
view is based on highlightjs, you can find all supported themes here: https://github.com/highlightjs/highlight.js/tree/master/src/styles
For example, use the atom-one-light
theme:
props: {
theme: "atom-one-light"
}
In many cases, the keyboard is annoying because it hides the editing area. code
view handles that for you, by default. It observes the keyboard height, and adjust the content insets automatically.
However, it's hard to make it perfect, you can disable it with adjustInsets: false
.
By default, there will be an editing toolbar, here is how to customize:
$ui.render({
views: [
{
type: "code",
props: {
language: "markdown",
keys: [
"#",
"-",
"*",
"`",
//...
]
},
layout: $layout.fill
}
]
});
If you want to remove the toolbar, you can override accessoryView
.
date-picker
is designed to display a date picker:
{
type: "date-picker",
layout: function(make) {
make.left.top.right.equalTo(0)
}
}
Create a simple date picker.
Prop | Type | Read/Write | Description |
---|---|---|---|
date | object | rw | current date |
min | object | rw | minimum date |
max | object | rw | maximum date |
mode | number | rw | Refer |
interval | number | rw | step (in minute) |
changed
will be called if the date has changed:
changed: function(sender) {
}
gallery
is used to display a series of views, it can be scrolled horizontally:
{
type: "gallery",
props: {
items: [
{
type: "image",
props: {
src: "https://images.apple.com/v/iphone/home/v/images/home/limited_edition/iphone_7_product_red_large_2x.jpg"
}
},
{
type: "image",
props: {
src: "https://images.apple.com/v/iphone/home/v/images/home/airpods_large_2x.jpg"
}
},
{
type: "image",
props: {
src: "https://images.apple.com/v/iphone/home/v/images/home/apple_pay_large_2x.jpg"
}
}
],
interval: 3,
radius: 5.0
},
layout: function(make, view) {
make.left.right.inset(10)
make.centerY.equalTo(view.super)
make.height.equalTo(320)
}
}
Create a gallery with 3 images.
Prop | Type | Read/Write | Description |
---|---|---|---|
items | object | w | all items |
page | number | rw | current page index |
interval | number | rw | autoplay interval, 0 means off |
pageControl | $view | r | page control component |
changed will be called when page changes:
changed: function(sender) {
}
You can retrieve subviews with methods as below:
const views = $("gallery").itemViews; // All views
const view = $("gallery").viewWithIndex(0); // The first view
If you want to scroll to a page with animation, do this:
$("gallery").scrollToPage(index);
Create a gradient layer:
$ui.render({
props: {
bgcolor: $color("white")
},
views: [
{
type: "gradient",
props: {
colors: [$color("red"), $color("clear"), $color("blue")],
locations: [0.0, 0.5, 1.0],
startPoint: $point(0, 0),
endPoint: $point(1, 1)
},
layout: function(make, view) {
make.left.top.equalTo(0)
make.size.equalTo($size(100, 100))
}
}
]
})
The design is exactly same as CAGradientLayer, for detail please refer to: https://developer.apple.com/documentation/quartzcore/cagradientlayer
Prop | Type | Read/Write | Description |
---|---|---|---|
colors | array | rw | colors |
locations | array | rw | locations |
startPoint | $point | rw | start point |
endPoint | $point | rw | end point |
image
is used to display an image, the resource could be local or remote.
{
type: "image",
props: {
src: "https://images.apple.com/v/ios/what-is/b/images/performance_large.jpg"
},
layout: function(make, view) {
make.center.equalTo(view.super)
make.size.equalTo($size(100, 100))
}
}
Load an image using remote resource, the size is 100*100.
scr could be a base64 data starts with data:image
:
{
type: "image",
props: {
src: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAASCAMAAAB7LJ7rAAAANlBMVEUAAABnZ2dmZmZmZmZnZ2dmZmZmZmZmZmZnZ2dnZ2dnZ2dmZmZoaGhnZ2dnZ2dubm5paWlmZmbvpwLOAAAAEXRSTlMA9h6lQ95r4cmLdHNbTzksJ9o8+Y0AAABcSURBVCjPhc1JDoAwFAJQWus8cv/LqkkjMXwjCxa8BfjLWuI9L/nqhmwiLYnpAMjqpuQMDI+bcgNyW921A+Sxyl3NXeWu7lL3WOXS0Ck1N3WXut/HEz6z92l8Lyf1mAh1wPbVFAAAAABJRU5ErkJggg=="
},
layout: function(make, view) {
make.center.equalTo(view.super)
make.size.equalTo($size(100, 100))
}
}
In addition, src supports url starts with share://
and drive://
to load files in JSBox.
Finally, JSBox icon set is also supported by image, refer.
Prop | Type | Read/Write | Description |
---|---|---|---|
src | string | w | source url |
source | object | rw | image loading info |
symbol | string | rw | SF symbols id |
data | $data | w | binary file |
size | $size | r | image size |
orientation | number | r | image orientation |
info | object | r | information, metadata |
scale | number | r | image scale |
After v1.55.0, the image can be specified with source
for more detailed information:
source: {
url: url,
placeholder: image,
header: {
"key1": "value1",
"key2": "value2",
}
}
Since v1.56.0
, you can create zoomable images easily:
$ui.render({
views: [
{
type: "scroll",
props: {
zoomEnabled: true,
maxZoomScale: 3, // Optional, default is 2,
doubleTapToZoom: false // Optional, default is true
},
layout: $layout.fill,
views: [
{
type: "image",
props: {
src: "https://..."
},
layout: $layout.fill
}
]
}
]
});
All you need to do is just wrapping the image view with a scroll
view, and set it as zoomEnabled
.
Returns a new image with the template
rendering image, it can be used with tintColor
to change the image color:
{
type: "image",
props: {
tintColor: $color("red"),
image: rawImage.alwaysTemplate
}
}
The ahove rawImage
is the original image you have.
It's similar to alwaysTemplate
, but it returns an image with the original
rendering mode, tintColor
will be ignored.
Get a resized image:
const resizedImage = image.resized($size(60, 60));
Get a resizable image:
const resizableImage = image.resizableImage($insets(10, 10, 10, 10));
You can specify the fill mode as tile
(default to stretch):
const resizableImage = image.resizableImage({
insets: $insets(10, 10, 10, 10),
mode: "tile"
});
input
is used to create a text field, to receive text from users:
{
type: "input",
props: {
type: $kbType.search,
darkKeyboard: true,
},
layout: function(make, view) {
make.center.equalTo(view.super)
make.size.equalTo($size(100, 32))
}
}
This code shows a text field on screen, the keyboard will be dark mode.
Prop | Type | Read/Write | Description |
---|---|---|---|
type | $kbType | rw | type |
darkKeyboard | boolean | rw | dark mode |
text | string | rw | text content |
styledText | object | rw | styled text, refer |
textColor | $color | rw | text color |
font | $font | rw | font |
align | $align | rw | alignment |
placeholder | string | rw | placeholder |
clearsOnBeginEditing | boolean | rw | clears text on begin |
autoFontSize | boolean | rw | adjust font size automatically |
editing | boolean | r | is editing |
secure | boolean | rw | is password |
Get the focus, show keyboard.
Blur the focus, dismiss keyboard.
changed
will be called when text changed:
changed: function(sender) {
}
returned
will be called once the return key pressed:
returned: function(sender) {
}
didBeginEditing
will be called after editing began:
didBeginEditing: function(sender) {
}
didEndEditing
will be called after editing ended:
didEndEditing: function(sender) {
}
You can customize toolbar as below:
$ui.render({
views: [
{
type: "input",
props: {
accessoryView: {
type: "view",
props: {
height: 44
},
views: [
]
}
}
}
]
});
You can customize keyboard as below:
$ui.render({
views: [
{
type: "input",
props: {
keyboardView: {
type: "view",
props: {
height: 267
},
views: [
]
}
}
}
]
});
Instead of create a text field, you could also use $input.text
:
$input.text({
type: $kbType.number,
placeholder: "Input a number",
handler: function(text) {
}
})
Speech to text:
$input.speech({
locale: "en-US", // Optional
autoFinish: false, // Optional
handler: function(text) {
}
})
It shows a popup text field to user directly, it's much easier.
label
is designed to display text:
{
type: "label",
props: {
text: "Hello, World!",
align: $align.center
},
layout: function(make, view) {
make.center.equalTo(view.super)
}
}
It shows Hello, World!
on the screen.
Prop | Type | Read/Write | Description |
---|---|---|---|
text | string | rw | text content |
styledText | object | rw | styled text, refer |
font | $font | rw | font |
textColor | $color | rw | text color |
shadowColor | $color | rw | shadow color |
align | $align | rw | alignment |
lines | number | rw | lines, 0 means no limit |
autoFontSize | boolean | rw | adjust font size automatically |
list
is the most complicated control in JSBox, it used to arrange a series of views:
{
type: "list",
props: {
data: ["JavaScript", "Swift"]
},
layout: $layout.fill,
}
It creates a list with only one section, each row inside is a label.
If you want to create a list with multiple sections, you need to:
{
type: "list",
props: {
data: [
{
title: "Section 0",
rows: ["0-0", "0-1", "0-2"]
},
{
title: "Section 1",
rows: ["1-0", "1-1", "1-2"]
}
]
},
layout: $layout.fill,
}
It creates 2 sections, each section has 3 rows, choose what you want.
Above samples are plain text list, actually list cells could be customized, we call it template
:
template: {
props: {
bgcolor: $color("clear")
},
views: [
{
type: "label",
props: {
id: "label",
bgcolor: $color("#474b51"),
textColor: $color("#abb2bf"),
align: $align.center,
font: $font(32)
},
layout: $layout.fill
}
]
}
Views in template is just a common view like you know in $ui.render
.
Now you could construct your data
as:
data: [
{
label: {
text: "Hello"
}
},
{
label: {
text: "World"
}
}
]
While rendering a row, JSBox searches each view by id
, then configures all properties.
With template
and data
, we almost can implement any style if we want, and don't forget, we also can use multiple sections with templates.
header
and footer
are views, they are attached at the top/bottom of a list:
footer: {
type: "label",
props: {
height: 20,
text: "Write the Code. Change the world.",
textColor: $color("#AAAAAA"),
align: $align.center,
font: $font(12)
}
}
Note: please specify the height
manually in props.
Cells in template are reused by iOS automatically, sometimes you want to static data to render them.
JSBox could do that for you, you just need to put your view in data
:
data: [
{
title: "System (Text)",
rows: [
{
type: "button",
props: {
title: "Button",
selectable: false
},
layout: function(make, view) {
make.center.equalTo(view.super)
make.width.equalTo(64)
},
events: {
tapped: function(sender) {
$ui.toast("Tapped")
}
}
}
]
},
{
title: "System (Contact Add)",
rows: [
{
type: "button",
props: {
type: 5
},
layout: $layout.center
}
]
}
]
It creates some static cells, instead of reuse them dynamically.
The selectable
property indicates whether the cell is selectable, default to false.
Prop | Type | Read/Write | Description |
---|---|---|---|
style | number | w | style 0 ~ 2 |
data | object | rw | data source |
separatorInset | $insets | rw | separator inset |
separatorHidden | boolean | rw | hide separator |
separatorColor | $color | rw | separator color |
header | object | rw | header view |
footer | object | rw | footer view |
rowHeight | number | w | row height |
autoRowHeight | boolean | w | whether auto sizing |
estimatedRowHeight | number | w | estimated row height |
sectionTitleHeight | number | w | section title height |
selectable | bool | w | is row selectable |
stickyHeader | boolean | w | section/header are sticky |
reorder | boolean | rw | whether can be reordered |
crossSections | boolean | rw | whether recorder can cross sections |
hasActiveAction | boolean | r | whether an action is being used |
actions
is designed to provide swipe actions:
actions: [
{
title: "delete",
color: $color("gray"), // default to gray
handler: function(sender, indexPath) {
}
},
{
title: "share",
handler: function(sender, indexPath) {
}
}
]
It creates 2 swipe actions, named delete
and share
.
Use swipeEnabled
to decide whether users can swipe a row:
swipeEnabled: function(sender, indexPath) {
return indexPath.row > 0;
}
That means the first row can't be swiped.
Returns the row data at indexPath:
const data = tableView.object($indexPath(0, 0));
Insert new data at indexPath or index:
// Either indexPath or index, the value shoule be a row data
tableView.insert({
indexPath: $indexPath(0, 0),
value: "Hey!"
})
Delete a row at indexPath or index:
// object could be indexPath or index
tableView.delete(0)
Returns the cell at indexPath (might be null):
const cell = tableView.cell($indexPath(0, 0));
We could specify row height dynamically by making the rowHeight
a function:
rowHeight: function(sender, indexPath) {
if (indexPath.row == 0) {
return 88.0
} else {
return 44.0
}
}
We could specify section title height dynamically by making the sectionTitleHeight
a function:
sectionTitleHeight: (sender, section) => {
if (section == 0) {
return 30;
} else {
return 40;
}
}
didSelect
will be called once a row selected:
didSelect: function(sender, indexPath, data) {
}
didLongPress
will be called when cell is long pressed:
didLongPress: function(sender, indexPath, data) {
}
Iterate all items:
forEachItem: function(view, indexPath) {
}
In many cases, we want to calculate row height manually. For example, we want to display sentences that have different length.
Starting from v2.5.0, list view supports auto sizing, just set autoRowHeight
and estimatedRowHeight
, then provide proper layout constraints:
const sentences = [
"Although moreover mistaken kindness me feelings do be marianne.",
"Effects present letters inquiry no an removed or friends. Desire behind latter me though in. Supposing shameless am he engrossed up additions. My possible peculiar together to. Desire so better am cannot he up before points. Remember mistaken opinions it pleasure of debating. Court front maids forty if aware their at. Chicken use are pressed removed.",
"He went such dare good mr fact. The small own seven saved man age no offer. Suspicion did mrs nor furniture smallness. Scale whole downs often leave not eat. An expression reasonably cultivated indulgence mr he surrounded instrument. Gentleman eat and consisted are pronounce distrusts.",
];
$ui.render({
views: [
{
type: "list",
props: {
autoRowHeight: true,
estimatedRowHeight: 44,
template: [
{
type: "image",
props: {
id: "icon",
symbol: "text.bubble"
},
layout: (make, view) => {
make.left.equalTo(10);
make.size.equalTo($size(24, 24));
make.centerY.equalTo(view.super);
}
},
{
type: "label",
props: {
id: "label",
lines: 0
},
layout: (make, view) => {
const insets = $insets(10, 44, 10, 10);
make.edges.equalTo(view.super).insets(insets);
}
}
],
data: sentences.map(text => {
return {
"label": {text}
}
})
},
layout: $layout.fill
}
]
});
For simple list that shows strings, also supports auto-sizing:
const sentences = [
"Although moreover mistaken kindness me feelings do be marianne.",
"Effects present letters inquiry no an removed or friends. Desire behind latter me though in. Supposing shameless am he engrossed up additions. My possible peculiar together to. Desire so better am cannot he up before points. Remember mistaken opinions it pleasure of debating. Court front maids forty if aware their at. Chicken use are pressed removed.",
"He went such dare good mr fact. The small own seven saved man age no offer. Suspicion did mrs nor furniture smallness. Scale whole downs often leave not eat. An expression reasonably cultivated indulgence mr he surrounded instrument. Gentleman eat and consisted are pronounce distrusts.",
];
$ui.render({
views: [
{
type: "list",
props: {
autoRowHeight: true,
data: sentences
},
layout: $layout.fill
}
]
});
List view supports long press to reorder items, we need to turn on this feature:
props: {
reorder: true
}
In addition, we need to implement several methods:
Reorder action began:
reorderBegan: function(indexPath) {
}
User moved a row:
reorderMoved: function(fromIndexPath, toIndexPath) {
// Reorder your data source here
}
User finished reordering:
reorderFinished: function(data) {
// Save your data source here
}
Decide whether it can be reordered by:
canMoveItem: function(sender, indexPath) {
return indexPath.row > 0;
}
It disables the first item's long press.
In short, we need to update data in reorderMoved
and reorderFinished
.
More example: https://github.com/cyanzhong/xTeko/blob/3ac0d3f5ac552a1c72ea39d0bd1099fd02f5ca70/extension-scripts/xteko-menu.js
Let's see an example first:
let data = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"];
$ui.render({
views: [
{
type: "list",
props: { data },
layout: $layout.fill,
events: {
didReachBottom: function(sender) {
$ui.toast("fetching...")
$delay(1.5, () => {
sender.endFetchingMore()
data = data.concat(data.map(item => {
return (parseInt(item) + 15).toString()
}))
$("list").data = data
})
}
}
}
]
})
When list view reaches bottom, it triggers didReachBottom
method, we load more data here.
When load more finished, we call endFetchingMore
first to terminate the loading state, and update data source.
Finally, we setup the data back to the list view.
Set editing state programmatically:
$("list").setEditing(false)
Scroll to a specific indexPath programmatically:
$("list").scrollTo({
indexPath: $indexPath(0, 0),
animated: true // Default is true
})
List views are subclasses of scroll view, they have all abilities inherited from scroll view.
lottie
creates a Lottie view:
{
type: "lottie",
props: {
src: "assets/lottie-btn.json",
},
layout: (make, view) => {
make.size.equalTo($size(100, 100));
make.center.equalTo(view.super);
}
}
src
could be a bundle path, or a http url. Also, you can load lottie with json
or data
:
// JSON
$("lottie").json = {};
// Data
$("lottie").data = $file.read("assets/lottie-btn.json");
Play the animation:
$("lottie").play();
Register completion callback:
$("lottie").play({
handler: finished => {
}
});
Specify from frame and to frame:
$("lottie").play({
fromFrame: 0, // Optional
toFrame: 0,
handler: finished => {
// Optional
}
});
Specify from progress and to progress:
$("lottie").play({
fromProgress: 0, // Optional
toProgress: 0.5,
handler: finished => {
// Optional
}
});
Work with promise:
let finished = await $("lottie").play({ toProgress: 0.5, async: true });
Pause the animation:
$("lottie").pause();
Stop the animation:
$("lottie").stop();
Force update current frame:
$("lottie").update();
Convert frame to progress:
let progress = $("lottie").progressForFrame(0);
Convert progress to frame:
let frame = $("lottie").frameForProgress(0.5);
Prop | Type | Read/Write | Description |
---|---|---|---|
json | json | w | load with json |
data | $data | w | load with data |
src | string | w | load with src |
playing | bool | r | is playing |
loop | bool | rw | is loop animation |
autoReverse | bool | rw | is auto-reverse animation |
progress | number | rw | animation progress |
frameIndex | number | rw | current frame |
speed | number | rw | animation speed |
duration | number | r | animation duration |
Learn more from here: https://github.com/cyanzhong/xTeko/tree/master/extension-demos/lottie-example
map
is designed to display a map view:
{
type: "map",
props: {
location: {
lat: 31.2990,
lng: 120.5853
}
},
layout: $layout.fill
}
It shows a map and locates at Suzhou, China.
Prop | Type | Read/Write | Description |
---|---|---|---|
location | object | w | user location |
We are still working on it, we might introduce new features like navigation in the future.
Render markdown contents, it's good way to display rich text:
$ui.render({
views: [
{
type: "markdown",
props: {
content: "## Hello, *World!*",
style: // optional, custom style sheet
`
body {
background: #f0f0f0;
}
`
},
layout: $layout.fill
}
]
})
Prop | Type | Read/Write | Description |
---|---|---|---|
webView | $view | r | webView |
content | string | rw | content |
scrollEnabled | bool | rw | is scroll enabled |
style | string | rw | custom style sheet |
matrix
is designed to show a grid view:
{
type: "matrix",
props: {
columns: 4,
itemHeight: 88,
spacing: 5,
template: {
props: {},
views: [
{
type: "label",
props: {
id: "label",
bgcolor: $color("#474b51"),
textColor: $color("#abb2bf"),
align: $align.center,
font: $font(32)
},
layout: $layout.fill
}
]
}
},
layout: $layout.fill
}
Why we call it matrix
instead of grid
or collection
?
Because collection
looks so long and grid
looks not cool.
The most important is, The Matrix
is one of my favourite movie!
You can consider matrix is a special list with multiple columns anyway.
Prop | Type | Read/Write | Description |
---|---|---|---|
data | object | rw | data source |
spacing | number | w | spacing between items |
itemSize | $size | w | item size of each item |
autoItemSize | boolean | w | whether auto sizing |
estimatedItemSize | $size | w | estimated item size |
columns | number | w | column numbers |
square | boolean | w | is square |
direction | $scrollDirection | w | .vertical: vertically .horizontal: horizontally |
selectable | boolean | rw | is selectable |
waterfall | boolean | w | whether a waterfall layout (Pinterest-like) |
Please choose constraints wisely to implement item size, columns
must be provided for waterfall
layout, layout issues can lead to app crash.
header
and footer
are views, they are attached at the top/bottom of a matrix:
footer: {
type: "label",
props: {
height: 20,
text: "Write the Code. Change the world.",
textColor: $color("#AAAAAA"),
align: $align.center,
font: $font(12)
}
}
For fixed height, please specify the height
manually in props
. For dynamic height, provide a height
function in its events
:
footer: {
type: "label",
props: {
text: "Write the Code. Change the world."
},
events: {
height: sender => {
return _height;
}
}
}
When you want to change the height, update the _height
value in the above example, then call matrix.reload()
to trigger the update. For horizontal views, use width
instead of height
to specify its width.
Since headers and footers on iOS are reusable, it's sometimes hard to update data dynamically. If you'd like to update your header or footer at runtime, you can use lazy rendering like this:
footer: sender => {
return {
type: "view",
props: {}
}
}
In short, iOS calls this function at runtime to re-generate a heade or footer.
Returns the item data at indexPath:
const data = matrix.object($indexPath(0, 0));
Insert new data to matrix:
// Either indexPath or index is fine
matrix.insert({
indexPath: $indexPath(0, 0),
value: {
}
})
Delete a item at indexPath or index:
// object could be either indexPath or index
matrix.delete($indexPath(0, 0))
Returns the cell at indexPath:
const cell = matrix.cell($indexPath(0, 0));
didSelect
will be called once item selected:
didSelect: function(sender, indexPath, data) {
}
didLongPress
will be called when cell is long pressed:
didLongPress: function(sender, indexPath, data) {
}
Of course, matrix is a subclass of scroll view, same as list.
Iterate all items:
forEachItem: function(view, indexPath) {
}
Customize highlighted visual:
highlighted: function(view) {
}
Provide dynamic item size:
itemSize: function(sender, indexPath) {
var index = indexPath.item + 1;
return $size(40 * index, 40 * index);
}
Scroll to a specific indexPath programmatically:
$("matrix").scrollTo({
indexPath: $indexPath(0, 0),
animated: true // Default to true
})
Starting from v2.5.0, matrix view supports auto sizing, just set autoItemSize
and estimatedItemSize
, then provide proper layout constraints:
const sentences = [
"Although moreover mistaken kindness me feelings do be marianne.",
"Effects present letters inquiry no an removed or friends. Desire behind latter me though in.",
"He went such dare good mr fact.",
];
$ui.render({
views: [
{
type: "matrix",
props: {
autoItemSize: true,
estimatedItemSize: $size(120, 0),
spacing: 10,
template: {
props: {
bgcolor: $color("#F0F0F0")
},
views: [
{
type: "image",
props: {
symbol: "sun.dust"
},
layout: (make, view) => {
make.centerX.equalTo(view.super);
make.size.equalTo($size(24, 24));
make.top.equalTo(10);
}
},
{
type: "label",
props: {
id: "label",
lines: 0
},
layout: (make, view) => {
make.left.bottom.inset(10);
make.top.equalTo(44);
make.width.equalTo(100);
}
}
]
},
data: sentences.map(text => {
return {
"label": {text}
}
})
},
layout: $layout.fill
}
]
});
Matrix view supports long press to reorder items, we need to turn on this feature:
props: {
reorder: true
}
In addition, we need to implement several methods:
Reorder action began:
reorderBegan: function(indexPath) {
}
User moved a row:
reorderMoved: function(fromIndexPath, toIndexPath) {
// Reorder your data source here
}
User finished reordering:
reorderFinished: function(data) {
// Save your data source here
}
Decide whether it can be reordered by:
canMoveItem: function(sender, indexPath) {
return indexPath.row > 0;
}
It disables the first item's long press.
In short, we need to update data in reorderMoved
and reorderFinished
.
JSBox provides two menu types, use tab
if you only have few items, use menu
to display a lot of items:
$ui.render({
views: [
{
type: "menu",
props: {
items: ["item 1", "item 2", "item 3", "item 4", "item 5", "item 6", "item 7", "item 8", "item 9"],
dynamicWidth: true, // dynamic item width, default is false
},
layout: function(make) {
make.left.top.right.equalTo(0)
make.height.equalTo(44)
},
events: {
changed: function(sender) {
const items = sender.items;
const index = sender.index;
$ui.toast(`${index}: ${items[index]}`)
}
}
}
]
})
It creates a menu, and shows a toast if the selected item has changed.
sender.items
represents all items, sender.index
means selected index (starts from 0).
index
could be used to set the initial value as well:
props: {
index: 1
}
Select the second item by default.
picker
is used to display a general picker:
{
type: "picker",
props: {
items: Array(3).fill(Array.from(Array(256).keys()))
},
layout: function(make) {
make.left.top.right.equalTo(0)
}
}
It shows a rgb wheel, each part of them is independent.
Besides, you could also create a cascade
picker, each colum is related tightly:
items: [
{
title: "Language",
items: [
{
title: "Web",
items: [
{
title: "JavaScript"
},
{
title: "PHP"
}
]
},
{
title: "Client",
items: [
{
title: "Swift"
},
{
title: "Objective-C"
}
]
}
]
},
{
title: "Framework",
// ...
}
]
This is a recursive structure, when user select title
, the next wheel will update its rows.
Prop | Type | Read/Write | Description |
---|---|---|---|
data | object | r | all selected items |
selectedRows | object | r | all selected rows |
changed
will be called once the selected item changed:
changed: function(sender) {
}
We provided an easy way to popup a date picker with $picker.date(object)
.
We also provided an easy way to popup a general picker with $picker.data(object)
.
The parameter of these 2 methods are exactly same as we mentioned before.
Select a color using the built-in color picker:
const color = await $picker.color({
// color: aColor
});
progress
is used to create a progress bar, for example show download progress:
{
type: "progress",
props: {
value: 0.5
},
layout: function(make, view) {
make.center.equalTo(view.super)
make.width.equalTo(100)
}
}
Create a progress bar, default value is 50%.
Prop | Type | Read/Write | Description |
---|---|---|---|
value | number | rw | current value (0.0 ~ 1.0) |
progressColor | $color | rw | color of the left part |
trackColor | $color | rw | color of the right part |
Initiate a view with Runtime APIs:
const label = $objc("UILabel").$new();
label.$setText("Hey!");
$ui.render({
views: [
{
type: "runtime",
props: {
view: label
},
layout: function(make, view) {
make.center.equalTo(view.super);
}
}
]
});
With this solution, you can mix native views and runtime views.
scroll
is used to create a scrollable view container:
{
type: "scroll",
layout: function(make, view) {
make.center.equalTo(view.super)
make.size.equalTo($size(100, 500))
},
views: [
{
},
{
}
]
}
This container can be scrolled, but we should specify its content size first.
The content size means the size of scroll area, it might be larger than its frame.
We have two ways to set scroll views' content size.
The first one, provide size for all subviews that inside a scroll view:
$ui.render({
views: [{
type: "scroll",
layout: $layout.fill,
views: [{
type: "label",
props: {
text,
lines: 0
},
layout: function(make, view) {
make.width.equalTo(view.super)
make.top.left.inset(0)
make.height.equalTo(1000)
}
}]
}]
})
Or, we could just set the contentSize
of a scroll view manually:
props: {
contentSize: $size(0, 1000)
}
Specify content size of a scroll view is very important, otherwise it works weird.
Prop | Type | Read/Write | Description |
---|---|---|---|
contentOffset | $point | rw | content offset |
contentSize | $size | rw | content size |
alwaysBounceVertical | boolean | rw | always bounce vertical |
alwaysBounceHorizontal | boolean | rw | always bounce horizontal |
pagingEnabled | boolean | rw | is paging enabled |
scrollEnabled | boolean | rw | is scroll enabled |
showsHorizontalIndicator | boolean | rw | shows horizontal indicator |
showsVerticalIndicator | boolean | rw | shows vertical indicator |
contentInset | $insets | rw | content insets |
indicatorInsets | $insets | rw | indicator insets |
tracking | boolean | r | is tracking |
dragging | boolean | r | is dragging |
decelerating | boolean | r | is decelerating |
keyboardDismissMode | number | rw | keyboard dismiss mode |
zoomEnabled | bool | rw | zoom images with 2-finger pinch |
maxZoomScale | number | rw | max zoom scale for images |
doubleTapToZoom | number | rw | enables double tap to zoom |
Begin the refreshing animation.
End the refreshing animation.
Resize the content size of itself.
Re-calculate the best scale for zoomable views, you may need to call this after screen rotation changes.
Scroll to an offset with animation:
$("scroll").scrollToOffset($point(0, 100));
pulled
will be called once user triggered a refresh:
pulled: function(sender) {
}
didScroll
will be called while scrolling:
didScroll: function(sender) {
}
willBeginDragging
will be called while dragging:
willBeginDragging: function(sender) {
}
willEndDragging
will be called before dragging ends:
willEndDragging: function(sender, velocity, target) {
}
The target
parameter tells us the target location, it can be overridden by returning a $point
:
willEndDragging: function(sender, velocity, target) {
return $point(0, 0);
}
didEndDragging
will be called after dragging ended:
didEndDragging: function(sender, decelerate) {
}
willBeginDecelerating
will be called before decelerating:
willBeginDecelerating: function(sender) {
}
didEndDecelerating
will be called after decelerating ended:
didEndDecelerating: function(sender) {
}
didEndScrollingAnimation
will be called after decelerating ended (program triggered):
didEndScrollingAnimation: function(sender) {
}
didScrollToTop
will be called once it did reach top:
didScrollToTop: function(sender) {
}
It is hard to make Auto Layout work for scrollViews, we suggest using layoutSubviews
to work around some issues:
const contentHeight = 1000;
$ui.render({
views: [
{
type: "scroll",
layout: $layout.fill,
events: {
layoutSubviews: sender => {
$("container").frame = $rect(0, 0, sender.frame.width, contentHeight);
}
},
views: [
{
type: "view",
props: {
id: "container"
},
views: [
{
type: "view",
props: {
bgcolor: $color("red")
},
layout: (make, view) => {
make.left.top.right.equalTo(0);
make.height.equalTo(300);
}
}
]
}
]
}
]
});
In short, instead of using Auto Layout for scrollView's subviews, we can add a container view to the scrollView, and set its frame with layoutSubviews
, then subviews that belong to the container view can work with Auto Layout correctly. Learn more: https://developer.apple.com/library/archive/technotes/tn2154/_index.html
slider
is used to create a slide control:
{
type: "slider",
props: {
value: 0.5,
max: 1.0,
min: 0.0
},
layout: function(make, view) {
make.center.equalTo(view.super)
make.width.equalTo(100)
}
}
It creates a slider with range from 0.0 to 1.0, default is 0.5.
Prop | Type | Read/Write | Description |
---|---|---|---|
value | number | rw | current value |
min | number | rw | minimum value |
max | number | rw | maximum value |
continuous | boolean | rw | is continuous |
minColor | $color | rw | color of left part |
maxColor | $color | rw | color of right part |
thumbColor | $color | rw | color of the knob |
changed
will be called once the value changed:
changed: function(sender) {
}
spinner
is designed to display a loading state:
{
type: "spinner",
props: {
loading: true
},
layout: function(make, view) {
make.center.equalTo(view.super)
}
}
You can switch the state by set on
to true/false.
Prop | Type | Read/Write | Description |
---|---|---|---|
loading | boolean | rw | loading state |
color | $color | rw | color |
style | number | rw | 0 ~ 2 for styles |
Equals to spinner.loading = true
.
Equals to spinner.loading = false
.
Equals to spinner.loading = !spinner.loading
.
A special view that doesn't render anything, it's designed for laying out a collection of views.
Note that, stack view in JSBox is based on UIStackView
, in order to understand how it works, you should take a look at Apple's documentation first: https://developer.apple.com/documentation/uikit/uistackview
Here is a simple demo which creates a stack view:
$ui.render({
views: [
{
type: "stack",
props: {
spacing: 10,
distribution: $stackViewDistribution.fillEqually,
stack: {
views: [
{
type: "label",
props: {
text: "Left",
align: $align.center,
bgcolor: $color("silver")
}
},
{
type: "label",
props: {
text: "Center",
align: $align.center,
bgcolor: $color("aqua")
}
},
{
type: "label",
props: {
text: "Right",
align: $align.center,
bgcolor: $color("lime")
}
}
]
}
},
layout: $layout.fill
}
]
});
If you want to arrange some views in a stack view, you should put them inside the stack
prop, not directly under the views
prop.
Also, you can control the layout with some properties, such as axis
, distribution
, alignment
, and spacing
.
The axis
property determines the stack’s orientation, either vertically or horizontally.
Possible values:
- $stackViewAxis.horizontal
- $stackViewAxis.vertical
The distribution
property determines the layout of the arranged views along the stack’s axis.
Possible values:
- $stackViewDistribution.fill
- $stackViewDistribution.fillEqually
- $stackViewDistribution.fillProportionally
- $stackViewDistribution.equalSpacing
- $stackViewDistribution.equalCentering
The alignment
property determines the layout of the arranged views perpendicular to the stack’s axis.
Possible values:
- $stackViewAlignment.fill
- $stackViewAlignment.leading
- $stackViewAlignment.top
- $stackViewAlignment.firstBaseline
- $stackViewAlignment.center
- $stackViewAlignment.trailing
- $stackViewAlignment.bottom
- $stackViewAlignment.lastBaseline
The spacing
property determines the minimum spacing between arranged views, should be a number.
You can also use the these two constants:
- $stackViewSpacing.useDefault
- $stackViewSpacing.useSystem
The isBaselineRelative
property determines whether the vertical spacing between views is measured from the baselines, should be a boolean value.
The isLayoutMarginsRelative
property determines whether the stack view lays out its arranged views relative to its layout margins, should be a boolean value.
After arranged views initialized, you can change them dynamically like this:
const stackView = $("stackView");
const views = stackView.stack.views;
views[0].hidden = true;
// This hides the first view in the stack
Other than initiate a stack view with stack.views
, you can also append a view dynamically like this:
const stackView = $("stackView");
stackView.stack.add(newView);
// newView can be created with $ui.create
Remove a view from a stack:
const stackView = $("stackView");
stackView.stack.remove(existingView);
// existingView can be retrieved with id
Insert a new view into a stack, with index:
const stackView = $("stackView");
stackView.stack.insert(newView, 2);
While stack view manages spacing automatically, you can specify custom spacing after a view:
const stackView = $("stackView");
stackView.stack.setSpacingAfterView(arrangedView, 20);
// arrangedView must be a view that is contained in the stack
Get custom spacing after a particular view:
const stackView = $("stackView");
const spacing = stackView.stack.spacingAfterView(arrangedView);
// arrangedView must be a view that is contained in the stack
It's hard to understand stack views, since it's a bit more complicated than other view types.
We have built an example that shows you how do those properties work, you can check it out: https://github.com/cyanzhong/xTeko/blob/master/extension-demos/stack-view
stepper
is used to create a control with minus
and plus
buttons:
{
type: "stepper",
props: {
max: 10,
min: 1,
value: 5
},
layout: function(make, view) {
make.centerX.equalTo(view.super)
make.top.equalTo(24)
},
events: {
changed: function(sender) {
}
}
}
Create a stepper with range from 1 to 10, default value is 5.
Usually we combine this control with a label to display its value.
Prop | Type | Read/Write | Description |
---|---|---|---|
value | number | rw | current value |
min | number | rw | minimum value |
max | number | rw | maximum value |
step | number | rw | step |
autorepeat | boolean | rw | responds long press |
continuous | boolean | rw | continuous |
changed
will be called once the value changed:
changed: function(sender) {
}
switch
is used to create an on/off control:
{
type: "switch",
props: {
on: true
},
layout: function(make, view) {
make.center.equalTo(view.super)
}
}
It creates a switch, default is on.
Prop | Type | Read/Write | Description |
---|---|---|---|
on | boolean | rw | on/off state |
onColor | $color | rw | color of on state |
thumbColor | $color | rw | color of off state |
changed
will be called after the state changed:
changed: function(sender) {
}
text
is more complicated than label
, it is editable:
{
type: "text",
props: {
text: "Hello, World!\n\nThis is a demo for Text View in JSBox extension!\n\nCurrently we don't support attributed string in iOS.\n\nYou can try html! Looks pretty cool."
},
layout: $layout.fill
}
Create an editable text view, shows a paragraph of text with multiple lines.
Prop | Type | Read/Write | Description |
---|---|---|---|
type | $kbType | rw | keyboard type |
darkKeyboard | boolean | rw | dark mode |
text | string | rw | text content |
styledText | object | rw | styled text |
html | string | w | html content |
font | $font | rw | font |
textColor | $color | rw | text color |
align | $align | rw | alignment |
placeholder | string | rw | placeholder |
selectedRange | $range | rw | selected range |
editable | boolean | rw | is editable |
selectable | boolean | rw | is selectable |
insets | $insets | rw | edge insets |
Get the focus, show keyboard.
Blur the focus, dismiss keyboard.
didBeginEditing
will be called after editing began:
didBeginEditing: function(sender) {
}
didEndEditing
will be called after editing ended:
didEndEditing: function(sender) {
}
didChange
will be called once text changed:
didChange: function(sender) {
}
didChangeSelection
will be called once selection changed:
didChangeSelection: function(sender) {
}
text
is a subclass of scroll
, it works like a scroll view.
Styled text based on markdown syntax, supports bold, italic and links, styles can be nested:
const text = `**Bold** *Italic* or __Bold__ _Italic_
[Inline Link](https://docs.xteko.com) <https://docs.xteko.com>
_Nested **styles**_`
$ui.render({
views: [
{
type: "text",
props: {
styledText: text
},
layout: $layout.fill
}
]
});
This uses the default font and color, for using custom values:
$ui.render({
views: [
{
type: "text",
props: {
styledText: {
text: "",
font: $font(15),
color: $color("black")
}
},
layout: $layout.fill
}
]
});
For literals like *
, _
, escape them with \\
.
If you want to control formats precisely, you can set styles
for each range:
const text = `
AmericanTypewriter Cochin-Italic
Text Color Background Color
Kern
Strikethrough Underline
Stroke
Link
Baseline Offset
Obliqueness
`;
const _range = keyword => {
return $range(text.indexOf(keyword), keyword.length);
}
$ui.render({
views: [
{
type: "text",
props: {
styledText: {
text,
font: $font(17),
color: $color("black"),
markdown: false, // whether to use markdown syntax
styles: [
{
range: _range("AmericanTypewriter"),
font: $font("AmericanTypewriter", 17)
},
{
range: _range("Cochin-Italic"),
font: $font("Cochin-Italic", 17)
},
{
range: _range("Text Color"),
color: $color("red")
},
{
range: _range("Background Color"),
color: $color("white"),
bgcolor: $color("blue")
},
{
range: _range("Kern"),
kern: 10
},
{
range: _range("Strikethrough"),
strikethroughStyle: 2,
strikethroughColor: $color("red")
},
{
range: _range("Underline"),
underlineStyle: 9,
underlineColor: $color("green")
},
{
range: _range("Stroke"),
strokeWidth: 3,
strokeColor: $color("black")
},
{
range: _range("Link"),
link: "https://xteko.com"
},
{
range: _range("Baseline Offset"),
baselineOffset: 10
},
{
range: _range("Obliqueness"),
obliqueness: 1
}
]
}
},
layout: $layout.fill
}
]
});
Attribute | Type | Description |
---|---|---|
range | $range | text range |
font | $font | font |
color | $color | foreground color |
bgcolor | $color | background color |
kern | number | font kerning |
strikethroughStyle | number | strikethrough style Refer |
strikethroughColor | $color | strikethrough color |
underlineStyle | number | underline style Refer |
underlineColor | $color | underline color |
strokeWidth | number | stroke width |
strokeColor | $color | stroke color |
link | string | link URL |
baselineOffset | number | baseline offset |
obliqueness | number | font obliqueness |
Markdown syntax is disable when styles
is used, it can be turned on by specifying markdown: true
.
For underline and strikethrough, please refer to Apple's documentation, here is an example:
NSUnderlineStyleNone = 0x00,
NSUnderlineStyleSingle = 0x01,
NSUnderlineStyleThick = 0x02,
NSUnderlineStyleDouble = 0x09,
NSUnderlineStylePatternSolid = 0x0000,
NSUnderlineStylePatternDot = 0x0100,
NSUnderlineStylePatternDash = 0x0200,
NSUnderlineStylePatternDashDot = 0x0300,
NSUnderlineStylePatternDashDotDot = 0x0400,
NSUnderlineStyleByWord = 0x8000,
If you want a single line with dots, you can combine NSUnderlineStyleSingle
and NSUnderlineStylePatternDot
:
underlineStyle: 0x01 | 0x0100
You can customize toolbar as below:
$ui.render({
views: [
{
type: "input",
props: {
accessoryView: {
type: "view",
props: {
height: 44
},
views: [
]
}
}
}
]
});
You can customize keyboard as below:
$ui.render({
views: [
{
type: "input",
props: {
keyboardView: {
type: "view",
props: {
height: 267
},
views: [
]
}
}
}
]
});
video
is used to play video:
{
type: "video",
props: {
src: "https://images.apple.com/media/cn/ipad-pro/2017/43c41767_0723_4506_889f_0180acc13482/films/feature/ipad-pro-feature-cn-20170605_1280x720h.mp4",
poster: "https://images.apple.com/v/iphone/home/v/images/home/limited_edition/iphone_7_product_red_large_2x.jpg"
},
layout: function(make, view) {
make.left.right.equalTo(0)
make.centerY.equalTo(view.super)
make.height.equalTo(256)
}
}
Since video component is implemented with WebView internally, you can also load local videos with local://
protocol:
{
type: "video",
props: {
src: "local://assets/video.mp4",
poster: "local://assets/poster.jpg"
},
layout: function(make, view) {
make.left.right.equalTo(0)
make.centerY.equalTo(view.super)
make.height.equalTo(256)
}
}
Besides, there is a demo that uses AVPlayerViewController
for video playing: https://gist.github.com/cyanzhong/c3992af39043c8e0f25424536c379595
Pause the video:
$("video").pause()
Play the video:
$("video").play()
Toggle the state:
$("video").toggle()
We can load the video by set src
, and the the image by poster
.
Video component is now implemented by a web view internally.
We're considering replace the implementation with native controls to have better performance in the future.
web
is used to render a website:
{
type: "web",
props: {
url: "https://www.apple.com"
},
layout: $layout.fill
}
It shows the home page of Apple.
You can also use the request
parameter to specify more information:
{
type: "web",
props: {
request: {
url: "https://www.apple.com",
method: "GET",
header: {},
body: body // $data type
}
},
layout: $layout.fill
}
You can load html, js and css files locally:
let html = $file.read("assets/index.html").string;
$ui.render({
views: [
{
type: "web",
props: {
html
},
layout: $layout.fill
}
]
});
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no">
<link rel="stylesheet" href="local://assets/index.css">
<script src="local://assets/index.js"></script>
</head>
<body>
<h1>Hey, there!</h1><img src="local://assets/icon.png">
</body>
<script>
window.onload = () => {
alert(sum(1, 1));
}
</script>
</html>
Local files will be loaded from bundle, shared://
and drive://
are supported.
Prop | Type | Read/Write | Description |
---|---|---|---|
title | string | r | web page title |
url | string | w | address |
toolbar | bool | w | shows a toolbar |
html | string | w | html content |
text | string | w | text content |
loading | boolean | r | is loading |
progress | number | r | loading progress |
canGoBack | boolean | r | can go back |
canGoForward | boolean | r | can go forward |
ua | string | w | User Agent |
scrollEnabled | bool | rw | is scroll enabled |
bounces | bool | rw | is bounces enabled |
transparent | bool | rw | is background transparent |
showsProgress | bool | w | whethe shows progress bar |
inlineMedia | bool | w | allows inline video |
airPlay | bool | w | allows AirPlay |
pictureInPicture | bool | w | allows picture in picture |
allowsNavigation | bool | rw | allows back gestures |
allowsLinkPreview | bool | rw | allows link preview |
Go back.
Go forward.
Reload current page.
Reload from origin.
Stop loading.
Evaluate JavaScript:
webView.eval({
script: "var sum = 1 + 2",
handler: function(result, error) {
}
})
Similar to eval
, but this one is an async function:
const {result, error} = await webView.exec("1 + 1");
didClose
will be called after closed:
didClose: function(sender) {
}
decideNavigation
can be used to intercept requests:
decideNavigation: function(sender, action) {
if (action.requestURL === "https://apple.com") {
return false
}
return true
}
didStart
will be called after started:
didStart: function(sender, navigation) {
}
didReceiveServerRedirect
will be called after received server redirect:
didReceiveServerRedirect: function(sender, navigation) {
}
didFinish
will be called after load finished:
didFinish: function(sender, navigation) {
}
didFail
will be called after load failed:
didFail: function(sender, navigation, error) {
}
didSendRequest
will be called after XMLHttpRequest sent:
didSendRequest: function(request) {
var method = request.method
var url = request.url
var header = request.header
var body = request.body
}
We could control the web view by inject JavaScript, for example:
props: {
script: function() {
var images = document.getElementsByTagName("img")
for (var i=0; i<images.length; ++i) {
var element = images[i]
element.onclick = function(event) {
var source = event.target || event.srcElement
$notify("share", {"url": source.getAttribute("data-original")})
return false
}
}
}
}
script
is a function
, all code inside it will be executed after website load finished.
script
could also be a string
, but need to be escaped, for example use Escape:
props: {
script: "for(var images=document.getElementsByTagName(\"img\"),i=0;i<images.length;++i){var element=images[i];element.onclick=function(e){var t=e.target||e.srcElement;return $notify(\"share\",{url:t.getAttribute(\"data-original\")}),!1}}"
}
Obviously, use function as script looks more delightful.
Send message from script to native components, $notify
message to events
:
props: {
script: function() {
$notify("customEvent", {"key": "value"})
}
},
events: {
customEvent: function(object) {
// object = {"key": "value"}
}
}
The message will be passed to events -> customEvent
, we could handle native events here.
Above logic looks a bit complicated, please check this example:
$ui.render({
props: {
title: "Doutu"
},
views: [
{
type: "button",
props: {
title: "Search"
},
layout: function(make) {
make.right.top.inset(10)
make.size.equalTo($size(64, 32))
},
events: {
tapped: function(sender) {
search()
}
}
},
{
type: "input",
props: {
placeholder: "Please input keywords"
},
layout: function(make) {
make.top.left.inset(10)
make.right.equalTo($("button").left).offset(-10)
make.height.equalTo($("button"))
},
events: {
returned: function(sender) {
search()
}
}
},
{
type: "web",
props: {
script: "for(var images=document.getElementsByTagName(\"img\"),i=0;i<images.length;++i){var element=images[i];element.onclick=function(e){var t=e.target||e.srcElement;return $notify(\"share\",{url:t.getAttribute(\"data-original\")}),!1}}"
},
layout: function(make) {
make.left.bottom.right.equalTo(0)
make.top.equalTo($("input").bottom).offset(10)
},
events: {
share: function(object) {
$http.download({
url: `http:${object.url}`,
handler: function(resp) {
$share.universal(resp.data)
}
})
}
}
}
]
})
function search() {
const keyword = $("input").text;
const url = `https://www.doutula.com/search?keyword=${encodeURIComponent(keyword)}`;
$("input").blur()
$("web").url = url
}
$("input").focus()
Similar to JavaScript Injection, CSS Injection is also supported:
props: {
style: ""
}
Of course, you can implement this by using JavaScript Injection, it's just an easy way.
Native code could send message to web view as well, just use notify(event, message)
:
const webView = $("webView");
webView.notify({
"event": "foobar",
"message": {"key": "value"}
});
Above mechanism provies opportunity to switch context between web and native.
Action Extension
is displayed on iOS share sheet, JSBox scripts could be launched from here.
Besides, action extension can retrieve the data selected by user.
Action extension also has some Limitations, not so much like today widget.
For now, there are basically two main restrictions:
$photo.pick
, it doesn't workWe could use $context
to fetch external data, that's very important to make an action extension.
Please refer to Method to see more details.
Get all query items (URL Scheme launched script):
const query = $context.query;
If we use jsbox://runjs?file=demo.js&text=test
to launch it, the query is:
{
"file": "demo.js",
"text": "test"
}
If the source application is another third-party app, there might be its bundle identifier:
const sourceApp = $context.query.source_app;
You can check it when needed, it doesn't work for iOS built-in apps.
Returns a text (user shared a text):
const text = $context.text;
Returns all text items.
Returns a link (user shared a link):
const link = $context.link;
Returns all link items.
Returns an image (user shared an image):
const image = $context.image;
Returns all image items.
Returns all Safari items.
const items = $context.safari.items;
Returns:
{
"baseURI": "",
"source": "",
"location": "",
"contentType": "",
"title": "",
"selection": {
"html": "",
"text": "",
"style": ""
}
}
Returns a binary data (user shared a file):
const data = $context.data;
Returns all binary files.
Returns all items, ignores the type.
Basically, if we want to get external parameters, use $context
APIs.
Clear all data in the context object, including query and Action Extension objects:
$context.clear();
Close current action extension, the script stops running.
Here are some constants provided by JSBox, you can use it easily in your scripts.
For example $align.left
is actually 0
, but it's no way to remember that, so we need constants.
JSBox environment:
const $env = {
app: 1 << 0,
today: 1 << 1,
action: 1 << 2,
safari: 1 << 3,
notification: 1 << 4,
keyboard: 1 << 5,
siri: 1 << 6,
widget: 1 << 7,
all: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6) | (1 << 7)
};
iOS text alignment type:
const $align = {
left: 0,
center: 1,
right: 2,
justified: 3,
natural: 4
};
iOS view content mode:
const $contentMode = {
scaleToFill: 0,
scaleAspectFit: 1,
scaleAspectFill: 2,
redraw: 3,
center: 4,
top: 5,
bottom: 6,
left: 7,
right: 8,
};
iOS button type:
const $btnType = {
custom: 0,
system: 1,
disclosure: 2,
infoLight: 3,
infoDark: 4,
contactAdd: 5,
};
Alert item style:
const $alertActionType = {
default: 0, cancel: 1, destructive: 2
};
Zero values:
const $zero = {
point: $point(0, 0),
size: $size(0, 0),
rect: $rect(0, 0, 0, 0),
insets: $insets(0, 0, 0, 0)
};
Common layout functions:
const $layout = {
fill: function(make, view) {
make.edges.equalTo(view.super)
},
fillSafeArea: function(make, view) {
make.edges.equalTo(view.super.safeArea)
},
center: function(make, view) {
make.center.equalTo(view.super)
}
};
iOS line cap:
const $lineCap = {
butt: 0,
round: 1,
square: 2
};
iOS line join:
const $lineJoin = {
miter: 0,
round: 1,
bevel: 2
};
const $mediaType = {
image: "public.image",
jpeg: "public.jpeg",
jpeg2000: "public.jpeg-2000",
tiff: "public.tiff",
pict: "com.apple.pict",
gif: "com.compuserve.gif",
png: "public.png",
icns: "com.apple.icns",
bmp: "com.microsoft.bmp",
ico: "com.microsoft.ico",
raw: "public.camera-raw-image",
live: "com.apple.live-photo",
movie: "public.movie",
video: "public.video",
audio: "public.audio",
mov: "com.apple.quicktime-movie",
mpeg: "public.mpeg",
mpeg2: "public.mpeg-2-video",
mp3: "public.mp3",
mp4: "public.mpeg-4",
avi: "public.avi",
wav: "com.microsoft.waveform-audio",
midi: "public.midi-audio"
};
iOS image picker constants:
const $imgPicker = {
quality: {
high: 0,
medium: 1,
low: 2,
r640x480: 3,
r1280x720: 4,
r960x540: 5
},
captureMode: {
photo: 0,
video: 1
},
device: {
rear: 0,
front: 1
},
flashMode: {
off: -1,
auto: 0,
on: 1
}
};
iOS keyboard types:
const $kbType = {
default: 0,
ascii: 1,
nap: 2,
url: 3,
number: 4,
phone: 5,
namePhone: 6,
email: 7,
decimal: 8,
twitter: 9,
search: 10,
asciiPhone: 11
};
iOS asset media constants:
const $assetMedia = {
type: {
unknown: 0,
image: 1,
video: 2,
audio: 3
},
subType: {
none: 0,
panorama: 1 << 0,
hdr: 1 << 1,
screenshot: 1 << 2,
live: 1 << 3,
depthEffect: 1 << 4,
streamed: 1 << 16,
highFrameRate: 1 << 17,
timelapse: 1 << 18
}
};
$pdf page sizes:
const $pageSize = {
letter: 0, governmentLetter: 1, legal: 2, juniorLegal: 3, ledger: 4, tabloid: 5,
A0: 6, A1: 7, A2: 8, A3: 9, A4: 10, A5: 11, A6: 12, A7: 13, A8: 14, A9: 15, A10: 16,
B0: 17, B1: 18, B2: 19, B3: 20, B4: 21, B5: 22, B6: 23, B7: 24, B8: 25, B9: 26, B10: 27,
C0: 28, C1: 29, C2: 30, C3: 31, C4: 32, C5: 33, C6: 34, C7: 35, C8: 36, C9: 37, C10: 38,
custom: 52
};
Event types which are used in addEventHandler
function:
const $UIEvent = {
touchDown: 1 << 0,
touchDownRepeat: 1 << 1,
touchDragInside: 1 << 2,
touchDragOutside: 1 << 3,
touchDragEnter: 1 << 4,
touchDragExit: 1 << 5,
touchUpInside: 1 << 6,
touchUpOutside: 1 << 7,
touchCancel: 1 << 8,
valueChanged: 1 << 12,
primaryActionTriggered: 1 << 13,
editingDidBegin: 1 << 16,
editingChanged: 1 << 17,
editingDidEnd: 1 << 18,
editingDidEndOnExit: 1 << 19,
allTouchEvents: 0x00000FFF,
allEditingEvents: 0x000F0000,
applicationReserved: 0x0F000000,
systemReserved: 0xF0000000,
allEvents: 0xFFFFFFFF,
};
Axis values for stack view:
const $stackViewAxis = {
horizontal: 0,
vertical: 1,
};
Distribution values for stack view:
const $stackViewDistribution = {
fill: 0,
fillEqually: 1,
fillProportionally: 2,
equalSpacing: 3,
equalCentering: 4,
};
Alignment values for stack view:
const $stackViewAlignment = {
fill: 0,
leading: 1,
top: 1,
firstBaseline: 2,
center: 3,
trailing: 4,
bottom: 4,
lastBaseline: 5,
};
Spacing values for stack view:
const $stackViewSpacing = {
useDefault: UIStackViewSpacingUseDefault,
useSystem: UIStackViewSpacingUseSystem,
};
Popover arrow directions for $ui.popover(...)
method:
const $popoverDirection = {
up: 1 << 0,
down: 1 << 1,
left: 1 << 2,
right: 1 << 3,
any: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3),
};
Scroll directions for matrix
views:
const $scrollDirection = {
vertical: 0,
horizontal: 1,
};
Style constants for blur
views:
const $blurStyle = {
// Additional Styles
extraLight: 0,
light: 1,
dark: 2,
extraDark: 3,
regular: 4,
prominent: 5,
// Adaptable Styles (iOS 13)
ultraThinMaterial: 6,
thinMaterial: 7,
material: 8,
thickMaterial: 9,
chromeMaterial: 10,
// Light Styles (iOS 13)
ultraThinMaterialLight: 11,
thinMaterialLight: 12,
materialLight: 13,
thickMaterialLight: 14,
chromeMaterialLight: 15,
// Dark Styles (iOS 13)
ultraThinMaterialDark: 16,
thinMaterialDark: 17,
materialDark: 18,
thickMaterialDark: 19,
chromeMaterialDark: 20,
};
Please refer to Apple's documentation for details: https://developer.apple.com/documentation/uikit/uiblureffectstyle
Check layout type of a home screen widget:
const $widgetFamily = {
small: 0,
medium: 1,
large: 2,
xLarge: 3, // iPadOS 15
};
Pass values between JavaScript and Native environment is not easy, we need to handle complicated data types.
Some types like number
, boolean
will be converted automatically, but there are still a lot of types can't be handled automatically, such as color
, size
etc.
So we provided a series of methods for data converting, you could check this doc whenever you need.
To be honest, this way is not perfect. We can also pass JavaScript Objects to iOS directly (it works like a dictionary, or map in other languages), then we parse JSON objects for each API everytime.
But that's looks ugly, and it's not easy to provide IDE features like auto completion and syntax highlighting with that pattern.
The most important is, manage all types together makes the program safer and easy to read.
As we mentioned before, we provided a series of methods.
Create a rectangle:
const rect = $rect(0, 0, 100, 100);
Create a size:
const size = $size(100, 100);
Create a point:
const point = $point(0, 0);
Create an edge insets:
const insets = $insets(10, 10, 10, 10);
Create a color with hex string:
const color = $color("#00EEEE");
Create a color with name:
const blackColor = $color("black");
Available color names:
Name | Color |
---|---|
tint | JSBox theme color |
black | black color |
darkGray | dark gray |
lightGray | light gray |
white | white color |
gray | gray color |
red | red color |
green | green color |
blue | blue color |
cyan | cyan color |
yellow | yellow color |
magenta | magenta color |
orange | orange color |
purple | purple color |
brown | brown color |
clear | clear color |
The following colors are semantic colors, used for dynamic theme, it will be different in light or dark mode:
Name | Color |
---|---|
tintColor | tint color |
primarySurface | primary surface |
secondarySurface | secondary surface |
tertiarySurface | tertiary surface |
primaryText | primary text |
secondaryText | secondary text |
backgroundColor | background color |
separatorColor | separator color |
groupedBackground | grouped background |
insetGroupedBackground | insetGrouped background |
Below are system default colors, they are implemented based on UI Element Colors
Name | Color |
---|---|
systemGray2 | UIColor.systemGray2Color |
systemGray3 | UIColor.systemGray3Color |
systemGray4 | UIColor.systemGray4Color |
systemGray5 | UIColor.systemGray5Color |
systemGray6 | UIColor.systemGray6Color |
systemLabel | UIColor.labelColor |
systemSecondaryLabel | UIColor.secondaryLabelColor |
systemTertiaryLabel | UIColor.tertiaryLabelColor |
systemQuaternaryLabel | UIColor.quaternaryLabelColor |
systemLink | UIColor.linkColor |
systemPlaceholderText | UIColor.placeholderTextColor |
systemSeparator | UIColor.separatorColor |
systemOpaqueSeparator | UIColor.opaqueSeparatorColor |
systemBackground | UIColor.systemBackgroundColor |
systemSecondaryBackground | UIColor.secondarySystemBackgroundColor |
systemTertiaryBackground | UIColor.tertiarySystemBackgroundColor |
systemGroupedBackground | UIColor.systemGroupedBackgroundColor |
systemSecondaryGroupedBackground | UIColor.secondarySystemGroupedBackgroundColor |
systemTertiaryGroupedBackground | UIColor.tertiarySystemGroupedBackgroundColor |
systemFill | UIColor.systemFillColor |
systemSecondaryFill | UIColor.secondarySystemFillColor |
systemTertiaryFill | UIColor.tertiarySystemFillColor |
systemQuaternaryFill | UIColor.quaternarySystemFillColor |
These colors behave differently for light or dark mode. For example, $color("tintColor")
returns theme color for the light mode, and light blue for the dark mode.
You can retrieve all available colors in the color palette with $color("namedColors")
, it returns a dictionary:
const colors = $color("namedColors");
Also, $color(...)
supports dynamic colors, it returns color for light and dark mode dynamically:
const dynamicColor = $color({
light: "#FFFFFF",
dark: "#000000"
});
That color is white for light mode, and black for dark mode, changes automatically, can also be simplified as:
const dynamicColor = $color("#FFFFFF", "#000000");
Colors can be nested, it can use colors generated by the $rgba(...)
method:
const dynamicColor = $color($rgba(0, 0, 0, 1), $rgba(255, 255, 255, 1));
Besides, if you want to provide colors for the pure black theme, you can do:
const dynamicColor = $color({
light: "#FFFFFF",
dark: "#141414",
black: "#000000"
});
Create a color with red, green, blue values.
The range of each number is 0 ~ 255:
const color = $rgb(100, 100, 100);
Create a color with red, green, blue and alpha channel:
const color = $rgba(100, 100, 100, 0.5);
Create a font, name is an optional parameter:
const font1 = $font(15);
const font2 = $font("bold", 15);
You can specify "bold"
to use system font with bold weight, otherwise it will search fonts with the name.
Learn more: http://iosfonts.com/
Create a range:
const range = $range(0, 10);
Create an indexPath, to indicates the section and row:
const indexPath = $indexPath(0, 10);
Create a binary data:
// string
const data = $data({
string: "Hello, World!",
encoding: 4 // default, refer: https://developer.apple.com/documentation/foundation/nsstringencoding
});
// path
const data = $data({
path: "demo.txt"
});
// url
const data = $data({
url: "https://images.apple.com/v/ios/what-is/b/images/performance_large.jpg"
});
// base64
const data = $data({
base64: "data:image/png;base64,..."
});
// byte array
const data = $data({
byteArray: [116, 101, 115, 116]
})
Returns an image object, supports the following types:
// file path
const image = $image("assets/icon.png");
// sf symbols
const image = $image("sunrise");
// url
const image = $image("https://images.apple.com/v/ios/what-is/b/images/performance_large.jpg");
// base64
const image = $image("data:image/png;base64,...");
The scale
argument indicates its scale, default to 1, 0 means using screen scale.
In the latest version, you can use $image(...)
to create dynamic images for dark mode, like this:
const dynamicImage = $image({
light: "light-image.png",
dark: "dark-image.png"
});
This image chooses different resources for light and dark mode, it switches automatically, can be simplified as:
const dynamicImage = $image("light-image.png", "dark-image.png");
Besides, images can also be nested, such as:
const lightImage = $image("light-image.png");
const darkImage = $image("dark-image.png");
const dynamicImage = $image(lightImage, darkImage);
Get an icon provided by JSBox, refer: https://github.com/cyanzhong/xTeko/tree/master/extension-icons
For example:
$ui.render({
views: [
{
type: "button",
props: {
icon: $icon("005", $color("red"), $size(20, 20)),
bgcolor: $color("clear")
},
layout: function(make, view) {
make.center.equalTo(view.super)
make.size.equalTo($size(24, 24))
}
}
]
})
color
is optional, uses gray style if not provided.
size
is also optional, uses raw size if not provided.
Here's a table shows all types handled by iOS automatically:
Objective-C type | JavaScript type |
---|---|
nil | undefined |
NSNull | null |
NSString | string |
NSNumber | number, boolean |
NSDictionary | Object object |
NSArray | Array object |
NSDate | Date object |
NSBlock (1) | Function object (1) |
id (2) | Wrapper object (2) |
Class (3) | Constructor object (3) |
But if we get an object from iOS native interface, it's not easy to know its properties.
For example:
didSelect: function(tableView, indexPath) {
var row = indexPath.row
}
indexPath
is actually an native object, in this example we can access its properties with .section
and .row
.
To understand iOS native objects, we provided a table: Object Properties.
$props
helps you get all properties of an object:
const props = $props("string");
$props is just a simple JavaScript function, it implemented like:
const $props = object => {
const result = [];
for (; object != null; object = Object.getPrototypeOf(object)) {
const names = Object.getOwnPropertyNames(object);
for (let idx=0; idx<names.length; idx++) {
const name = names[idx];
if (!result.includes(name)) {
result.push(name)
}
}
}
return result
};
$desc
returns description of an object:
let desc = $desc(object);
console.log(desc);
Console related APIs
Bug is always exists, so we need to debug
. As a development tool, JSBox has some utilities for troubleshooting.
JSBox provides a simple console for debug purpose, we can use it to log some information, and execute some scripts.
console
provides 4 methods for logging:
console.info("General Information") // Log information
console.warn("Warning Message") // Log warning message
console.error("Error Message") // Log error message
console.assert(false, "Message") // Assertion
console.clear() // Clear all messages
console.open() // Open console
console.close() // Close console
Of course, the console of JSBox can run JavaScript as well, the result will be display on the console.
Besides, when JavaScript exception occured, it shows on console as an error automatically.
JSBox has builtin archiver/unarchiver module
Compress files to a zip:
var files = $context.dataItems
var dest = "Archive.zip"
if (files.length == 0) {
return
}
$archiver.zip({
files: files,
dest: dest,
handler: function(success) {
if (success) {
$share.sheet([dest, $file.read(dest)])
}
}
})
files
is a file array, we could use paths
as well.
You could also compress a folder by:
$archiver.zip({
directory: "",
dest: "",
handler: function(success) {
}
})
Unzip a file:
$archiver.unzip({
file,
dest: "folder",
handler: function(success) {
}
})
File has to be a zip document, dest is the destination folder, it has to be existed.
Starts from 2.0, you can also use path
property which points to the zip file, it uses less memory:
const success = await $archiver.unzip({
path: "archive.zip",
dest: "folder"
});
Provides a webView based JavaScript environment, lets you use Web APIs:
$browser.exec({
script: function() {
const parser = new DOMParser();
const doc = parser.parseFromString("<a>hey</a>", "application/xml");
// $notify("customEvent", {"key": "value"})
return doc.children[0].innerHTML;
},
handler: function(result) {
$ui.alert(result);
},
customEvent: function(message) {
}
})
You can use Promise with super neat style:
var result = await $browser.exec("return 1 + 1;");
You can create script dynamically by:
const name = "JSBox";
$browser.exec({
script: `
var parser = new DOMParser();
var doc = parser.parseFromString("<a>hey ${name}</a>", "application/xml");
return doc.children[0].innerHTML;`,
handler: function(result) {
$ui.alert(result);
}
})
For details of $notify
, refer to web component.
JSBox has builtin data detector, it enhances the readability of regex
PS: If you are an expert of regex, you don't need this then.
Retrieve all dates from a string:
const dates = $detector.date("2017.10.10");
Retrieve all addresses from a string:
const addresses = $detector.address("");
Retrieve all links from a string:
const links = $detector.link("http://apple.com hello http://xteko.com");
Retrieve all phone numbers from a string:
const phoneNumbers = $detector.phoneNumber("18666666666 hello 18777777777");
In JSBox, you can create plugins for the code editor, it helps you like an assistant.
Many useful features can be made with these APIs, such as custom indentation, or text encoding tools.
Get or set all text in the code editor:
const text = $editor.text;
$editor.text = "Hey!";
Returns the text view of the current running editor:
const editorView = $editor.view;
editorView.alpha = 0.5;
Get or set selected range in the code editor:
const range = $editor.selectedRange;
$editor.selectedRange = $(0, 10);
Get or set selected text in the code editor:
const text = $editor.selectedText;
$editor.selectedText = "Hey!";
Returns true when the editor has text:
const hasText = $editor.hasText;
Check whether the code editor is active:
const isActive = $editor.isActive;
Check whether undo action can be taken:
const canUndo = $editor.canUndo;
Check whether redo action can be taken:
const canRedo = $editor.canRedo;
Save changes in the current editor:
$editor.save();
Perform undo action in the current editor:
$editor.undo();
Perform redo action in the current editor:
$editor.redo();
Activate the current editor:
$editor.activate()
Deactivate the current editor:
$editor.deactivate()
Insert text into the selected range:
$editor.insertText("Hello");
Remove the character just before the cursor:
$editor.deleteBackward();
Get text in a range:
const text = $editor.textInRange($range(0, 10));
Set text in a range:
$editor.setTextInRange("Hey!", $range(0, 10));
JSBox provided various of APIs other than iOS builtin APIs, we will talk about those.
Basically we provided $text
, $share
, $qrcode
and $detector
for now.
Handle text easily, such as base64 encode/decode and much more.
Share media (text/image etc) to social network services.
QRCode related features, such as encode/decode and scan.
Simulate a browser environment, so you can leverage the ability of BOM and DOM.
Some functions to handle common data detection easily, similar to regular expressions.
Used to schedule a notification or cancel a scheduled notification
Schedule a local notification:
$push.schedule({
title: "title",
body: "content",
delay: 5,
handler: function(result) {
const id = result.id;
}
})
This notification will be delivered after 5 seconds.
Param | Type | Description |
---|---|---|
title | string | title |
body | string | body |
id | string | identifier (optional) |
sound | string | sound |
mute | bool | mute |
repeats | bool | repeats |
script | string | script name |
height | number | 3D Touch view height |
query | json | extra parameters, will be passed to $context.query |
attachments | array | media attachments, e.g. ["assets/icon.png"] |
renew | bool | whether renew |
Above case is how to schedule a push with delay, you can also setup a date:
const date = new Date();
date.setSeconds(date.getSeconds() + 10)
$push.schedule({
title: "title",
body: "content",
date,
handler: function(result) {
const id = result.id;
}
})
Not only that, you have another cool choice, schedule by location:
$push.schedule({
title: "title",
body: "content",
region: {
lat: 0, // latitude
lng: 0, // longitude
radius: 1000, // meters
notifyOnEntry: true, // notify on entry
notifyOnExit: true // notify on exit
}
})
JSBox will ask user for location access once this code get called.
Started from v1.10.0, $push supports setup a script to push notification by setting script
, user can tap to run it, or simply force touch to have a quick preview.
Cancel a scheduled notification:
$push.cancel({
title: "title",
body: "content",
})
JSBox will cancel all notifications that match the title
and body
.
You can also can it the identifier:
$push.cancel({id: ""})
Clear all scheduled notifications (notifications that registered before build 462 will be ignored):
$push.clear()
JSBox has ability to handle QRCode
Encode a string to QRCode image:
const image = $qrcode.encode("https://apple.com");
Decode a string from QRCode image:
const text = $qrcode.decode(image);
Scan QRCode with camera:
$qrcode.scan(text => {
})
We could provide cancelled
callback:
$qrcode.scan({
useFrontCamera: false, // Optional
turnOnFlash: false, // Optional
handler(string) {
$ui.toast(string)
},
cancelled() {
$ui.toast("Cancelled")
}
})
Provides APIs to share media to social network services, such as WeChat, QQ
Using system builtin share sheet
:
$share.sheet(["https://apple.com", "apple"])
The object could be either a file or an array.
Currently we support text
, link
, image
and data
.
It's better to specify file name if the object is a file:
$share.sheet([
{
"name": "sample.mp4",
"data": data
}
])
Start from Build 80, you can use following style:
$share.sheet({
items: [
{
"name": "sample.mp4",
"data": data
}
], // or item
handler: function(success) {
}
})
Share content to WeChat:
$share.wechat(image)
The object will be detected automatically, either image or text is correct.
Share content to QQ:
$share.qq(image)
The object will be detected automatically, either image or text is correct.
Deprecated, please use $share.sheet
instead.
Provides a lot of utility functions to handle text
Generates a UUID string:
const uuid = $text.uuid;
Text tokenize:
$text.tokenize({
text: "我能吞下玻璃而不伤身体",
handler: function(results) {
}
})
// TODO: Text analysis
Lookup text in system builtin dictionaries.
$text.lookup("apple")
Text to speech (TTS):
$text.speech({
text: "Hello, World!",
rate: 0.5,
language: "en-US", // optional
})
You can stop/pause/continue a speaker by:
const speaker = $text.speech({});
speaker.pause()
speaker.continue()
speaker.stop()
Events for state changes:
$text.speech({
text: "Hello, World!",
events: {
didStart: (sender) => {},
didFinish: (sender) => {},
didPause: (sender) => {},
didContinue: (sender) => {},
didCancel: (sender) => {},
}
})
Supported languages:
ar-SA
cs-CZ
da-DK
de-DE
el-GR
en-AU
en-GB
en-IE
en-US
en-US
en-ZA
es-ES
es-MX
fi-FI
fr-CA
fr-FR
he-IL
hi-IN
hu-HU
id-ID
it-IT
ja-JP
ko-KR
nl-BE
nl-NL
no-NO
pl-PL
pt-BR
pt-PT
ro-RO
ru-RU
sk-SK
sv-SE
th-TH
tr-TR
zh-CN
zh-HK
zh-TW
Returns supported voices, you can use one of them to specify the voice in $text.speech:
const voices = $text.ttsVoices;
console.log(voices);
$text.speech({
text: "Hello, World!",
voice: voices[0]
});
Voice Object:
Prop | Type | Read/Write | Description |
---|---|---|---|
language | string | r | language |
identifier | string | r | identifier |
name | string | r | name |
quality | number | r | quality |
gender | number | r | gender |
audioFileSettings | object | r | audio file settings |
Base64 encode.
Base64 decode.
URL encode.
URL decode.
HTML escape.
HTML unescape.
MD5.
SHA1.
SHA256.
Get Chinese PinYin of a string.
Convert Markdown text to HTML text.
Convert HTML text to Markdown text, this is an async func:
$text.htmlToMarkdown({
html: "<p>Hey</p>",
handler: markdown => {
}
})
// Or
var markdown = await $text.htmlToMarkdown("<p>Hey</p>");
Convert data to a string:
const string = $text.decodeData({
data: file,
encoding: 4 // default, refer: https://developer.apple.com/documentation/foundation/nsstringencoding
});
Calculate text bounding size dynamically:
const size = $text.sizeThatFits({
text: "Hello, World",
width: 320,
font: $font(20),
lineSpacing: 15, // Optional
});
JSBox provides a simple XML/HTML parser, which is very easy to use, it supports xPath
and CSS selector
for node querying.
Parsing XML string as XML document:
let xml =
`
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>
`;
let doc = $xml.parse({
string: xml, // Or data: data
mode: "xml", // Or "html", default to xml
});
$xml.parse() returns XML Document object:
let version = doc.version;
let rootElement = doc.rootElement;
Element represents a node in XML Document, it contains properties as below:
Prop | Type | Read/Write | Description |
---|---|---|---|
node | string | r | node itself |
document | $xmlDoc | r | Document |
blank | bool | r | is blank node |
namespace | string | r | namespace |
tag | string | r | tag |
lineNumber | number | r | line number |
attributes | object | r | attributes |
parent | $xmlElement | r | parent node |
previous | $xmlElement | r | previous sibling |
next | $xmlElement | r | next sibling |
string | string | r | string represented value |
number | number | r | number represented value |
date | Date | r | date represented value |
Document and Element object can query first child with xPath, selector and tag:
let c1 = doc.rootElement.firstChild({
"xPath": "//food/name"
});
let c2 = doc.rootElement.firstChild({
"selector": "food > serving[units]"
});
let c3 = doc.rootElement.firstChild({
"tag": "daily-values",
"namespace": "namespace", // Optional
});
Document and Element object can query children with tag and namespace:
let children = doc.rootElement.children({
"tag": "daily-values",
"namespace": "namespace", // Optional
});
// Get all
let allChildren = doc.rootElement.children();
Document and Element object can enumerate elements with xPath and CSS selector:
let element = doc.rootElement;
element.enumerate({
xPath: "//food/name", // Or selector (e.g. food > serving[units])
handler: (element, idx) => {
}
});
Document and Element object can query value with attribute and namespace:
let value = doc.rootElement.value({
"attribute": "attribute",
"namespace": "namespace", // Optional
});
Document can define prefix for namespace:
doc.definePrefix({
"prefix": "prefix",
"namespace": "namespace"
});
Here is an example for $xml: https://github.com/cyanzhong/xTeko/tree/master/extension-demos/xml-demo
In JSBox we provided a lot of APIs to store/fetch files, in order to save files to disk, for example downloaded files.
As you know, iOS has an awesome mechanism called Sandbox
, it makes each application independent.
JSBox has this ability as well, it sounds like Sandbox in Sandbox
, it makes each script has its own file zone.
So, a script can't access all paths, it's limited, and JSBox hides all details to script developers. It is very safe and convenient.
Other than sandboxes, JSBox also provided a special folder can be shared by all scripts.
To use this folder, just use file path starts with shared://
.
Please note all contents in this folder could be modified by all scripts.
In addition, file system also supports read/write files in iCloud Drive Container.
To use this folder, just use file path starts with drive://
.
Please note this folder doesn't work if user didn't turn on iCloud Drive for JSBox.
One more thing, all files imported by share sheet or AirDrop will be placed in a special folder.
To use this folder, just use file path starts with inbox://
.
Please note all contents in this folder could be modified by all scripts.
When absolute paths need to be handled, you can indicate that the current path is an absolute path by starting it with absolute://
.
For example $file.copy(...)
, when the target path is an absolute path, the above protocol is needed to provide information for this method.
JSBox provides
$drive
in order to access iCloud Drive
There are three types of operations:
$drive.open
open Document Picker$drive.save
save file using Document Picker$drive.read
read file directly, like $file
Pick a file using iCloud Document Picker:
$drive.open({
handler: function(data) {
}
})
We can set the UTI types to make it more accurate:
types: ["public.png"]
You can also specify multi
to allow multiple selection:
const files = await $drive.open({ multi: true });
In this case, the returned files
is an array of data.
Save a file using Document Picker:
$drive.save({
data: $data({string: "Hello, World!"}),
name: "File Name",
handler: function() {
}
})
This API looks similar to $file.read:
const file = $drive.read("demo.txt");
The only difference is we are reading file in iCloud Drive container.
It equals to:
const file = $file.read("drive://demo.txt");
Besides, every $file
API has an equivalent $drive
API:
$drive.read
vs $file.read$drive.download
vs $file.download$drive.write
vs $file.write$drive.delete
vs $file.delete$drive.list
vs $file.list$drive.copy
vs $file.copy$drive.move
vs $file.move$drive.mkdir
vs $file.mkdir$drive.exists
vs $file.exists$drive.isDirectory
vs $file.isDirectory$drive.absolutePath
vs $file.absolutePathJSBox provides some APIs to store/fetch files safely
Read a file:
const file = $file.read("demo.txt");
This method ensures a iCloud drive file is downloaded before reading it:
const data = await $file.download("drive://.test.db.icloud");
Write a file:
const success = $file.write({
data: $data({string: "Hello, World!"}),
path: "demo.txt"
});
Delete a file:
const success = $file.delete("demo.txt");
Get all file names in a folder:
const contents = $file.list("download");
Copy a file:
const success = $file.copy({
src: "demo.txt",
dst: "download/demo.txt"
});
Move a file:
const success = $file.move({
src: "demo.txt",
dst: "download/demo.txt"
});
Make a directory (folder):
const success = $file.mkdir("download");
Check if a file exists:
const exists = $file.exists("demo.txt");
Checkc if a path is directory:
const isDirectory = $file.isDirectory("download");
Merge multiple files into one file:
$file.merge({
files: ["assets/1.txt", "assets/2.txt"],
dest: "assets/merged.txt",
chunkSize: 1024 * 1024, // optional, default is 1024 * 1024
});
Split a file into multiple files:
$file.split({
file: "assets/merged.txt",
chunkSize: 1024, // optional, default is 1024
});
The file will be split as: merged-001.txt, merged-002.txt, ...
Returns the absolute path of a relative path:
const absolutePath = $file.absolutePath(path);
Returns the root path of the documents folder (in absolute path style):
const rootPath = $file.rootPath;
Returns all installed scripts:
const extensions = $file.extensions;
Access shared folder like we mentioned before:
const file = $file.read("shared://demo.txt");
Access iCloud Drive container like we mentioned before:
const file = $file.read("drive://demo.txt");
Offers APIs that relate to the application and addin itself
Specify theme
for the script, used for Dark Mode related stuff, possible values are light
/ dark
/ auto
.
It will be overridden if a screen has its own theme
value.
Set the minimal available version of JSBox:
$app.minSDKVer = "3.1.0"
Set the minimal available version of iOS:
$app.minOSVer = "10.3.3"
PS: Numbers are separated by .
, version comparation goes from left to right.
Give user a nice hint, the effect looks like $ui.alert
, but only once for an addin's whole life:
$app.tips("Hey!")
Returns the info of JSBox itself:
{
"bundleID": "app.cyan.jsbox",
"version": "3.0.0",
"build": "9527",
}
Disable auto dimming of the screen:
$app.idleTimerDisabled = true
Close the addin that user current uses, delay
is an optional parameter to specify a delay seconds.
PS: It's better to call this manually if your addin doesn't have a user interface
Check whether it is debugging:
if ($app.isDebugging) {
}
Get current environment:
const env = $app.env;
Value | Description |
---|---|
$env.app | Main App |
$env.today | Today Widget |
$env.action | Action Extension |
$env.safari | Safari Extension |
$env.notification | Notification Extension |
$env.keyboard | Keyboard Extension |
$env.siri | Siri Extension |
$env.widget | Home Screen Widget |
$env.all | All (Default) |
We can check whether an addin runs on widget:
if ($app.env == $env.today) {
}
Since JSBox supports multiple widgets, you can check which widget is being used:
const index = $app.widgetIndex;
// 0 ~ 2, other value means not a widget
Manage scroll views automatically, to avoid the keyboard hides text fields:
$app.autoKeyboardEnabled = true
Display a toolbar at the top of keyboard:
$app.keyboardToolbarEnabled = true
Set to true to disable screen rotating:
$app.rotateDisabled = true
Open a URL or a URL Scheme, for example open WeChat:
$app.openURL("weixin://")
Open a URL with external browsers:
$app.openBrowser({
type: 10000,
url: "https://apple.com"
})
Type | Browser |
---|---|
10000 | Chrome |
10001 | UC |
10002 | Firefox |
10003 | |
10004 | Opera |
10005 | Quark |
10006 | iCab |
10007 | Maxthon |
10008 | Dolphin |
10009 | 2345 |
We don't promise it works fine, because above browsers change their APIs frequently
Open another JSBox script, for example:
$app.openExtension("demo.js")
Observe notifications posted by JSBox addins:
$app.listen({
// Will be called when app is ready
ready: function() {
},
// Will be called when app exit
exit: function() {
},
// Will be called when app resign active
pause: function() {
},
// Will be called when app resume active
resume: function() {
}
});
Post a custom event:
$app.listen({
eventName: function(object) {
console.log(object);
}
});
$app.notify({
name: "eventName",
object: {"a": "b"}
});
Persistence is always needed for a software, JSBox offers various persistence ways, cache is the easiest one
JSBox provides memory cache and disk cache with a really simple interface, all JavaScript objects could be cached.
All APIs have both sync
and async
ways, choose one according to your scenarios.
Write to cache:
$cache.set("sample", {
"a": [1, 2, 3],
"b": "1, 2, 3"
})
Write to cache (async):
$cache.setAsync({
key: "sample",
value: {
"a": [1, 2, 3],
"b": "1, 2, 3"
},
handler: function(object) {
}
})
Read from cache:
$cache.get("sample")
Read from cache (async):
$cache.getAsync({
key: "sample",
handler: function(object) {
}
})
Delete a cache:
$cache.remove("sample")
Delete a cache (async):
$cache.removeAsync({
key: "sample",
handler: function() {
}
})
Delete all cached objects:
$cache.clear()
Delete all cached objects (async):
$cache.clearAsync({
handler: function() {
}
})
Clipboard is very important for iOS data sharing, JSBox provides various interfaces
// Get clipboard text
const text = $clipboard.text;
// Set clipboard text
$clipboard.text = "Hello, World!"
// Get clipboard image data
const data = $clipboard.image;
// Set clipboard image data
$clipboard.image = data
// Get all items from clipboard
const items = $clipboard.items;
// Set items to clipboard
$clipboard.items = items
Get all phone numbers from clipboard.
Get the first phone number from clipboard.
Get all links from clipboard.
Get the first link from clipboard.
Get all emails from clipboard.
Get the first email from clipboard.
Get all dates from clipboard.
Get the first date from clipboard.
Set text to clipboard, but ignore Universal Clipboard
.
Set clipboard by type
and value
:
$clipboard.set({
"type": "public.plain-text",
"value": "Hello, World!"
})
Set clipboard, you can provide an expiration date:
$clipboard.copy({
text: "Temporary text",
ttl: 20
})
Param | Type | Description |
---|---|---|
text | string | text |
image | image | image |
data | data | data |
ttl | number | time to live |
locally | bool | locally |
UTTypes
: https://developer.apple.com/documentation/mobilecoreservices/uttype
Clear all items in clipboard.
Retrieve some useful information of the device, such as language, device model etc
Returns the basic info of device:
{
"model": "string",
"language": "string",
"version": "string",
"name": "cyan's iPhone",
"screen": {
"width": 240,
"height": 320,
"scale": 2.0,
"orientation": 1,
},
"battery": {
"state": 1, // 0: unknown 1: normal 2: charging 3: charging & fully charged
"level": 0.9399999976158142
}
}
Get the SSID of current Wi-Fi:
const ssid = $device.ssid;
Example:
{
"SSIDDATA": {},
"BSSID": "aa:bb:cc:dd:ee:ff",
"SSID": "SSID"
}
Note: In iOS 13 and above, this API needs location access, you can use $location
APIs to request the access.
Returns the network type:
const networkType = $device.networkType;
Value | Type |
---|---|
0 | None |
1 | Wi-Fi |
2 | Cellular |
Returns memory usage and disk usage of the device:
const space = $device.space;
Example:
{
"disk": {
"free": {
"bytes": 87409733632,
"string": "87.41 GB"
},
"total": {
"bytes": 127989493760,
"string": "127.99 GB"
}
},
"memory": {
"free": {
"bytes": 217907200,
"string": "207.8 MB"
},
"total": {
"bytes": 3221225472,
"string": "3 GB"
}
}
}
Generate a Taptic Engine Feedback:
$device.taptic(0)
Param | Type | Description |
---|---|---|
level | number | 0 ~ 2 |
Get WLAN address:
const address = $device.wlanAddress;
Check whether device is dark mode:
if ($device.isDarkMode) {
}
Check screen type quickly:
const isIphoneX = $device.isIphoneX;
const isIphonePlus = $device.isIphonePlus;
const isIpad = $device.isIpad;
const isIpadPro = $device.isIpadPro;
Check whether Touch ID is supported:
const hasTouchID = $device.hasTouchID;
Check whether Face ID is supported:
const hasFaceID = $device.hasFaceID;
Check whether device is jailbroken:
const isJailbroken = $device.isJailbroken;
Check whether VoiceOver is running:
const isVoiceOverOn = $device.isVoiceOverOn;
In this part we introduce some basic APIs in JSBox, including app/device/cache/network
etc.
Device related APIs.
Application related APIs.
Operating System related APIs.
HTTP client, create requests like HTTP GET/POST
.
Disk Cache and Memory Cache, all in one.
Execute code on main thread or background thread, or schedule a function after delay.
Compared to object caching, keychain stores sensitive data like passwords and credentials in a safe way.
Write to keychain:
const succeeded = $keychain.set("key", "value", "my.domain");
When
domain
is provided,key
should be unique in your script. Otherwise,key
should be unique in all scripts.
Read from keychain:
const item = $keychain.get("key", "my.domain");
When
domain
is provided,key
should be unique in your script. Otherwise,key
should be unique in all scripts.
Delete a keychain item:
const succeeded = $keychain.remove("key", "my.domain");
When
domain
is provided,key
should be unique in your script. Otherwise,key
should be unique in all scripts.
Delete all keychain items:
const succeeded = $keychain.clear("my.domain");
domain
is required.
Retrieve all keychain item keys:
const keys = $keychain.keys("my.domain");
domain
is required.Provide localized texts for your script, it's a great habits, we should do that.
l10n
means Localization
, because localization has 10 characters.
You can use $l10n
to support multiple languages easily, or you can detect the language and display what ever you want by yourself (not recommend).
There are only 2 steps to localize a text:
$app.strings
to define all strings$l10n
to get localized stringsFor example:
$app.strings = {
"en": {
"title": "tortoise"
},
"zh-Hans": {
"title": "乌龟"
},
"zh-Hant": {
"title": "烏龜"
}
}
en
means English, zh-Hans
means Simplified-Chinese, zh-Hant
means Traditional-Chinese.
How to use it:
const title = $l10n("title");
The fallback logic:
Network module is very important in JSBox, you can leverage this ability to achieve more
Send a general HTTP request:
$http.request({
method: "POST",
url: "https://apple.com",
header: {
k1: "v1",
k2: "v2"
},
body: {
k1: "v1",
k2: "v2"
},
handler: function(resp) {
const data = resp.data;
}
})
Param | Type | Description |
---|---|---|
method | string | GET/POST/DELETE etc |
url | string | url |
header | object | http header |
body | object | http body |
timeout | number | timeout (second) |
form | object | form-data |
files | array | file list |
proxy | json | proxy configuration |
progress | function | upload/download callback |
showsProgress | bool | shows progress |
message | string | upload/download message |
handler | function | finished callback |
body
could be a JSON object or a binary data,
If body
is a JSON object:
Content-Type
is application/json
, body will be encoded as JSON objectContent-Type
is application/x-www-form-urlencoded
, body will be converted to a=b&c=d
formatted stringContent-Type
is application/json
If body
is a binary data:
body won't be converted, it directly pass to body field.
For form
and files
, refer $http.upload
to see details.
resp
properties:
Param | Type | Description |
---|---|---|
data | string | json object will be parsed automatically |
rawData | data | raw response binary data |
response | response | Refer |
error | error | Refer |
Proxy configuration:
{
proxy: {
"HTTPEnable": true,
"HTTPProxy": "",
"HTTPPort": 0,
"HTTPSEnable": true,
"HTTPSProxy": "",
"HTTPSPort": 0
}
}
Send a GET
request:
$http.get({
url: "https://apple.com",
handler: function(resp) {
const data = resp.data;
}
})
It just like a general request, but the method is always GET
.
Send a POST
request:
$http.post({
url: "https://apple.com",
handler: function(resp) {
const data = resp.data;
}
})
It just like a general request, but the method is always POST
.
It just like a general request, but the data
inside resp is a binary data (file):
$http.download({
url: "https://images.apple.com/v/ios/what-is/b/images/performance_large.jpg",
showsProgress: true, // Optional, default is true
backgroundFetch: true, // Optional, default is false
progress: function(bytesWritten, totalBytes) {
const percentage = bytesWritten * 1.0 / totalBytes;
},
handler: function(resp) {
$share.sheet(resp.data)
}
})
File upload request, here's an example based on qiniu:
$photo.pick({
handler: function(resp) {
const image = resp.image;
if (image) {
$http.upload({
url: "http://upload.qiniu.com/",
form: {
token: "<token>"
},
files: [
{
"image": image,
"name": "file",
"filename": "file.png"
}
],
progress: function(percentage) {
},
handler: function(resp) {
}
})
}
}
})
Pick a photo from photo library, upload it to qiniu (fill your own token).
You can fill form-data parameters in form
, like token
in this example.
files
properties:
Param | Type | Description |
---|---|---|
image | object | image |
data | object | binary data |
name | string | form name |
filename | string | file name |
content-type | string | content type |
Start a web server, serve local files to provide HTTP interfaces:
$http.startServer({
port: 5588, // port number
path: "", // script root path
handler: function(result) {
const url = result.url;
}
})
This code will create a server based on root path, you can access by url, we also provide a web interface that allows use access on browser.
At the same time, you can access server by following APIs:
GET
/list?path=path list contentsGET
/download?path=path download filePOST
/upload {"files[]": file}
upload filePOST
/move {"oldPath": "", "newPath": ""}
move filePOST
/delete {"path": ""}
delete filePOST
/create {"path": ""}
create folderPlease understand above contents by using your knowledge of HTTP.
Stop a web server:
$http.stopServer()
Shorten a link:
$http.shorten({
url: "https://apple.com",
handler: function(url) {
}
})
Expand a link:
$http.lengthen({
url: "http://t.cn/RJZxkFD",
handler: function(url) {
}
})
Get network interface received/sent data in bytes:
const ifa_data = $network.ifa_data;
console.log(ifa_data)
Example output:
{
"en0" : {
"received" : 2581234688,
"sent" : 469011456
},
"awdl0" : {
"received" : 2231296,
"sent" : 8180736
},
"utun0" : {
"received" : 0,
"sent" : 0
},
"pdp_ip1" : {
"received" : 2048,
"sent" : 2048
},
"pdp_ip0" : {
"received" : 2215782400,
"sent" : 211150848
},
"en2" : {
"received" : 5859328,
"sent" : 6347776
},
"utun2" : {
"received" : 0,
"sent" : 0
},
"lo0" : {
"received" : 407684096,
"sent" : 407684096
},
"utun1" : {
"received" : 628973568,
"sent" : 35312640
}
}
Returns all network interfaces on the current device:
const interfaces = $network.interfaces;
// E.g. { 'en0/ipv4': 'x.x.x.x' }
start pinging:
$network.startPinging({
host: "google.com",
timeout: 2.0, // default
period: 1.0, // default
payloadSize: 56, // default
ttl: 49, // default
didReceiveReply: function(summary) {
},
didReceiveUnexpectedReply: function(summary) {
},
didSendPing: function(summary) {
},
didTimeout: function(summary) {
},
didFail: function(error) {
},
didFailToSendPing: function(response) {
}
})
summary
structure:
{
"sequenceNumber": 0,
"payloadSize": 0,
"ttl": 0,
"host": "",
"sendDate": null,
"receiveDate": null,
"rtt": 0,
"status": 0
}
For more information: https://github.com/lmirosevic/GBPing
Stop pinging.
Port of CFNetworkCopySystemProxySettings
The easiest way to create user preferences
Start from v1.51.0, you can create user preferences with a simple prefs.json
in the root folder. It generates settings view for you automatically.
Here is an example:
{
"title": "SETTINGS",
"groups": [
{
"title": "GENERAL",
"items": [
{
"title": "USER_NAME",
"type": "string",
"key": "user.name",
"value": "default user name"
},
{
"title": "AUTO_SAVE",
"type": "boolean",
"key": "auto.save",
"value": true
},
{
"title": "OTHERS",
"type": "child",
"value": {
"title": "OTHERS",
"groups": [
]
}
}
]
}
]
}
In your script, the settings view can be opened with the following code:
$prefs.open(() => {
// Done
});
The root element is a JSON object that contains title
and groups
nodes. title
will be displayed as the page title, and groups
specifies all setting groups in the current page.
Under groups
, there is a JSON object which contains title
and items
. items
means all setting rows in the current group.
If there is only one group, you can also simplify the configuration as:
{
"title": "GENERAL",
"items": [
{
"title": "USER_NAME",
"type": "string",
"key": "user.name",
"value": "default user name",
"inline": true // inline edit, default is false
}
]
}
A setting item contains the following attributes:
title
: titletype
: value type, such as "string" or "boolean"key
: the key that will be used to persistent settingsvalue
: default value for the current setting, can be nullplaceholder
: placeholder shown when input is emptyinsetGrouped
: whether to use insetGrouped style listinline
: whether to edit text inlineIn order to make localization easier, a title will be used as the localization key first, if nothing is found in the strings
folder, itself will be used.
Currently, below types are supported:
string
: normal text, edits multi-line text by defaultpassword
: secure text entry, renders characters as dotsnumber
: decimal numbersinteger
: integer numbersboolean
: boolean value, shows a switchslider
: decimal between 0 and 1, shows a sliderlist
: shows a string list, let the user pick oneinfo
: shows a readonly informative textlink
: shows a link, tap it to openscript
: contains a code snippet, tap it to runchild
: child node, tap it to open a new settings viewTypes like string
, number
or integer
are relatively easy to use, I'm going to show you some exceptions.
Works the same as type: "string"
, used for sensitive data like passwords.
It provides a slider that can be used to select a decimal value between 0 and 1. So, the value
should also between 0 and 1.
If you don't want a value between 0 and 1, you need to do some transformations. In short, take it as a percentage.
Shows a string list, like a memu:
{
"title": "IMAGE_QUALITY",
"type": "list",
"key": "image.quality",
"items": ["LOW", "MEDIUM", "HIGH"],
"value": 1
}
It displays "LOW", "MEDIUM" and "HIGH" in a list, and the value
is actually an index
. Also, it will be localized.
Note that, items
are user-facing strings, not something that will be stored. What will be stored is the index value.
Sometimes, you may want some customizable behaviors, like this:
{
"title": "TEST",
"type": "script",
"value": "require('scripts/test').runTest();"
}
Sometimes, you may want to fold some settings to 2nd level or 3rd level, like this:
{
"title": "OTHERS",
"type": "child",
"value": {
"title": "OTHERS",
"groups": []
}
}
The above value
has exactly the same structure comparing to the root element, you can nest them as much as you can.
key
is a string that will be used to save/read settings, you have to make sure they are unique in the whole script.
Read settings:
const name = $prefs.get("user.name");
In most cases, settings should be changed by users, but just in case you want some flexibility, you can change them programmatically:
$prefs.set("user.name", "cyan");
Returns all key values:
const prefs = $prefs.all();
Obviously, $prefs
cannot cover all scenarios, but for most common used ones, it's good enough, and easy to use. Here is an example: https://github.com/cyanzhong/xTeko/tree/master/extension-demos/prefs
Other than using the default prefs.json
file, you can also edit any JSON preference objects, with formats mentioned above:
const edited = await $prefs.edit({
"title": "SETTINGS",
"groups": [
{
"title": "GENERAL",
"items": [
{
"title": "USER_NAME",
"type": "string",
"key": "user.name",
"value": "default user name"
}
// ...
]
}
]
});
The returned object is edited preferences, you can make user preferences more flexible that way.
Operating System related APIs
Get/Set the brightness of screen:
$system.brightness = 0.5
Get/Set the volume of speaker (0.0 ~ 1.0):
$system.volume = 0.5
Make a phone call, similar to $app.openURL("tel:number")
.
Send a text message, similar to $app.openURL("sms:number")
.
Send an email, similar to $app.openURL("mailto:email")
.
Create a FaceTime session, similar to $app.openURL("facetime:number")
.
Create home screen icon for url:
$system.makeIcon({
title: "Title",
url: "https://sspai.com",
icon: image
})
This topic including thread switching, after delay and timer
Run on background thread:
$thread.background({
delay: 0,
handler: function() {
}
})
Run on main thread (with delay):
$thread.main({
delay: 0.3,
handler: function() {
}
})
Param | Type | Description |
---|---|---|
delay | number | delay (seconds) |
handler | function | handler |
It can be simplifies as below if no delay is needed:
$thread.main(() => {
});
Run after delay easily:
$delay(3, () => {
$ui.alert("Hey!")
})
Present an alert after 3 seconds.
You can cancel the task by:
const task = $delay(10, () => {
});
// Cancel it
task.cancel();
It's similar to $delay but Promise is supported:
await $wait(2);
alert("Hey!");
Schedule a timer:
const timer = $timer.schedule({
interval: 3,
handler: function() {
$ui.toast("Hey!")
}
});
Show a toast "Hey!" in every 3 seconds, cancel it by:
timer.invalidate()
l10n
means Localization
, because localization has 10 characters.
You can use $l10n
to support multiple languages easily, or you can detect the language and display what ever you want by yourself (not recommend).
const text = $l10n("MAIN_TITLE");
Run after delay easily:
$delay(3, () => {
$ui.alert("Hey!")
})
Present an alert after 3 seconds.
You can cancel the task by:
const task = $delay(10, () => {
});
// Cancel it
task.cancel();
Create a rectangle:
const rect = $rect(0, 0, 100, 100);
Create a size:
const size = $size(100, 100);
Create a point:
const point = $point(0, 0);
Create an edge insets:
const insets = $insets(10, 10, 10, 10);
Create a color with hex string:
const color = $color("#00EEEE");
Create a color with name:
const blackColor = $color("black");
Available color names:
Name | Color |
---|---|
tint | JSBox theme color |
black | black color |
darkGray | dark gray |
lightGray | light gray |
white | white color |
gray | gray color |
red | red color |
green | green color |
blue | blue color |
cyan | cyan color |
yellow | yellow color |
magenta | magenta color |
orange | orange color |
purple | purple color |
brown | brown color |
clear | clear color |
The following colors are semantic colors, used for dynamic theme, it will be different in light or dark mode:
Name | Color |
---|---|
tintColor | tint color |
primarySurface | primary surface |
secondarySurface | secondary surface |
tertiarySurface | tertiary surface |
primaryText | primary text |
secondaryText | secondary text |
backgroundColor | background color |
separatorColor | separator color |
groupedBackground | grouped background |
insetGroupedBackground | insetGrouped background |
Below are system default colors, they are implemented based on UI Element Colors
Name | Color |
---|---|
systemGray2 | UIColor.systemGray2Color |
systemGray3 | UIColor.systemGray3Color |
systemGray4 | UIColor.systemGray4Color |
systemGray5 | UIColor.systemGray5Color |
systemGray6 | UIColor.systemGray6Color |
systemLabel | UIColor.labelColor |
systemSecondaryLabel | UIColor.secondaryLabelColor |
systemTertiaryLabel | UIColor.tertiaryLabelColor |
systemQuaternaryLabel | UIColor.quaternaryLabelColor |
systemLink | UIColor.linkColor |
systemPlaceholderText | UIColor.placeholderTextColor |
systemSeparator | UIColor.separatorColor |
systemOpaqueSeparator | UIColor.opaqueSeparatorColor |
systemBackground | UIColor.systemBackgroundColor |
systemSecondaryBackground | UIColor.secondarySystemBackgroundColor |
systemTertiaryBackground | UIColor.tertiarySystemBackgroundColor |
systemGroupedBackground | UIColor.systemGroupedBackgroundColor |
systemSecondaryGroupedBackground | UIColor.secondarySystemGroupedBackgroundColor |
systemTertiaryGroupedBackground | UIColor.tertiarySystemGroupedBackgroundColor |
systemFill | UIColor.systemFillColor |
systemSecondaryFill | UIColor.secondarySystemFillColor |
systemTertiaryFill | UIColor.tertiarySystemFillColor |
systemQuaternaryFill | UIColor.quaternarySystemFillColor |
These colors behave differently for light or dark mode. For example, $color("tintColor")
returns theme color for the light mode, and light blue for the dark mode.
You can retrieve all available colors in the color palette with $color("namedColors")
, it returns a dictionary:
const colors = $color("namedColors");
Also, $color(...)
supports dynamic colors, it returns color for light and dark mode dynamically:
const dynamicColor = $color({
light: "#FFFFFF",
dark: "#000000"
});
That color is white for light mode, and black for dark mode, changes automatically, can also be simplified as:
const dynamicColor = $color("#FFFFFF", "#000000");
Colors can be nested, it can use colors generated by the $rgba(...)
method:
const dynamicColor = $color($rgba(0, 0, 0, 1), $rgba(255, 255, 255, 1));
Besides, if you want to provide colors for the pure black theme, you can do:
const dynamicColor = $color({
light: "#FFFFFF",
dark: "#141414",
black: "#000000"
});
Create a color with red, green, blue values.
The range of each number is 0 ~ 255:
const color = $rgb(100, 100, 100);
Create a color with red, green, blue and alpha channel:
const color = $rgba(100, 100, 100, 0.5);
Create a font, name is an optional parameter:
const font1 = $font(15);
const font2 = $font("bold", 15);
You can specify "bold"
to use system font with bold weight, otherwise it will search fonts with the name.
Learn more: http://iosfonts.com/
Create a range:
const range = $range(0, 10);
Create an indexPath, to indicates the section and row:
const indexPath = $indexPath(0, 10);
Create a binary data:
// string
const data = $data({
string: "Hello, World!",
encoding: 4 // default, refer: https://developer.apple.com/documentation/foundation/nsstringencoding
});
// path
const data = $data({
path: "demo.txt"
});
// url
const data = $data({
url: "https://images.apple.com/v/ios/what-is/b/images/performance_large.jpg"
});
Returns an image object, supports the following types:
// file path
const image = $image("assets/icon.png");
// sf symbols
const image = $image("sunrise");
// url
const image = $image("https://images.apple.com/v/ios/what-is/b/images/performance_large.jpg");
// base64
const image = $image("data:image/png;base64,...");
The scale
argument indicates its scale, default to 1, 0 means using screen scale.
In the latest version, you can use $image(...)
to create dynamic images for dark mode, like this:
const dynamicImage = $image({
light: "light-image.png",
dark: "dark-image.png"
});
This image chooses different resources for light and dark mode, it switches automatically, can be simplified as:
const dynamicImage = $image("light-image.png", "dark-image.png");
Besides, images can also be nested, such as:
const lightImage = $image("light-image.png");
const darkImage = $image("dark-image.png");
const dynamicImage = $image(lightImage, darkImage);
Get an icon provided by JSBox, refer: https://github.com/cyanzhong/xTeko/tree/master/extension-icons
For example:
$ui.render({
views: [
{
type: "button",
props: {
icon: $icon("005", $color("red"), $size(20, 20)),
bgcolor: $color("clear")
},
layout: function(make, view) {
make.center.equalTo(view.super)
make.size.equalTo($size(24, 24))
}
}
]
})
color
is optional, uses gray style if not provided.
size
is also optional, uses raw size if not provided.
Create a UIAccessibilityCustomAction, for instance:
{
type: "view",
props: {
isAccessibilityElement: true,
accessibilityCustomActions: [
$accessibilityAction("Hello", () => alert("Hello"))
]
}
}
Sometimes, values generated by Runtime will be released by system automatically, in order to avoid that, you could manage reference yourself:
const manager = $objc("Manager").invoke("new");
$objc_retain(manager)
It maintains the object until script stopped.
Release a Runtime object manually:
$objc_release(manager)
Get an Objective-C Protocol:
const p = $get_protocol(name);
Clean all Objective-C definitions:
$objc_clean();
Besides functions that provided by JavaScript, JSBox offers many extra functions, you can use them globally.
For instance:
const text = $l10n("MAIN_TITLE");
This function gives you a localized string.
In order to distinguish them from JavaScript built-in functions, built-in functions provided by JSBox start with $
.
Apple has introduced home screen widgets in iOS 14, and JSBox v2.12.0
provides full support for that. At the same time, support for the today widget has been deprecated and will be removed by Apple in future iOS releases.
Home screen widgets are very different from today widgets, so it's worth taking a moment to understand some of the basic concepts.
Home screen widgets are essentially a series of snapshot based on a timeline, rather than dynamically built interfaces. So when a user sees a widget, iOS doesn't run the code contained in the widget directly. Instead, the system calls the widget's code at some point (the "timeline" mechanism, we will discuss later), and our code can provide a snapshot. The system then shows these snapshots at the appropriate time, and repeat this based on some policies.
Also, home screen widgets can only handle limited user interactions:
These limitations dictate that the role of the home screen widget is more like information board, complex interactions should be delegated to the main app.
JSBox supports all widget layouts, to add them:
In contrast to the today widget, which can only run one script (although JSBox provides three), the home screen widget can be configured to create as many instances as you like, and can be stacked to display different content via the system's "stack" feature.
For more information on how to use it, please refer to tutorials provided by Apple or other posts.
For each widget, you can set which script to run, and an additional parameter to provide more information.
The parameter is a string value that can be retrieved like this:
const inputValue = $widget.inputValue;
For script packages, widget parameters can be provided dynamically like:
[
{
"name": "Option 1",
"value": "Value 1"
},
{
"name": "Option 2",
"value": "Value 2"
}
]
name
is the display string for user, value
stands for the actual value as mentioned above. To let widget provide dynamic options, just place the configuration as widget-options.json
under the root path for each package.
To make your learning curve smoother, we have created some sample projects for reference:
We will improve this repository later to provide more examples.
As we have already mentioned, the layout system of the widget is completely different from $ui.render
, so before learning the JSBox implementation, it is recommended to have a basic understanding of SwiftUI's layout system.
In short, compared to UIKit where we specify an auto layout constraint for each view, in the widget we implement the layout of its child views by using hstack
, vstack
, zstack
and so on.
Create a horizontal layout space, for example:
$widget.setTimeline(ctx => {
return {
type: "hstack",
props: {
alignment: $widget.verticalAlignment.center,
spacing: 20
},
views: [
{
type: "text",
props: {
text: "Hello"
}
},
{
type: "text",
props: {
text: "World"
}
}
]
}
});
Where spacing
specifies the spacing between views, and alignment
uses $widget.verticalAlignment to specify the vertical alignment of views.
Create a vertical layout space, for example:
$widget.setTimeline(ctx => {
return {
type: "vstack",
props: {
alignment: $widget.horizontalAlignment.center,
spacing: 20
},
views: [
{
type: "text",
props: {
text: "Hello"
}
},
{
type: "text",
props: {
text: "World"
}
}
]
}
});
Where spacing
specifies the spacing between views, and alignment
uses $widget.horizontalAlignment to specify the horizontal alignment of views.
Create a space that stacks its children, for example:
$widget.setTimeline(ctx => {
return {
type: "zstack",
props: {
alignment: $widget.alignment.center
},
views: [
$color("red"),
{
type: "text",
props: {
text: "Hello, World!"
}
}
]
}
});
Where alignment
uses $widget.alignment to specify the views's alignment.
Add spacing to the layout space, for example:
$widget.setTimeline(ctx => {
return {
type: "hstack",
views: [
{
type: "text",
props: {
text: "Hello"
}
},
{
type: "spacer",
props: {
// minLength: 10
}
},
{
type: "text",
props: {
text: "World"
}
}
]
}
});
This will squeeze the two text views to the sides of the layout space, and minLength
is the minimum length of the spacer.
Create a horizontal grid layout, E.g.:
$widget.setTimeline(ctx => {
return {
type: "hgrid",
props: {
rows: Array(4).fill({
flexible: {
minimum: 10,
maximum: Infinity
},
// spacing: 10,
// alignment: $widget.alignment.left
}),
// spacing: 10,
// alignment: $widget.verticalAlignment.center
},
views: Array(8).fill({
type: "text",
props: {
text: "Item"
}
})
}
});
The above code creates a horizontal grid of 4 rows and 2 columns, the contents of which are displayed as Item
, and the item size can be used as follows:
{
fixed: 10
}
Refer: https://developer.apple.com/documentation/swiftui/griditem/size-swift.enum
{
flexible: {
minimum: 10,
maximum: Infinity
}
}
Refer: https://developer.apple.com/documentation/swiftui/griditem/size-swift.enum
{
adaptive: {
minimum: 10,
maximum: Infinity
}
}
Refer: https://developer.apple.com/documentation/swiftui/griditem/size-swift.enum
Create a vertical grid layout, E.g.:
$widget.setTimeline(ctx => {
return {
type: "vgrid",
props: {
columns: Array(4).fill({
flexible: {
minimum: 10,
maximum: Infinity
},
// spacing: 10,
// alignment: $widget.alignment.left
}),
// spacing: 10,
// alignment: $widget.horizontalAlignment.center
},
views: Array(8).fill({
type: "text",
props: {
text: "Item"
}
})
}
});
The above code creates a 2-row, 4-column vertical grid that is displayed as Item
, the parameters have the same meaning as hgrid
.
We added some new methods and constants for home screen widgets to the $widget
module for ease of use.
Provide a timeline:
$widget.setTimeline({
entries: [
{
date: new Date(),
info: {}
}
],
policy: {
atEnd: true
},
render: ctx => {
return {
type: "text",
props: {
text: "Hello, World!"
}
}
}
});
Please refer to timeline for details.
Trigger timeline refresh manually, the system can decide whether to refresh or not:
$widget.reloadTimeline();
Return the parameter set for the current widget:
const inputValue = $widget.inputValue;
inputValue
is undefined when the main app is running, use a mock value for testing purposes
Return the layout of the current widget, 0 ~ 3 means small, medium, large, and extra large (iPadOS 15) respectively:
const family = $widget.family;
// 0, 1, 2, 3
In most cases, you should rely on the ctx
returned in the render
function to retrieve family
. Use this API only if you need to get it before calling setTimeline
.
By default, this value is 0 when the main app is running, and can be overridden by the test code:
$widget.family = $widgetFamily.medium;
Return the display size of the current widget:
const size = $widget.displaySize;
// size.width, size.height
In most cases, you should rely on the ctx
returned in the render
function to retrieve displaySize
. Use this API only if you need to get it before calling setTimeline
.
By default, this value refers to small size when the main app is running, and can be tweaked by modifying the family:
$widget.family = $widgetFamily.medium;
Check if the current widget is running in dark mode:
const isDarkMode = $widget.isDarkMode;
In most cases, you should rely on the ctx
returned in the render
function to retrieve isDarkMode
. Use this API only if you need to get it before calling setTimeline
.
Return the alignment
constants for layout:
const alignment = $widget.alignment;
// center, leading, trailing, top, bottom
// topLeading, topTrailing, bottomLeading, bottomTrailing
You can also use string literals, such as "center", "leading"...
Return the horizontalAlignment
constants for layout:
const horizontalAlignment = $widget.horizontalAlignment;
// leading, center, trailing
You can also use string literals, such as "leading", "center"...
Return the verticalAlignment
constants for layout:
const verticalAlignment = $widget.verticalAlignment;
// top, center, bottom
// firstTextBaseline, lastTextBaseline
You can also use string literals, such as "top", "center"...
Return the dateStyle
constants that is used when configure a text
view using dates:
const dateStyle = $widget.dateStyle;
// time, date, relative, offset, timer
You can also use string literals, such as "time", "date"...
Check if it's running in a home screen widget environment:
if ($app.env == $env.widget) {
}
The properties of a widget specify its display effects and behavior, some properties support all types of views, and some properties are unique to text or images.
You can set multiple properties inside props
, something like this:
props: {
frame: {
width: 100,
height: 100
},
background: $color("red"),
padding: 15
}
Note that this simplification differs from SwiftUI's native View Modifier:
The way SwiftUI modifiers work dictates that different sequences produce different results, and each modifier produces a new view, so you can apply the same type of modifier over and over again.
When you need to fully mimic the logic in SwiftUI, you can use the modifiers
array.
modifiers: [
{
frame: { width: 100, height: 100 },
background: $color("red")
},
{ padding: 15 }
]
In the above code, the order of frame
and background
is undefined, but padding
will be applied later.
The syntax is exactly the same as in props
and the following examples will not be repeated.
Specify the size and alignment of the view to:
props: {
frame: {
width: 100,
height: 100,
alignment: $widget.alignment.center
}
}
It can be more flexible:
props: {
frame: {
minWidth: 0,
idealWidth: 100,
maxWidth: Infinity,
minHeight: 0,
idealHeight: 100,
maxHeight: Infinity,
}
}
The appropriate layout is inferred automatically, using maxWidth: Infinity
and maxHeight: Infinity
when the view is needed to fill the parent view.
Specify the position of the view:
props: {
position: $point(0, 0) // {"x": 0, "y": 0}
}
Specifies the view's position offset:
props: {
offset: $point(-10, -10) // {"x": -10, "y": -10}
}
Specify the padding of the view:
props: {
padding: 10
}
This will give it a padding of 10 in all edges, or you can specify each edge separately:
props: {
padding: $insets(10, 0, 10, 0) // {"top": 10, "left": 0, "bottom": 10, "right": 0}
}
Sets the priority by which a parent layout should apportion space to this child (default: 0):
props: {
layoutPriority: 1
}
Apply corner radius:
props: {
cornerRadius: 10
}
When smooth corners are needed:
props: {
cornerRadius: {
value: 10,
style: 1 // 0: circular, 1: continuous
}
}
Create a border:
props: {
border: {
color: $color("red"),
width: 2
}
}
Whether to clip any content that extends beyond the layout bounds of the shape:
props: {
clipped: true
}
You can also enable antialiasing with antialiased
:
props: {
clipped: {
antialiased: true
}
}
Change the opacity of the view:
props: {
opacity: 0.5
}
Rotate the view with an angle in radians:
props: {
rotationEffect: Math.PI * 0.5
}
Apply a Gaussian blur:
props: {
blur: 10
}
Set the foreground color, such as text color:
props: {
color: $color("red")
}
Fill the background, it can be color, image, or gradient:
props: {
background: {
type: "gradient",
props: {
colors: [
$color("#f9d423", "#4CA1AF"),
$color("#ff4e50", "#2C3E50"),
]
}
}
}
Specify the link that will open when the view is tapped (only for 2 * 4 and 4 * 4 layouts).
props: {
link: "jsbox://run?name="
}
Regardless of what this link fills in, iOS will first open the JSBox main app, but the task to be performed can be delegated to the main app using a URL scheme.
Also, you can also use a special URL scheme to run scripts directly:
props: {
link: "jsbox://run?script=alert%28%22hello%22%29"
}
It opens the JSBox main app, and runs the script specified by the script
parameter.
Similar to link
, but widgetURL
specifies the link that is opened when the entire widget is tapped.
props: {
widgetURL: "jsbox://run?script=alert%28%22hello%22%29"
}
2 * 2 layout only supports this type of interaction.
Use bold fonts:
props: {
bold: true
}
Specify the font:
props: {
font: {
name: "AmericanTypewriter",
size: 20,
// weight: "regular" (ultraLight, thin, light, regular, medium, semibold, bold, heavy, black)
// monospaced: true
}
}
It can also be a font created using $font
:
props: {
font: $font("bold", 20)
}
Limit the maximum number of lines:
props: {
lineLimit: 1
}
iOS may reduce the font size when there is not enough text to display. This property specifies the minimum acceptable scale:
props: {
minimumScaleFactor: 0.5
}
If it cannot be displayed in full, the content will be truncated.
Specifies whether the image can be scaled:
props: {
resizable: true
}
Similar to $contentMode.scaleAspectFill, this property determines whether the image fills the parent view in a way that stretches and is cropped.
props: {
scaledToFill: true
}
Similar to $contentMode.scaleAspectFit, this property determines whether the image is placed in the parent view in a way that stretches and holds the content.
props: {
scaledToFit: true
}
Whether to disable VoiceOver:
props: {
accessibilityHidden: false
}
Set VoiceOver label:
props: {
accessibilityLabel: "Hey"
}
Set VoiceOver hint:
props: {
accessibilityHint: "Double tap to open"
}
As we mentioned before, a home screen widget is a series of time-based snapshots. Timelines are the foundation of the entire way widgets work, and we'll spend a little time explaining that concept. But before we do, please read an article provided by Apple: https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date
This article is extremely important for understanding how the timeline works, especially the following diagram:
Our code plays the role of the timeline provider in the diagram, where the system fetches a timeline and reload policy from us at the appropriate time. The system then calls the method we provide to display the widget, and makes the next fetch when the policy is satisfied.
So, an accurate timeline and a well-designed reload strategy are essential for the widget experience. For example, the weather app knows what the weather will be like for the next few hours, so it can provide multiple snapshots to the system at once and perform the next update after all snapshots are displayed.
JSBox uses $widget.setTimeline(...)
to provide timelines mentioned above, E.g.:
$widget.setTimeline({
entries: [
{
date: new Date(),
info: {}
}
],
policy: {
atEnd: true
},
render: ctx => {
return {
type: "text",
props: {
text: "Hello, World!"
}
}
}
});
Before calling setTimeline
, we can perform some data fetching operations, such as requesting network or getting location. However, please note that we must provide a timeline as quick as we can, to avoid possible performance issues.
Specify the date and context data for a snapshot, when timeline is predictable, we can provide many entries at the same time.
In an entry
, we use date
for the time when a snapshot will be displayed, and use info
to carry some contextual information that can be retrieved in the render
function.
If no entry is provided, JSBox will generate a default entry using the current time.
Specify the reload policy, there are several ways to do this:
$widget.setTimeline({
policy: {
atEnd: true
}
});
This method reloads the timeline after all entries are used.
$widget.setTimeline({
policy: {
afterDate: aDate
}
});
This method reloads the timeline with an expiration date called afterDate
.
$widget.setTimeline({
policy: {
never: true
}
});
This methods marks the timeline as static, it won't be updated periodically.
Note that, the above strategy is just "suggestions" to the system, the system does not guarantee that the reload will be done. To prevent the system from filtering out aggressive updates, please design a moderate strategy to ensure the experience.
Without providing
entries
andpolicy
, JSBox provides a default implementation of an hourly refresh for each script.
Upon reaching the time specified in each entry, the system will call the above render
function, where our code returns a JSON data to describe the view.
$widget.setTimeline({
render: ctx => {
return {
type: "text",
props: {
text: "Hello, World!"
}
}
}
});
This code shows a "Hello, World!" on the widget, we will go into more detail about the syntax later.
ctx
contains the context to get some environmental information including entry:
$widget.setTimeline({
render: ctx => {
const entry = ctx.entry;
const family = ctx.family;
const displaySize = ctx.displaySize;
const isDarkMode = ctx.isDarkMode;
}
});
Where family
represents the type of the widget, and the values from 0 to 2 represent the three layouts: small, medium and large. displaySize
reflects the current display size of the widget.
With these values, we can dynamically decide what view to return to the system.
When your script doesn't need to be refreshed, or the default refresh strategy is sufficient, you can simply provide a render function.
$widget.setTimeline(ctx => {
});
As a result, JSBox will automatically create entries and set the policy to refresh every hour.
During development, when $widget.setTimeline
is called from within the main app, a preview of the widget is opened. Three sizes are supported, and the first entry is fixed to be displayed in the timeline.
To apply the script to your actual home screen, please refer to the aforementioned setup method.
To read data asynchronously, you can send network requests before calling setTimeline
. Due to limitations of the timeline mechanism, we may not be able to implement the classic caching logic of first showing the cache, then fetching new data, and then refreshing the UI.
However, network requests are likely to fail, in that case, it is better to show cached content instead of an error. To achieve that, here is a suggested workflow we recommend:
async function fetch() {
const cache = readCache();
const resp1 = await $http.get();
if (failed(resp1)) {
return cache;
}
const resp2 = await $http.download();
if (failed(resp2)) {
return cache;
}
const data = resp2.data;
if (data) {
writeCache(data);
}
return data;
}
const data = await fetch();
Then use data
to build the timeline, so that there are always contents in the widget, even though it may not be up to date.
For a complete example, please refer to: https://github.com/cyanzhong/jsbox-widgets/blob/master/xkcd.js
Compared to the $ui.render
function, which supports a rich set of views, the widget supports a very limited set of view types.
Also, $ui.render
is based on the UIKit, while $widget.setTimeline
is based on the SwiftUI. Even though we designed a similar syntax for the widget, the differences between the two UI technologies meant that prior knowledge could not be completely transferred.
For any kind of view, we define it using the following syntax:
{
type: "",
props: {}, // or modifiers: []
views: []
}
This is similar to $ui.render
in that type
specifies the type, props
or modifiers
specifies the properties, and views
specifies its child views.
The difference is that in SwiftUI we no longer use a UIKit-like layout system, so we don't use layout
anymore.
We will cover the layout system later, let's get started by introducing some visible views.
This view is used to display a piece of non-editable text, similar to the type: "label"
in $ui.render
, and can be constructed as following ways:
{
type: "text",
props: {
text: "Hey"
}
}
This method displays a piece of text directly using the text
property.
{
type: "text",
props: {
date: new Date(),
style: $widget.dateStyle.time
}
}
This method uses date
and style
to display a time or date, style can be referred to $widget.dateStyle。
{
type: "text",
props: {
startDate: startDate,
endDate: endDate
}
}
The method uses startDate
and endDate
to display a time interval.
Properties supported by the text view: bold, font, lineLimit, minimumScaleFactor。
Display an image, like the type: image
in $ui.render
, and can be constructed in the following ways:
{
type: "image",
props: {
image: $image("assets/icon.png"),
// data: $file.read("assets/icon.png")
}
}
This method uses JSBox's existing APIs to provide image
or data
objects.
{
type: "image",
props: {
symbol: "trash",
resizable: true
}
}
This method uses SF Symbols to display an icon.
Since SF Symbols is essentially fonts, you can also specify font size and weight:
{
type: "image",
props: {
symbol: {
glyph: "trash",
size: 64, // Default: 24
weight: "medium" // Default: "regular" Values: ultraLight, thin, light, regular, medium, semibold, bold, heavy, black
},
resizable: true
}
}
{
type: "image",
props: {
path: "assets/icon.png"
}
}
This method uses the file path directly to set the image content.
{
type: "image",
props: {
uri: "https://..."
}
}
This method can use either an online image address, or an image string in base64 format.
Properties supported by the image view: resizable, scaledToFill, scaledToFit, accessibilityHidden, accessibilityLabel, accessibilityHint。
In home screen widgets, a color can be a view, or a property of other views, it can be constructed in the following ways:
{
type: "color",
props: {
color: "#00EEEE"
}
}
This methods creates a color using hex value.
{
type: "color",
props: {
light: "#00EEEE",
dark: "#EE00EE"
}
}
This methods provides both colors for light
mode and dark
mode.
{
type: "color",
props: {
color: $color("red")
}
}
This methods uses the existing $color
function provided by JSBox.
Create a linear gradient effect, E.g.:
{
type: "gradient",
props: {
startPoint: $point(0, 0), // {"x": 0, "y": 0}
endPoint: $point(0, 1), // {"x": 0, "y": 1}
// locations: [0, 1],
colors: [
$color("#f9d423", "#4CA1AF"),
$color("#ff4e50", "#2C3E50"),
]
}
}
Use startPoint
and endPoint
to specify the start and end points, and colors
and locations
to determine the color and position of each gradient section. Note: if locations
is specified, it must be equal in number to colors
.
Create a divider, E.g.:
{
type: "divider",
props: {
background: $color("blue")
}
}
Since v1.12.0, JSBox supports design a keyboard extension with JavaScript, you can create a plugin to improve editing experience.
There's no need to understand how it works, you just leverage abilities of $keyboard
Here are two examples for demonstrating how to build a keyboard plugin:
https://github.com/cyanzhong/xTeko/tree/master/extension-demos/keyboard
https://github.com/cyanzhong/xTeko/tree/master/extension-demos/emoji-key
Insert a string into current editing context:
$keyboard.insert("Hey!")
Delete selected text or delete backward:
$keyboard.delete()
Move cursor by an offset:
$keyboard.moveCursor(5)
Play sound effect of keyboard clicking:
$keyboard.playInputClick()
Check whether current input context has text:
const hasText = $keyboard.hasText;
Get current selected text (iOS 11 only):
const selectedText = $keyboard.selectedText;
Get text before input:
const textBeforeInput = $keyboard.textBeforeInput;
Get text after input:
const textAfterInput = $keyboard.textAfterInput;
Get all text (iOS 11 only):
$keyboard.getAllText(text => {
});
Switch to next keyboard:
$keyboard.next()
Simulate send action in the keyboard:
$keyboard.send()
Dismiss the keyboard:
$keyboard.dismiss()
Whether hides the bottom bar, this is a configuration, you should use it before the UI starts.
Please note, above APIs are only available for keyboard, if you try it on other environments, it will raise an error.
Get or set keyboard height:
let height = $keyboard.height;
$keyboard.height = 500;
JSBox won't track user inputs, will never upload to our server, or even save it locally.
To make sure scripts work correctly, you may need to "Allow Full Access":
When "Allow Full Access" is off, there are many restrictions:
Please consider allow full access on your behalf, but we have to say, even allow full access is on, we won't use any technical measures to manipulate user inputs.
Please be confident with our products.
JSBox can play simple audio, for example a keyboard clicked sound
Use sound id:
// Use sound id
$audio.play({
id: 1000
})
Refer https://github.com/TUNER88/iOSSystemSoundsLibrary to learn more about system sounds.
// Play remote audio
$audio.play({
url: "https://"
})
// Play local audio
$audio.play({
path: "audio.wav"
})
// Pause audio track
$audio.pause()
// Resume audio track
$audio.resume()
// Stop audio track
$audio.stop()
// Seek to a position (in seconds)
$audio.seek(60)
// Get current status
const status = $audio.status;
// 0: paused, 1: waiting, 2: playing
// Get duration
const duration = $audio.duration;
// Get current time
const offset = $audio.offset;
You can observe some events during play an audio track:
$audio.play({
events: {
itemEnded: function() {},
timeJumped: function() {},
didPlayToEndTime: function() {},
failedToPlayToEndTime: function() {},
playbackStalled: function() {},
newAccessLogEntry: function() {},
newErrorLogEntry: function() {},
}
})
Refer: https://developer.apple.com/documentation/avfoundation/avplayeritem?language=objc
JSBox 2.1.0 brings $imagekit
module for image processing, you can achieve many effects easily:
In order to make it easier to understand, we created a demo project that uses all APIs: https://github.com/cyanzhong/jsbox-imagekit
Create an image with options and drawing actions callback:
const options = {
size: $size(100, 100),
color: $color("#00FF00"),
// scale: default to screen scale
// opaque: default to false
}
const image = $imagekit.render(options, ctx => {
const centerX = 50;
const centerY = 50;
const radius = 25;
ctx.fillColor = $color("#FF0000");
ctx.moveToPoint(centerX, centerY - radius);
for (let i = 1; i < 5; ++i) {
const x = radius * Math.sin(i * Math.PI * 0.8);
const y = radius * Math.cos(i * Math.PI * 0.8);
ctx.addLineToPoint(x + centerX, centerY - y);
}
ctx.fillPath();
});
ctx
works exactly the same as canvas
, refer to canvas documentation.
Get image information:
const info = $imagekit.info(source);
// width, height, orientation, scale, props
Get grayscaled image:
const output = $imagekit.grayscale(source);
Invert colors:
const output = $imagekit.invert(source);
Apply sepia filter:
const output = $imagekit.sepia(source);
Enhance image automatically:
const output = $imagekit.adjustEnhance(source);
Red-eye adjustment:
const output = $imagekit.adjustRedEye(source);
Adjust brightness:
const output = $imagekit.adjustBrightness(source, 100);
// value range: (-255, 255)
Adjust contrast:
const output = $imagekit.adjustContrast(source, 100);
// value range: (-255, 255)
Adjust gamma value:
const output = $imagekit.adjustGamma(source, 4);
// value range: (0.01, 8)
Adjust opacity:
const output = $imagekit.adjustOpacity(source, 0.5);
// value range: (0, 1)
Apply gaussian blur:
const output = $imagekit.blur(source, 0);
Emboss effect:
const output = $imagekit.emboss(source, 0);
Sharpen:
const output = $imagekit.sharpen(source, 0);
Unsharpen:
const output = $imagekit.unsharpen(source, 0);
Edge detection:
const output = $imagekit.detectEdge(source, 0);
Crop an image with mask
:
const output = $imagekit.mask(source, mask);
Create an up-down reflected image, from height
position, change alpha value from fromAlpha
to toAlpha
:
const output = $imagekit.reflect(source, 100, 0, 1);
Crop an image:
const output = $imagekit.cropTo(source, $size(100, 100), 0);
// mode:
// - 0: top-left
// - 1: top-center
// - 2: top-right
// - 3: bottom-left
// - 4: bottom-center
// - 5: bottom-right
// - 6: left-center
// - 7: right-center
// - 8: center
Resize an image with scale:
const output = $imagekit.scaleBy(source, 0.5);
Resize an image to a specific size:
const output = $imagekit.scaleTo(source, $size(100, 100), 0);
// mode:
// - 0: scaleFill
// - 1: scaleAspectFit
// - 2: scaleAspectFill
Resize an image using scaleFill
mode:
const output = $imagekit.scaleFill(source, $size(100, 100));
Resize an image using scaleAspectFit
mode:
const output = $imagekit.scaleAspectFit(source, $size(100, 100));
Resize an image using scaleAspectFill
mode:
const output = $imagekit.scaleAspectFill(source, $size(100, 100));
Rotate an image (it may change the size):
const output = $imagekit.rotate(source, Math.PI * 0.25);
Rotate an image (keeps the image size, some contents might be clipped):
const output = $imagekit.rotatePixels(source, Math.PI * 0.25);
Flip an image:
const output = $imagekit.flip(source, 0);
// mode:
// - 0: vertically
// - 1: horizontally
Concatenate images with space:
const output = $imagekit.concatenate(images, 10, 0);
// mode:
// - 0: vertically
// - 1: horizontally
Add mask
directly on image
:
const output = $imagekit.combine(image1, image2, mode);
// mode:
// - 0: top-left
// - 1: top-center
// - 2: top-right
// - 3: bottom-left
// - 4: bottom-center
// - 5: bottom-right
// - 6: left-center
// - 7: right-center
// - 8: center (default)
// - $point(x, y): absolute position
Get an image with rounded corners:
const output = $imagekit.rounded(source, 10);
Get a circular image, it will be centered and clipped if the source image isn't a square:
const output = $imagekit.circular(source);
Extract GIF data to frames:
const {images, durations} = await $imagekit.extractGIF(data);
// image: all image frames
// durations: duration for each frame
Make GIF with image array or video data:
const images = [image1, image2];
const options = {
durations: [0.5, 0.5],
// size: 16, 12, 8, 4, 2
}
const data = await $imagekit.makeGIF(images, options);
You can also use duration
instead of durations
, it makes the duration of each frame are the same.
Make video with image array or GIF data:
const images = [image1, image2];
const data = await $imagekit.makeVideo(images, {
durations: [0.5, 0.5]
});
You can also use duration
instead of durations
, it makes the duration of each frame are the same, GIF data doesn't require durations.
Create PDF documents with simple API
Here is an example:
$pdf.make({
html: "<p>Hello, World!</p><h1 style='background-color: red;'>xTeko</h1>",
handler: function(resp) {
const data = resp.data;
if (data) {
$share.sheet(["sample.pdf", data])
}
}
})
Or, create PDF with images:
let {data} = await $pdf.make({"images": images});
We could specify the pageSize
:
$pdf.make({
url: "https://github.com",
pageSize: $pageSize.A5,
handler: function(resp) {
const data = resp.data;
if (data) {
$share.sheet(["sample.pdf", data])
}
}
})
To understand how $pageSize
works, please refer to: http://en.wikipedia.org/wiki/Paper_size.
Render PDF as an image array:
const images = $pdf.toImages(pdf);
Render PDF as a single image:
const image = $pdf.toImage(pdf);
JSBox provided a series of APIs to interact with photos
Take a photo with camera:
$photo.take({
handler: function(resp) {
const image = resp.image;
}
})
Param | Type | Description |
---|---|---|
edit | boolean | edit image after picked |
mediaTypes | array | media types |
maxDuration | number | max duration of video |
quality | number | quality |
showsControls | boolean | shows controls |
device | number | front/rear camera |
flashMode | number | flash mode |
Refer Constant to see how constant works.
Pick a photo from photo library:
$photo.pick({
handler: function(resp) {
const image = resp.image;
}
})
All parameters are same as $photo.take
, they are just have different source type.
Unlike the take
method, pick
allows you to choose the return data type:
Param | Type | Description |
---|---|---|
format | string | "image" or "data", default is "image" |
Besides, we can set multi: true
to pick multiple photos, and selectionLimit
for maximum number of selections, the result list is like:
Prop | Type | Description |
---|---|---|
status | bool | success |
results | array | all images |
The structure of an object in result (when format is image
):
Prop | Type | Description |
---|---|---|
image | image | image object |
metadata | object | metadata |
filename | string | file name |
The structure of an object in result (when format is data
):
Prop | Type | Description |
---|---|---|
data | data | image file |
metadata | object | metadata |
filename | string | file name |
Ask user take a photo or pick a photo:
$photo.prompt({
handler: function(resp) {
const image = resp.image;
}
})
We can retrieve metadata from a photo:
$photo.pick({
handler: function(resp) {
const metadata = resp.metadata;
if (metadata) {
const gps = metadata["{GPS}"];
const url = `https://maps.apple.com/?ll=${gps.Latitude},${gps.Longitude}`;
$app.openURL(url)
}
}
})
It gets the GPS information, open it in Maps.
Open documentation camera (iOS 13 only):
const response = await $photo.scan();
// response.status, response.results
Save a photo to photo library:
// data
$photo.save({
data,
handler: function(success) {
}
})
// image
$photo.save({
image,
handler: function(success) {
}
})
Fetch photos from photo library:
$photo.fetch({
count: 3,
handler: function(images) {
}
})
There are some parameters to make fetch operation more accurate:
Param | Type | Description |
---|---|---|
type | number | type |
subType | number | sub type |
format | string | "image" or "data", default is "image" |
size | $size | image size, default to raw size |
count | number | fetch limit |
Delete photos in photo library:
$photo.delete({
count: 3,
handler: function(success) {
}
})
The parameters are same as $photo.fetch
.
We could convert image object to PNG or JPEG data:
// PNG
const png = image.png;
// JPEG
const jpg = image.jpg(0.8);
There is a number (0.0 ~ 1.0) means jpeg compress quality.
iOS has a builtin previewer to open files, a lot of file formats are supported.
This is very useful, we can use it to open office files, PDFs, and much more.
We can use it through url:
$quicklook.open({
url: "",
handler: function() {
// Handle dismiss action, optional
}
})
Binary data:
$quicklook.open({
type: "jpg",
data
})
Image object:
$quicklook.open({
image
})
Plain text:
$quicklook.open({
text: "Hello, World!"
})
JSON object:
$quicklook.open({
json: "{\"a\": [1, 2, true]}"
})
HTML contents:
$quicklook.open({
html: "<p>HTML</p>"
})
Or, a content list (they should be same type, either file or url):
$quicklook.open({
list: ["", "", ""]
})
Please note, we can set type
to indicate the file type, it's optional, but it's better to have one.
You can create a simple web server with $http.startServer, it creates a http server with standard html template.
If you want to create a server with custom request handlers, you can use $server APIs:
const server = $server.new();
const options = {
port: 6060, // Required
bonjourName: "", // Optional
bonjourType: "", // Optional
};
server.start(options);
Start a simple HTTP server:
const server = $server.start({
port: 6060,
path: "assets/website",
handler: () => {
$app.openURL("http://localhost:6060/index.html");
}
});
Stop the web server.
Observer server events:
server.listen({
didStart: server => {
$delay(1, () => {
$app.openURL(`http://localhost:${port}`);
});
},
didConnect: server => {},
didDisconnect: server => {},
didStop: server => {},
didCompleteBonjourRegistration: server => {},
didUpdateNATPortMapping: server => {}
});
Register a request handler:
const handler = {};
// Handler filter
handler.filter = rules => {
const method = rules.method;
const url = rules.url;
// rules.headers, rules.path, rules.query;
return "data"; // default, data, file, multipart, urlencoded
}
// Handler response
handler.response = request => {
const method = request.method;
const url = request.url;
return {
type: "data", // default, data, file, error
props: {
html: "<html><body style='font-size: 300px'>Hello!</body></html>"
// json: {
// "status": 1,
// "values": ["a", "b", "c"]
// }
}
};
}
// Handler async response
handler.asyncResponse = (request, completion) => {
const method = request.method;
const url = request.url;
completion({
type: "data", // default, data, file, error
props: {
html: "<html><body style='font-size: 300px'>Hello!</body></html>"
// json: {
// "status": 1,
// "values": ["a", "b", "c"]
// }
}
});
}
\## handler.filter
filter is a function, it passes rules as the parameter, you should return the type of request for it. Rules:
{
"method": "",
"url": "",
"headers": {
},
"path": "",
"query": {
}
}
Types include: default
, data
, file
, multipart
, urlencoded
.
Starting from v1.51.0, you can also return an object that overrides the original rules:
handler.filter = rules => {
return {
"type": "data",
"method": "GET"
}
}
You probably need this if you want to redirect a request, or change a POST
method to GET
, etc.
Response function passes request as the parameter, you should return a response for it:
{
type: "data",
props: {
html: "<html><body style='font-size: 300px'>Hello!</body></html>"
}
}
Response type can be: default
, data
, file
, it initiates response with different props.
data
response can be created with html
, text
or json
parameter. file
response can be created with path
parameter.
Other parameters:
Prop | Type | Description |
---|---|---|
contentType | string | content type |
contentLength | number | content length |
statusCode | number | status code |
cacheControlMaxAge | number | cache control max age |
lastModifiedDate | Date | last modified date |
eTag | string | E-Tag |
gzipEnabled | bool | gzip enabled |
headers | object | HTTP headers |
Remove all registered handlers.
For detailed example, refer to: https://github.com/cyanzhong/xTeko/blob/master/extension-demos/server.js
Request and Response are complicated objects, please take a look Detailed Docs.
JSBox provides WebSocket like interfaces, it creates socket connection between client and server.
Create a new socket connection:
const socket = $socket.new("wss://echo.websocket.org");
You can specify some parameters:
const socket = $socket.new({
url: "wss://echo.websocket.org",
protocols: [],
allowsUntrustedSSLCertificates: true
});
Observe socket events:
socket.listen({
didOpen: (sock) => {
console.log("Websocket Connected");
},
didFail: (sock, error) => {
console.error(`:( Websocket Failed With Error: ${error}`);
},
didClose: (sock, code, reason, wasClean) => {
console.log("WebSocket closed");
},
didReceiveString: (sock, string) => {
console.log(`Received: ${string}`);
},
didReceiveData: (sock, data) => {
console.log(`Received: ${data}`);
},
didReceivePing: (sock, data) => {
console.log("WebSocket received ping");
},
didReceivePong: (sock, data) => {
console.log("WebSocket received pong");
}
});
Open WebSocket.
Close WebSocket.
socket.close({
code: 1000, // Optional, see: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
reason: "reason", // Optional
});
Send content:
const object = socket.send("Message");
const result = object.result;
const error = object.error;
You can also use data:
socket.send({
data,
noCopy: true, // Optional
});
const object = socket.ping(data);
const result = object.result;
const error = object.error;
Get ready state:
const readyState = socket.readyState;
// 0: connecting, 1: open, 2: closing, 3: closed
For detailed example: https://github.com/cyanzhong/xTeko/blob/master/extension-demos/socket.js
calendarItem
is used on $calendar
APIs:
Prop | Type | Read/Write | Description |
---|---|---|---|
title | string | rw | title |
location | string | rw | location |
notes | string | rw | notes |
url | string | rw | url |
allDay | bool | rw | is all day event |
startDate | date | rw | start date |
endDate | date | rw | end date |
status | number | r | status |
eventIdentifier | string | r | event identifier |
creationDate | date | r | creation date |
modifiedDate | date | r | last modified date |
color
represents a color, it can be generated by $color(hex)
:
const color = $color("#00eeee");
Returns hex code of a color:
const hexCode = color.hexCode;
// -> "#00eeee"
Returns RGB values of a color:
const components = color.components;
const red = components.red;
const green = components.green;
const blue = components.blue;
const alpha = components.alpha;
contact
is used on $contact
APIs:
Prop | Type | Read/Write | Description |
---|---|---|---|
identifier | string | r | identifier |
content | string | r | content |
contactType | number | r | type |
namePrefix | string | rw | name prefix |
givenName | string | rw | given name |
middleName | string | rw | middle name |
familyName | string | rw | family name |
nameSuffix | string | rw | name suffix |
nickname | string | rw | nick name |
organizationName | string | rw | organization name |
departmentName | string | rw | department name |
jobTitle | string | rw | job title |
note | string | rw | note |
imageData | data | rw | avatar image data |
phoneNumbers | array | rw | phone numbers |
emailAddresses | array | rw | email addresses |
postalAddresses | array | rw | postal addresses |
urlAddresses | array | rw | url addresses |
instantMessageAddresses | array | rw | instant message addresses |
group
is object that returned from $contact.fetchGroup
:
Prop | Type | Read/Write | Description |
---|---|---|---|
identifier | string | r | identifier |
name | string | rw | name |
data
means a binary file:
Prop | Type | Read/Write | Description |
---|---|---|---|
info | object | r | metadata |
string | string | r | convert to utf-8 string |
byteArray | [number] | r | convert to byte array |
image | image | r | convert to image object |
fileName | string | r | possible file name |
gzipped | $data | r | get gzipped data |
gunzipped | $data | r | get gunzipped data |
isGzipped | bool | r | check whether is a gzip file |
error
indicates an error message:
Prop | Type | Read/Write | Description |
---|---|---|---|
domain | string | r | domain |
code | number | r | code |
userInfo | object | r | user info |
localizedDescription | string | r | localized description |
localizedFailureReason | string | r | localized failure reason |
localizedRecoverySuggestion | string | r | localized recovery suggestion |
image
means a bitmap in memory:
Prop | Type | Read/Write | Description |
---|---|---|---|
size | $size | r | size |
orientation | number | r | orientation |
info | object | r | metadata |
scale | number | r | scale |
png | data | r | png format data |
Returns a new image with the template
rendering image, it can be used with tintColor
to change the image color:
{
type: "image",
props: {
tintColor: $color("red"),
image: rawImage.alwaysTemplate
}
}
The ahove rawImage
is the original image you have.
It's similar to alwaysTemplate
, but it returns an image with the original
rendering mode, tintColor
will be ignored.
Returns a resized image:
const resized = image.resized($size(100, 100));
Returns a data with jpeg format, the number means compress quality (0 ~ 1):
const jpg = image.jpg(0.8);
Get color at a pixel:
const color = image.colorAtPixel($point(0, 0));
const hexCode = color.hexCode;
Get average color of image:
const avgColor = image.averageColor;
Get orientation fixed image:
const fixedImage = image.orientationFixedImage;
In list
or matrix
controls, indexPath indicates the position of an item:
Prop | Type | Read/Write | Description |
---|---|---|---|
section | number | rw | section |
row | number | rw | row |
item | number | rw | equals to row , usually use on matrix |
navigationAction
is used on web
views:
Prop | Type | Read/Write | Description |
---|---|---|---|
type | number | r | type |
sourceURL | string | r | source url |
targetURL | string | r | target url |
requestURL | string | r | request url |
reminderItem
is used on $reminder
APIs:
Prop | Type | Read/Write | Description |
---|---|---|---|
title | string | rw | title |
location | string | rw | location |
notes | string | rw | notes |
url | string | rw | url |
priority | number | rw | priority |
completed | bool | rw | is completed |
completionDate | date | r | completion date |
alarmDate | date | rw | alarm date |
creationDate | date | r | creation date |
modifiedDate | date | r | last modified date |
response
is the response object when we handle http requests:
$http.get({
url: "",
handler: function(resp) {
const response = resp.response;
}
})
Prop | Type | Read/Write | Description |
---|---|---|---|
url | string | r | url |
MIMEType | string | r | MIME type |
expectedContentLength | number | r | expected content length (bytes) |
textEncodingName | string | r | text encoding |
suggestedFilename | string | r | suggested file name |
statusCode | number | r | HTTP status code |
headers | object | r | HTTP header |
ResultSet
is returned from a SQLite query:
db.query("SELECT * FROM USER", (rs, err) => {
while (rs.next()) {
}
rs.close();
});
Prop | Type | Read/Write | Description |
---|---|---|---|
query | string | r | SQL Query |
columnCount | number | r | column count |
values | object | r | all values |
Move to next result.
Close result set.
Get index for a column name.
Get name for a column index.
Get value for a name or index.
Check whether null for a name or index.
Prop | Type | Read/Write | Description |
---|---|---|---|
method | string | r | http method |
url | string | r | url |
headers | json | r | http headers |
path | string | r | path |
query | json | r | query |
contentType | string | r | Content-Type |
contentLength | number | r | Content-Length |
ifModifiedSince | date | r | If-Modified-Since |
ifNoneMatch | bool | r | If-None-Match |
acceptsGzip | bool | r | accepts Gzip encoding |
localAddressData | data | r | local address data |
localAddress | string | r | local address string |
remoteAddressData | data | r | remote address data |
remoteAddress | string | r | remote address string |
hasBody | bool | r | has body |
hasByteRange | bool | r | has byte range |
data request contains all properties like request, these are special:
Prop | Type | Read/Write | Description |
---|---|---|---|
data | data | r | data |
text | string | r | text |
json | json | r | json |
file request contains all properties like request, these are special:
Prop | Type | Read/Write | Description |
---|---|---|---|
temporaryPath | string | r | temporary file path |
multipart request contains all properties like request, these are special:
Prop | Type | Read/Write | Description |
---|---|---|---|
arguments | array | r | arguments |
files | array | r | files |
mimeType | string | r | MIME Type |
arguments properties:
Prop | Type | Read/Write | Description |
---|---|---|---|
controlName | string | r | control name |
contentType | string | r | Content-Type |
mimeType | string | r | MIME Type |
data | data | r | data |
string | string | r | string |
fileName | string | r | file name |
temporaryPath | string | r | temporary file path |
response is returned from handler.response:
Prop | Type | Read/Write | Description |
---|---|---|---|
redirect | string | rw | redirect url |
permanent | bool | rw | permanent |
statusCode | number | rw | http status code |
contentType | string | rw | Content-Type |
contentLength | string | rw | Content-Length |
cacheControlMaxAge | number | rw | Cache-Control |
lastModifiedDate | date | rw | Last-Modified |
eTag | string | rw | ETag |
gzipEnabled | bool | rw | gzip enabled |
hasBody | bool | rw | has body |
You can create a response like:
return {
type: "default",
props: {
statusCode: 404
}
}
data response contains all properties like response, these are special:
Prop | Type | Read/Write | Description |
---|---|---|---|
text | string | rw | text |
html | string | rw | html |
json | json | rw | json |
file response contains all properties like response, these are special:
Prop | Type | Read/Write | Description |
---|---|---|---|
path | string | rw | file path |
isAttachment | bool | rw | is attachment |
byteRange | range | rw | byte range |
In order to make your life easier, JSBox has bundled some famous JavaScript modules:
These modules can be used with require
directly, for instance:
const lodash = require("lodash");
Please refer to their documentation, we don't want to be too verbose here.
If you want to use some other modules, you can bundle them with browserify. Generally, CommonJS modules that don't rely on native code are available.
There are tons of ways to install scripts:
If your desktop device is macOS, you can use AirDrop as the most cool way.
PS: All solutions above support js/zip/box
formats.
When you share a package, there're 3 options:
Share folder to desktop makes you easier to modify, it will be cool to work with VSCode.
Zip files are more likely being used on SNS, box files are basically zip files with .box
path extensions.
Starting from v1.9.0
, JSBox supports both JavaScript file and package format.
There're so many advantages:
JSBox Package is actually a zip file, any zip file including these structure will be considered as a package:
Here's an example: https://github.com/cyanzhong/xTeko/tree/master/extension-demos/package
By the way, you can also generate a package template inside JSBox app.
Put the resource files of your app here, you can refer them by path like assets/filename
.
These files could be used in $file
related APIs, also it can be a src
for image and button.
There's an icon.png
inside by default, the icon of this app, you replace it with your own design.
For the spec of icon, please refer to: https://github.com/cyanzhong/xTeko/tree/master/extension-icons
Put script files here, you can require them by syntax like require('./scripts/utility')
, we will introduce modules soon.
You can also create sub-folders here, with this said you can manage your scripts gracefully.
This folder is designed for providing localizable strings, in other words, provide resource for $l10n
:
The pattern of a string file:
"OK" = "好的";
"DONE" = "完成";
"HELLO_WORLD" = "你好,世界!";
PS: If you're using strings
files and $app.strings
at the same time, the later one will be used.
Currently this file including 2 parts, the info
node represents metadata of the app, refer: $addin.
The settings
node provides some settings like $app
provided formerly: $app
We will add more settings here in near future.
main.js is the main entrance of an app, we can load other scripts here:
const app = require('./scripts/app');
app.sayHello();
We are gonna to talk about how it works.
require
is designed for modularizing a script, with its own namespace we can expose functions and variables correctly.
You can easily expose APIs with module.exports
:
function sayHello() {
$ui.alert($l10n('HELLO_WORLD'));
}
module.exports = {
sayHello
}
With the name sayHello
, the function sayHello will be accessible to other scripts.
Here's how to require:
const app = require('./scripts/app');
app.sayHello();
This mechanism separates all logic into multiple files, without naming conflicts.
Note: please use relative as you can, auto-completion system doesn't work with absolute path.
Another way is $include
, but this is totally different, this function just simply copy the code from another file, and execute it like eval
.
With 2 solutions above, we can design our architecture better comparing to single file.
Both absolute paths and relative paths are supported:
const app = require('./scripts/app');
In require function, .js
can be omitted, in above case it represents scripts/app.js
.
You can use this pattern to access all folders inside current package.
How to access files:
const file = $file.read('strings/zh-Hans.strings');
This code opens the file in strings/zh-Hans.strings
.
In short, in package mode, the root path is the package folder.
We provided VSCode for JSBox from very early version: https://marketplace.visualstudio.com/items?itemName=Ying.jsbox
It improves your experience to writing code for JSBox, and also provides real-time syncing for JSBox.
Starting from v0.0.6, JSBox can upload package to your device, as long as the folder looks like:
It archives the folder, upload to your iOS device, and run it automatically.
Here is a list to show you promise interfaces that supported by JSBox, we are using required
to indicate the callback must be handled, which means you can use promise directly. And optional
means the callback can be omitted, as a result you need to set async: true
to use Promise style.
API | Type |
---|---|
main | required |
background | required |
API | Type |
---|---|
request | required |
get | required |
post | required |
download | required |
upload | required |
shorten | required |
lengthen | required |
startServer | required |
API | Type |
---|---|
schedule | optional |
API | Type |
---|---|
save | required |
open | required |
API | Type |
---|---|
setAsync | required |
getAsync | required |
removeAsync | required |
clearAsync | required |
API | Type |
---|---|
take | required |
pick | required |
save | optional |
fetch | required |
delete | optional |
API | Type |
---|---|
text | required |
speech | required |
API | Type |
---|---|
animate | optional |
alert | required |
action | required |
menu | required |
API | Type |
---|---|
sms | optional |
optional |
API | Type |
---|---|
fetch | required |
create | optional |
save | optional |
delete | optional |
API | Type |
---|---|
fetch | required |
create | optional |
save | optional |
delete | optional |
API | Type |
---|---|
pick | required |
fetch | required |
create | optional |
save | optional |
delete | optional |
fetchGroups | required |
addGroup | optional |
deleteGroup | optional |
updateGroup | optional |
addToGroup | optional |
removeFromGroup | optional |
API | Type |
---|---|
select | required |
API | Type |
---|---|
connect | required |
API | Type |
---|---|
analysis | required |
tokenize | required |
htmlToMarkdown | required |
API | Type |
---|---|
scan | required |
API | Type |
---|---|
make | required |
API | Type |
---|---|
open | optional |
API | Type |
---|---|
open | optional |
API | Type |
---|---|
zip | required |
unzip | required |
API | Type |
---|---|
exec | required |
API | Type |
---|---|
date | required |
data | required |
color | required |
API | Type |
---|---|
getAllText | required |
Promise is an elegant solution to resolve callback hell, please refer to Mozilla's doc to have a better understanding: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
Of course you can use Promise in JSBox, but previously APIs that we provided don't support Promise style, for example:
$http.get({
url: 'https://docs.xteko.com',
handler: function(resp) {
const data = resp.data;
}
})
You have to specify a handler to take next action, but since v1.15.0, you have better solution:
$http.get({ url: 'https://docs.xteko.com' }).then(resp => {
const data = resp.data;
})
This can save you from callback hell, or even better (>= iOS 10.3):
var resp = await $http.get({ url: 'https://docs.xteko.com' })
var data = resp.data
Or, you can use a very handy shortcut:
var resp = await $http.get('https://docs.xteko.com')
var data = resp.data
There are so many details in Promise, we don't want to go deep at here, but we recommend you this article: https://javascript.info/async
Overall, we provide Promise style for JSBox APIs.
We have 2 different ways to handle an asynchronous function:
async: true
to indicate it's Promise callThat's really easy to understand, in short, in some cases like:
$ui.menu({
items: ["A", "B", "C"],
handler: (title, index) => {
}
})
If you popup a menu, it doesn't make sense to not handle it, so we treat this as a must be handled
API, you can directly:
var result = await $ui.menu({ items: ["A", "B", "C"] })
var title = result.title
var index = result.index
// Or: var result = await $ui.menu(["A", "B", "C"])
But in some other cases, the callback could be omitted, you can simply do nothing:
$photo.delete({
count: 3,
screenshot: true,
handler: success => {
// It's OK to remove handler here
}
})
In that case you need to specify async mode to use Promise:
var success = await $photo.delete({
count: 3,
screenshot: true,
async: true
})
If you don't do that, we will treat this as a normal call instead of Promise.
Also, not all APIs are async calls, some APIs are total sync call, for example:
const success = $file.delete("sample.txt");
There's no need (and can't) to provide Promise API for that, we will have a checklist for each API soon.
Once you use Promise, you may realize there is only a parameter, so it's not needed to wrap it as JSON:
var resp = await $http.get('https://docs.xteko.com')
Remember these shortcuts can save your time:
// Thread
await $thread.main(3)
await $thread.background(3)
// HTTP
var resp = await $http.get('https://docs.xteko.com')
var result = await $http.shorten("http://docs.xteko.com")
// UIKit
var result = await $ui.menu(["A", "B", "C"])
// Cache
var cache = await $cache.getAsync("key")
JavaScript
is a powerful and flexible programming language, to read this document, we suppose you have basic knowledge of JavaScript.
Resources
JSBox is not alone, there are so many products can run scripts
, JSBox is heavily inspired from them, for example Editorial
and Automator
, they can run script too.
But JSBox is not just a copycat of those projects, we based on JavaScript
and provided a lot of simple APIs, let you interact with iOS much easier.
By the way, there're so many frameworks provide ability to: render native UI with JavaScript
, for example React Native
from Facebook and Weex
from Alibaba, JSBox didn't leverage them, there are several reasons:
Keep it simple and easy to learn
The only thing you need to know is JavaScript, you don't need to understand what's MVVM, what's React...
$
, e.g. $clipboard
Cocoa
, JSBox API looks extremely short, because write code on mobile device is not easyJavaScript Object
, unless it's only one parameterAbove is the brief introduction, I'm familiar with JavaScript, can't wait to see Sample >.
Introduce API design patterns with some simple examples, helps you have a general understanding
// Show alert
$ui.alert("Hello, World!")
// Log to console
console.info("Hello, World!")
$ui.preview({
text: JSON.stringify($clipboard.items)
})
$http.get({
url: 'https://docs.xteko.com',
handler: function(resp) {
const data = resp.data;
}
})
$ui.render({
views: [
{
type: "button",
props: {
title: "Button"
},
layout: function(make, view) {
make.center.equalTo(view.super)
make.width.equalTo(64)
},
events: {
tapped: function(sender) {
$ui.toast("Tapped")
}
}
}
]
})
Just a simple tour, explanation is coming soon.
Block is a special type in Objective-C, it's very similar to closure in other languages. It works like a function, and it can capture variable outside the block, it can be a variable, a parameter, or even a return value.
Teach you Blocks isn't the goal of our document, you can read more at here: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithBlocks/WorkingwithBlocks.html
Let's see how to implement Blocks in JSBox.
In JSBox you can use $block to declare a block, for example:
const handler = $block("void, UITableViewRowAction *, NSIndexPath *", (action, indexPath) => {
$ui.alert("Action")
});
That means you need to declare all types (including return value and all parameters) with a string, and pass a function as the block body.
If you implement this block in Objective-C, it would be:
void(^handler)(UITableViewRowAction *, NSIndexPath *) = ^(UITableViewRowAction *action, NSIndexPath *indexPath) {
// Alert
};
Please don't forget the return value, otherwise it can't be recognized correctly.
Here's an example to create a TableView with only Runtime code:
//-- Create window --//
$ui.render()
//-- Cell --//
$define({
type: "TableCell: UITableViewCell"
})
//-- TableView --//
$define({
type: "TableView: UITableView",
events: {
"init": function() {
self = self.invoke("super.init")
self.invoke("setTableFooterView", $objc("UIView").invoke("new"))
self.invoke("registerClass:forCellReuseIdentifier:", $objc("TableCell").invoke("class"), "identifier")
return self
}
}
})
//-- Manager --//
$define({
type: "Manager: NSObject <UITableViewDelegate, UITableViewDataSource>",
events: {
"tableView:numberOfRowsInSection:": function(tableView, section) {
return 5
},
"tableView:cellForRowAtIndexPath:": function(tableView, indexPath) {
const cell = tableView.invoke("dequeueReusableCellWithIdentifier:forIndexPath:", "identifier", indexPath);
cell.invoke("textLabel").invoke("setText", `Row: ${indexPath.invoke("row")}`)
return cell
},
"tableView:didSelectRowAtIndexPath:": function(tableView, indexPath) {
tableView.invoke("deselectRowAtIndexPath:animated:", indexPath, true)
const cell = tableView.invoke("cellForRowAtIndexPath:", indexPath);
const text = cell.invoke("textLabel.text").jsValue();
$ui.alert(`Tapped: ${text}`)
},
"tableView:editActionsForRowAtIndexPath:": function(tableView, indexPath) {
const handler = $block("void, UITableViewRowAction *, NSIndexPath *", (action, indexPath) => {
$ui.alert("Action")
});
const action = $objc("UITableViewRowAction").invoke("rowActionWithStyle:title:handler:", 1, "Foobar", handler);
return [action]
}
}
})
const window = $ui.window.ocValue();
const manager = $objc("Manager").invoke("new");
const table = $objc("TableView").invoke("new");
table.invoke("setFrame", window.invoke("bounds"))
table.invoke("setDelegate", manager)
table.invoke("setDataSource", manager)
window.invoke("addSubview", table)
In JSBox, call C functions is also possible, you can include a C function like:
$defc("NSClassFromString", "Class, NSString *")
The first parameter is name of C function, the second parameter is a type list that describes all parameters.
After that you can use it:
NSClassFromString("NSURL")
Also, for a C function like: int func(void *ptr, NSObject *obj, float num)
you need to:
$defc("func", "int, void *, NSObject *, float")
Note: struct and block are not supported yet.
We can create Objective-C class in JSBox at runtime
Here is an example:
$define({
type: "MyHelper: NSObject",
events: {
instanceMethod: function() {
$ui.alert("instance")
},
"indexPathForRow:inSection:": function(row, section) {
$ui.alert(`row: ${row}, section: ${section}`)
}
},
classEvents: {
classMethod: function() {
$ui.alert("class")
}
}
})
There are basically 3 parts:
type
The class nameevents
All instance methodsclassEvents
All class methodsNow you can use it this way:
$objc("MyHelper").invoke("alloc.init").invoke("instanceMethod")
$objc("MyHelper").invoke("classMethod")
It works like a native class.
In Runtime code, we often create delegate instances, you can do that with $delegate function:
const textView = $objc("UITextView").$new();
textView.$setDelegate($delegate({
type: "UITextViewDelegate",
events: {
"textViewDidChange:": sender => {
console.log(sender.$text().jsValue());
}
}
}));
It creates an anonymous delegate instance automatically, with the similar parameters like $define.
Objective-C Runtime is a powerful ability in iOS, it looks like reflection system in other languages, but it's more flexible.
Base on runtime we could do a lot:
Overall, runtime can do a lot, is the most important feature of Objective-C.
The main purpose is provide backup for defective APIs, so please don't use it in most cases.
You should consider runtime only if you have no better choice.
If a JSBox API is not strong enough, you can consider use Runtime APIs, but a better way would be send a feedback to us.
$objc(className)
Get a class dynamicallyinvoke(methodName, arguments ...)
Call a method dynamically$define(object)
Create a classjsValue()
Convert an Objective-C value to JavaScript valueocValue()
Convert a JavaScript value to Objective-C value$objc_retain(object)
retain object$objc_release(object)
release objectThis example creates a button on screen, tap it to open WeChat:
$define({
type: "MyHelper",
classEvents: {
open: function(scheme) {
const url = $objc("NSURL").invoke("URLWithString", scheme);
$objc("UIApplication").invoke("sharedApplication.openURL", url)
}
}
})
$ui.render({
views: [
{
type: "button",
props: {
bgcolor: $objc("UIColor").invoke("blackColor").jsValue(),
titleColor: $color("#FFFFFF").ocValue().jsValue(),
title: "WeChat"
},
layout: function(make, view) {
make.center.equalTo(view.super)
make.size.equalTo($size(100, 32))
},
events: {
tapped: function(sender) {
$objc("MyHelper").invoke("open", "weixin://")
}
}
}
]
})
const window = $ui.window.ocValue();
const label = $objc("UILabel").invoke("alloc.init");
label.invoke("setTextAlignment", 1)
label.invoke("setText", "Runtime")
label.invoke("setFrame", { x: $device.info.screen.width * 0.5 - 50, y: 240, width: 100, height: 32 })
window.invoke("addSubview", label)
You can learn how to handle values from this example.
Here's a complicated example, it shows you how to create a game (2048) with Runtime: https://github.com/cyanzhong/xTeko/tree/master/extension-scripts/2048
Call Objective-C APIs directly using runtime APIs
We could get an Objective-C object, and call its method:
const label = $objc("UILabel").invoke("alloc.init");
label.invoke("setText", "Runtime")
It creates a label, and sets the text to Runtime
.
Methods are chainable, so invoke("alloc.init")
equals to invoke("alloc").invoke("init")
.
If you want to import many Objective-C classes in one go, here is the solution:
$objc("UIColor, UIApplication, NSIndexPath");
const color = UIColor.$redColor();
const application = UIApplication.$sharedApplication();
Invoke a method that has multiple parameters (Objective-C):
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
In JSBox runtime, we could do:
const indexPath = $objc("NSIndexPath").invoke("indexPathForRow:inSection:", 0, 0);
indexPathForRow:inSection:
is a selector (function) in Objective-C, the following are parameters.
Sometimes, values generated by Runtime will be released by system automatically, in order to avoid that, you could manage reference yourself:
const manager = $objc("Manager").invoke("new");
$objc_retain(manager)
It maintains the object until script stopped.
Release a Runtime object manually:
$objc_release(manager)
Runtime is running code in another language, it's kind of like reflection, so you will feel tired to write code like:
const app = $objc("UIApplication").invoke("sharedApplication");
const url = $objc("NSURL").invoke("URLWithString", "https://sspai.com");
app.invoke("openURL", url);
So we need a syntactic sugar to avoid this complex, since v1.24.0, we have new syntax:
const app = $objc("UIApplication").$sharedApplication();
const url = $objc("NSURL").$URLWithString("https://sspai.com");
app.$openURL(url);
You can write less code and achieve the same effect, and it looks more like nature code.
This syntax has very simple rules:
$
(dollar sign) to announce this is a runtime method_
(dash sign) to replace :
in Objective-C method__
(2 dash signs) to replace _
in Objective-C methodFor instance:
const app = $objc("UIApplication").$sharedApplication();
app.$sendAction_to_from_forEvent(action, target, null, null);
This works exactly the same as this Objective-C code:
UIApplication *app = [UIApplication sharedApplication];
[app sendAction:action to:target from:nil forEvent:nil];
For more detailed example, you can take a look this game (2048): https://github.com/cyanzhong/xTeko/tree/master/extension-scripts/%242048
Some classes are used very frequently, so we import it automatically for you:
Class | Refer |
---|---|
NSDictionary | https://developer.apple.com/documentation/foundation/nsdictionary |
NSMutableDictionary | https://developer.apple.com/documentation/foundation/nsmutabledictionary |
NSArray | https://developer.apple.com/documentation/foundation/nsarray |
NSMutableArray | https://developer.apple.com/documentation/foundation/nsmutablearray |
NSSet | https://developer.apple.com/documentation/foundation/nsset |
NSMutableSet | https://developer.apple.com/documentation/foundation/nsmutableset |
NSString | https://developer.apple.com/documentation/foundation/nsstring |
NSMutableString | https://developer.apple.com/documentation/foundation/nsmutablestring |
NSData | https://developer.apple.com/documentation/foundation/nsdata |
NSMutableData | https://developer.apple.com/documentation/foundation/nsmutabledata |
NSNumber | https://developer.apple.com/documentation/foundation/nsnumber |
NSURL | https://developer.apple.com/documentation/foundation/nsurl |
NSEnumerator | https://developer.apple.com/documentation/foundation/nsenumerator |
These classes can be used directly without declare:
const url = NSURL.$URLWithString("https://sspai.com");
Other classes you need to declare with $objc, you can use the return value:
const appClass = $objc("UIApplication");
var app = appClass.$sharedApplication();
// Or
var app = UIApplication.$sharedApplication();
In short, $objc("UIApplication") also creates a variable named UIApplication
, it's very convenient if you want to use it many times.
Convert values between runtime and native environment
Note:
Therefore, there are 2 methods provided to convert them:
jsValue()
Objective-C value to JavaScript valueocValue()
JavaScript value to Objective-C valueThis is a JavaScript value:
const color1 = $color("red");
If we want to use this in runtime APIs, we need to concert it first: color1.ocValue()
.
And this is a Objective-C value:
const color2 = $objc("UIColor").invoke("grayColor");
If we want to use this in JSBox APIs, we also need to convert it:
props: {
bgcolor: color2.jsValue()
}
Through these two methods, we can handle all values between Runtime APIs and JSBox APIs.
On iOS 15 and above, JSBox offers full-fledged support for Safari Extensions, you can customize your Safari using JavaScript, scripts can be run automatically or manually.
For more information on how to use Safari extensions, please refer to the official Apple documentation mentioned above, we are focusing on extension development here.
Unlike JSBox scripts, Safari extensions run in the browser environment and scripts can use all web APIs, but not JSBox-specific APIs.
For development documentation related to this, please refer to Mozilla's Web APIs documentation.
You can test Safari extensions in a desktop browser or in an in-app WebView environment, but testing in a real Safari environment is still a necessary step.
Safari tools are designed to provide extensions to Safari that require users to manually select to run them, creating tools:
To create Safari tools using VS Code, make the file name to end with .safari-tool.js
, so that JSBox will recognize it as a Safari tool.
Example code:
const video = document.querySelector("video");
if (video) {
video.webkitSetPresentationMode("picture-in-picture");
} else {
alert("No videos found.");
}
This tool turns videos on the YouTube website into Picture in Picture (PiP) mode.
Safari rules are designed to provide Safari with plugins that run automatically, without the user having to manually select to run them, creating rules:
To create Safari rules using VS Code, make the file name to end with .safari-rule.js
, so that JSBox will recognize it as a Safari rule.
Example code:
const style = document.createElement("style");
const head = document.head || document.getElementsByTagName("head")[0];
head.appendChild(style);
style.appendChild(document.createTextNode("._9AhH0 { display: none }"));
When this rule is enabled, users will be able to download images on the Instagram website.
For security reasons, Safari rules are disabled by default and need to be turned on manually by users in the app settings.
Manage calendar items in Calendar.app
Fetch calendar items (request access first):
$calendar.fetch({
startDate: new Date(),
hours: 3 * 24,
handler: function(resp) {
const events = resp.events;
}
})
Fetch all calendar items from now to 3 days later, we could either use hours
or endDate
.
The structure of an object in results is:
Prop | Type | Read/Write | Description |
---|---|---|---|
title | string | rw | title |
identifier | string | r | id |
location | string | rw | location |
notes | string | rw | notes |
url | string | rw | url |
modifiedDate | date | r | last modified date |
creationDate | date | r | creation date |
allDay | boolean | r | is all day event |
startDate | date | r | start date |
endDate | date | r | end date |
status | number | r | Refer |
Create a calendar item:
$calendar.create({
title: "Hey!",
startDate: new Date(),
hours: 3,
notes: "Hello, World!",
handler: function(resp) {
}
})
Save modified calendar item:
$calendar.fetch({
startDate: new Date(),
hours: 3 * 24,
handler: function(resp) {
const event = resp.events[0];
event.title = "Modified"
$calendar.save({
event
})
}
})
Delete a calendar item:
$calendar.delete({
event,
handler: function(resp) {
}
})
Manage contact items in Contacts.app
Pick one or multiple contact:
$contact.pick({
multi: false,
handler: function(contact) {
}
})
Please set multi
to true
if you want to pick up multiple items.
Fetch contacts with keywords:
$contact.fetch({
key: "Ying",
handler: function(contacts) {
}
})
The returned contacts is a list, please refer https://developer.apple.com/documentation/contacts/cncontact to understand the structure.
You can also query all contacts that in a group:
$contact.fetch({
group,
handler: function(contacts) {
}
})
Create contact item:
$contact.create({
givenName: "Ying",
familyName: "Zhong",
phoneNumbers: {
"Home": "18000000000",
"Office": "88888888"
},
emails: {
"Home": "log.e@qq.com"
},
handler: function(resp) {
}
})
Save modified contact item:
$contact.save({
contact,
handler: function(resp) {
}
})
Delete contacts:
$contact.delete({
contacts: contacts
handler: function(resp) {
}
})
Fetch all groups:
var groups = await $contact.fetchGroups();
console.log("name: " + groups[0].name);
Create a new group with name:
var group = await $contact.addGroup({"name": "Group Name"});
Delete a group:
var groups = await $contact.fetchGroups();
$contact.deleteGroup(groups[0]);
Save updated group:
var group = await $contact.fetchGroups()[0];
group.name = "New Name";
$contact.updateGroup(group);
Add a contact to a group:
$contact.addToGroup({
contact,
group
});
Remove a contact from a group:
$contact.removeFromGroup({
contact,
group
});
In this part we are going to introduce some native SDKs to you.
With these APIs, we can talk to iOS native APIs directly.
Send text message or mail using native interface.
Manage calendar items.
Manage reminder items.
Manage your contacts.
Fetch/Track user location.
Track motion data from sensors.
Schedule local push notifications.
Open website in safari
Above is a brief introduction, examples are coming soon.
Fetch/Track GPS information easily
Check whether location service is available:
let available = $location.available;
Fetch location:
$location.fetch({
handler: function(resp) {
const lat = resp.lat;
const lng = resp.lng;
const alt = resp.alt;
}
})
Track user location updates:
$location.startUpdates({
handler: function(resp) {
const lat = resp.lat;
const lng = resp.lng;
const alt = resp.alt;
}
})
Track heading data (compass):
$location.trackHeading({
handler: function(resp) {
const magneticHeading = resp.magneticHeading;
const trueHeading = resp.trueHeading;
const headingAccuracy = resp.headingAccuracy;
const x = resp.x;
const y = resp.y;
const z = resp.z;
}
})
Stop updates.
Select a location from iOS built-in Map:
$location.select({
handler: function(result) {
const lat = result.lat;
const lng = result.lng;
}
})
Get the current location, similar to $location.fetch but uses async await.
const location = await $location.get();
Generate a snapshot image:
const loc = await $location.get();
const lat = loc.lat;
const lng = loc.lng;
const snapshot = await $location.snapshot({
region: {
lat,
lng,
// distance: 10000
},
// size: $size(256, 256),
// showsPin: false,
// style: 0 (0: unspecified, 1: light, 2: dark)
});
Send text messages and mails
Here is an example:
$http.download({
url: "https://images.apple.com/v/iphone/compare/f/images/compare/compare_iphone7_jetblack_large_2x.jpg",
handler: function(resp) {
$message.sms({
recipients: ["18688888888", "10010"],
body: "Message body",
subject: "Message subject",
attachments: [resp.data],
handler: function(result) {
}
})
}
})
Param | Description |
---|---|
recipients | receivers |
body | body |
subject | subject |
attachments | attachments (files) |
result | 0: cancelled 1: succeeded 2: failed |
Here is an example:
$http.download({
url: "https://images.apple.com/v/iphone/compare/f/images/compare/compare_iphone7_jetblack_large_2x.jpg",
handler: function(resp) {
$message.mail({
subject: "Message subject",
to: ["18688888888", "10010"],
cc: [],
bcc: [],
body: "Message body",
attachments: [resp.data],
handler: function(result) {
}
})
}
})
Param | Description |
---|---|
subject | subject |
to | receiver |
cc | cc |
bcc | bcc |
body | body |
isHTML | is body an HTML |
attachments | attachments (files) |
result | 0: cancelled 1: succeeded 2: failed |
Track sensor data updates
Start updates:
$motion.startUpdates({
interval: 0.1,
handler: function(resp) {
}
})
Learn more about the data CMDeviceMotion.
Stop updates.
Manage reminder items in Reminders.app
Fetch reminder items (request access first):
$reminder.fetch({
startDate: new Date(),
hours: 2 * 24,
handler: function(resp) {
const events = resp.events;
}
})
It looks very similar to $calendar
APIs, the object structure:
Prop | Type | Read/Write | Description |
---|---|---|---|
title | string | rw | title |
identifier | string | r | id |
location | string | rw | location |
notes | string | rw | notes |
url | string | rw | url |
modifiedDate | date | r | last modified date |
creationDate | date | r | creation date |
completed | boolean | rw | is completed |
completionDate | date | r | completion date |
alarmDate | date | rw | alarm date |
priority | number | rw | priority (1 ~ 9) |
Create a reminder item:
$reminder.create({
title: "Hey!",
alarmDate: new Date(),
notes: "Hello, World!",
url: "https://apple.com",
handler: function(resp) {
}
})
We could have alarm by set alarmDate
or alarmDates
.
Similar to $calendar.save
:
$reminder.save({
event,
handler: function(resp) {
}
})
Delete a calendar item:
$reminder.delete({
event,
handler: function(resp) {
}
})
You may notice that
$reminder
is very similar to$calendar
, you're right, they have same structure in iOS.APIs related to Safari or Safari View Controller
Open website with Safari View Controller:
$safari.open({
url: "https://www.apple.com",
entersReader: true,
height: 360,
handler: function() {
}
})
entersReader
: enters reader if available, optional.
height
: the height when it runs on widget, optional.
handler
: callback to handle dismiss event.
Above 3 parameters are optional.
Get items in Safari when you are using Action Extension:
const items = $safari.items; // JSON format
Deprecated on iOS 15, use Safari Extension instead.
Inject JavaScript code to Safari when you are using Action Extension:
$safari.inject("window.location.href = 'https://apple.com';")
The action extension will be closed, and the JavaScript will be executed on Safari.
More useful examples: https://github.com/cyanzhong/xTeko/tree/master/extension-scripts/safari-extensions
Add item to Safari reading list:
$safari.addReadingItem({
url: "https://sspai.com",
title: "Title", // Optional
preview: "Preview text" // Optional
})
It's very easy to run JSBox script in Siri/Shortcuts app, you can send a request and return a result for it.
All you need to do is use $intents.finish()
like:
var result = await $http.get("");
$intents.finish(result.data);
You can do it asynchronously, and finish it with $intents.finish()
.
If you want to run code synchronously, you can simply ignore $intents.finish()
, the shortcut will be finished automatically.
By default, the view height is 320, you can change it by:
$intents.height = 180;
If you are using Shortcuts app on iOS 13 or above, you can specify two arguments: Name
and Parameters
.
In the Shortcuts app, you will see Parameters
is a Text, but please fill it with a Shortcuts Dictionary.
JSBox will decode the Dictionary to a JSON, and you can get it with $context.query
:
const query = $context.query;
Also, the result in $intents.finish(result)
will be passed to the next Shortcuts action.
Shortcuts is the redesigned Workflow, here's an introduction that helps you understand: https://www.macrumors.com/2018/07/09/hands-on-with-ios-12-shortcuts-app/
In short, there isn't many differences between Workflow and Shortcuts, but Shortcuts has better support of Siri, and it can be improved with 3rd-party apps.
When we talk about "Shortcuts", the meaning has two parts:
For now, JSBox supports Shortcuts as below:
JSBox can also donate JavaScript ability to Shortcuts app, when you handle complicated data or logic in Shortcuts, it's cool to leverage JavaScript.
Since Shortcuts doesn't support passing data between 3rd-party apps, we can use clipboard as a workaround:
In short, JSBox reads JavaScript code from clipboard, evaluate it and set the result back to clipboard.
For async task, we need to finish actions with $intents.finish
like:
const a = "Hello";
const b = "World";
const result = [a, b].join(", ");
$intents.finish(result);
You can install this Shortcut to understand how it works: Run JavaScript.
You can present views that provided by JSBox script on Siri, it looks no differences compare to other JSBox scripts that shows a view:
$ui.render({
views: [
{
type: "label",
props: {
text: "Hey, Siri!"
},
layout: function(make, view) {
make.center.equalTo(view.super);
}
}
]
});
This code shows a Hey, Siri!
on Siri's view (You can add it to Siri/Shortcuts in script's setting view).
If you are using Shortcuts app on iOS 13 or above, you can specify two arguments: Name
and Parameters
.
In the Shortcuts app, you will see Parameters
is a Text, but please fill it with a Shortcuts Dictionary.
JSBox will decode the Dictionary to a JSON, and you can get it with $context.query
:
const query = $context.query;
You can launch JSBox scripts with Siri voice command, it can execute code or present views.
Do it with these two ways:
Other than file or cache system, in JSBox you can also use SQLite as persistence layer, SQLite is a lightweight database which is very good for mobile. For more information you can refer to: https://www.sqlite.org/.
FMDB is an Objective-C wrapper for SQLite, JSBox uses this library and provides a wrapper on a higher level, so you can interact SQLite with JavaScript. That means, you can also call SQLite APIs with Runtime if necessary, but in most cases JSBox APIs are good enough.
For better user experience, we also provide a lightweight SQLite browser, you get it for free in file explorer, it can preview SQLite files with a decent user interface.
It is read-only for now, we will add more functionalities in the future.
Don't use a SQLite connection in multiple threads, if necessary, use Queue instead:
const queue = $sqlite.dbQueue("test.db");
// Operations
queue.operations(db => {
db.update();
db.query();
//...
});
// Transaction
queue.transaction(db => {
db.update();
db.query();
//...
const rollback = errorOccured;
return rollback;
});
queue.close();
For a SQLite connection, beginTransaction()
starts a transaction.
For a SQLite connection, commit()
commits a transaction.
For a SQLite connection, rollback()
rollbacks a transaction.
Open a SQLite connection with file path:
const db = $sqlite.open("test.db");
Close a SQLite connection:
const db = $sqlite.open("test.db");
//...
$sqlite.close(db); // Or db.close();
Here's how to execute update:
db.update("CREATE TABLE User(name text, age integer)");
// Return: { result: true, error: error }
You can also use placeholders and arguments:
db.update({
sql: "INSERT INTO User values(?, ?)",
args: ["Cyan", 28]
});
PS: never concatenate strings as sql, use placeholders and values forever.
Let's see how to execute query:
db.query("SELECT * FROM User", (rs, err) => {
while (rs.next()) {
const values = rs.values;
const name = rs.get("name"); // Or rs.get(0);
}
rs.close();
});
Result set supports functions as below:
const columnCount = rs.columnCount; // Column count
const columnName = rs.nameForIndex(0); // Column name
const columnIndex = rs.indexForName("age"); // Column index
const query = rs.query; // SQL Query
Please take a look at Result Set for details.
It's similar to update when you have arguments:
db.query({
sql: "SELECT * FROM User where age = ?",
args: [28]
}, (rs, err) => {
});
session object provides script executing operations, properties:
Prop | Type | Description |
---|---|---|
session | session | session |
bufferSize | number | buffer size |
type | number | type |
lastResponse | string | last response |
requestPty | bool | request pty |
ptyTerminalType | number | pty terminal type |
environmentVariables | json | environment variables |
Execute script:
channel.execute({
script: "ls -l /var/lib/",
timeout: 0,
handler: function(result) {
console.log(`response: ${result.response}`)
console.log(`error: ${result.error}`)
}
})
Execute command:
channel.write({
command: "",
timeout: 0,
handler: function(result) {
console.log(`success: ${result.success}`)
console.log(`error: ${result.error}`)
}
})
Upload local file to remote:
channel.upload({
path: "resources/notes.md",
dest: "/home/user/notes.md",
handler: function(success) {
console.log(`success: ${success}`)
}
})
Download remote file to local:
channel.download({
path: "/home/user/notes.md",
dest: "resources/notes.md",
handler: function(success) {
console.log(`success: ${success}`)
}
})
Since v1.14.0 JSBox provides SSH abilities, you can connect to your server with very simple JavaScript, here is an example which can help you have a brief understanding: ssh-example.
Here are basically 3 things that related to SSH:
With these APIs, you can execute scripts or upload/download files.
JSBox's SSH based on NMSSH, so you can refer to MNSSH's documentation to understand the design concept.
Connect to server with password or keys, you can execute a script at the same time:
$ssh.connect({
host: "",
port: 22,
username: "",
public_key: "",
private_key: "",
// password: "",
script: "ls -l /var/lib/",
handler: function(session, response) {
console.log(`connect: ${session.connected}`)
console.log(`authorized: ${session.authorized}`)
console.log(`response: ${response}`)
}
})
This example uses public_key
and private_key
for authentication, you can put your keys in app bundle, just like ssh-example does.
The result contains a session instance and a response string, here is how session looks like:
Prop | Type | Description |
---|---|---|
host | string | host |
port | number | port |
username | string | user name |
timeout | number | timeout |
lastError | error | last error |
fingerprintHash | string | fingerprint hash |
banner | string | banner |
remoteBanner | string | remote banner |
connected | bool | is connected |
authorized | bool | is authorized |
channel | channel | channel instance |
sftp | sftp | sftp instance |
We can do many taks through channel
and sftp
object.
Disconnect all SSH sessions connected by JSBox:
$ssh.disconnect()
sftp instance in session object provides file related APIs, object properties:
Prop | Type | Description |
---|---|---|
session | session | session |
bufferSize | number | buffer size |
connected | bool | is connected |
Create SFTP connection:
await sftp.connect();
Move a file:
sftp.moveItem({
src: "/home/user/notes.md",
dest: "/home/user/notes-new.md",
handler: function(success) {
}
})
Check whether a directory exists:
sftp.directoryExists({
path: "/home/user/notes.md",
handler: function(exists) {
}
})
Create a directory:
sftp.createDirectory({
path: "/home/user/folder",
handler: function(success) {
}
})
Delete a directory:
sftp.removeDirectory({
path: "/home/user/folder",
handler: function(success) {
}
})
List all files in a directory:
sftp.contentsOfDirectory({
path: "/home/user/folder",
handler: function(contents) {
}
})
Get file information:
sftp.infoForFile({
path: "/home/user/notes.md",
handler: function(file) {
}
})
Object properties:
Prop | Type | Description |
---|---|---|
filename | string | file name |
isDirectory | bool | is directory |
modificationDate | date | modification date |
lastAccess | date | last access date |
fileSize | number | file size |
ownerUserID | number | owner user id |
ownerGroupID | number | owner group id |
permissions | string | permissions |
flags | number | flags |
Check whether a file exists:
sftp.fileExists({
path: "/home/user/notes.md",
handler: function(exists) {
}
})
Create symbolic link:
sftp.createSymbolicLink({
path: "/home/user/notes.md",
dest: "/home/user/notes-symbolic.md",
handler: function(success) {
}
})
Delete a file:
sftp.removeFile({
path: "/home/user/notes.md",
handler: function(success) {
}
})
Get a file (binary):
sftp.contents({
path: "/home/user/notes.md",
handler: function(file) {
}
})
Write file (binary) to remote:
sftp.write({
file,
path: "/home/user/notes.md",
progress: function(sent) {
// Optional: determine whether is finished here
return sent > 1024 * 1024
},
handler: function(success) {
}
})
Append file (binary) to remote:
sftp.append({
file,
path: "/home/user/notes.md",
handler: function(success) {
}
})
Copy a file:
sftp.copy({
path: "/home/user/notes.md",
dest: "/home/user/notes-copy.md",
progress: function(copied, totalBytes) {
// Optional: determine whether is finished here
return sent > 1024 * 1024
},
handler: function(success) {
}
})
Note: handler could be implemented with async/await.
iOS animation is awesome, JSBox provides two ways to implement animations
The most simple way is use $ui.animate
:
$ui.animate({
duration: 0.4,
animation: function() {
$("label").alpha = 0
},
completion: function() {
$("label").remove()
}
})
This code changes alpha value of the label to 0 in 0.4 seconds, then remove this view.
Yes, you just need to put your code inside animation
, everything works like magic.
You can create a spring animation
by set:
Param | Type | Description |
---|---|---|
delay | number | delay |
damping | number | damping |
velocity | number | initial value |
options | number | Refer |
We also introduced a chainable API based on JHChainableAnimations:
$("label").animator.makeBackground($color("red")).easeIn.animate(0.5)
We could do same effect with a much cleaner syntax, just use an animator.
For more details, please refer to the official docs of JHChainableAnimations.
It is no longer recommended, because that project is not being maintained actively
You can provide Context Menus for any views, sub menus and SF Symbols are supported as well.
Here's a minimal example:
$ui.render({
views: [
{
type: "button",
props: {
title: "Long Press!",
menu: {
title: "Context Menu",
items: [
{
title: "Title",
handler: sender => {}
}
]
}
},
layout: (make, view) => {
make.center.equalTo(view.super);
make.size.equalTo($size(120, 36));
}
}
]
});
Other than just title, you can also set up an icon with the symbol
property:
{
title: "Title",
symbol: "paperplane",
handler: sender => {}
}
For more information about SF Symbols, you can use the Apple official app: https://developer.apple.com/design/downloads/SF-Symbols.dmg or this JSBox script: https://xteko.com/install?id=141
For important actions that may be dangerous, you can use the destructive style:
{
title: "Title",
destructive: true,
handler: sender => {}
}
When a menu item has items
property, it becomes a sub-menu:
{
title: "Sub Menu",
items: [
{
title: "Item 1",
handler: sender => {}
},
{
title: "Item 2",
handler: sender => {}
}
]
}
This sub-menu has two sub-items, sub-menus can be nested.
The above sub-menu items will be collapsed as secondary items, you can also display them directly, with some separators, just use the inline
property:
{
title: "Sub Menu",
inline: true,
items: [
{
title: "Item",
handler: sender => {}
}
]
}
For an element in list
or matrix
, an indexPath
will be provided in the handler
function:
$ui.render({
views: [
{
type: "list",
props: {
data: ["A", "B", "C"],
menu: {
title: "Context Menu",
items: [
{
title: "Action 1",
handler: (sender, indexPath) => {}
}
]
}
},
layout: $layout.fill
}
]
});
It's very similar to event: didSelect
in each case, the only difference is how to trigger the action.
As one of the most important changes in iOS 14, you can add Pull-Down menus to button
and navButtons
. They won't blur the background, also can be triggered as primary action.
To support Pull-Down menus for button
, simply add a pullDown: true
parameter to parameters mentioned above:
$ui.render({
views: [
{
type: "button",
props: {
title: "Long Press!",
menu: {
title: "Context Menu",
pullDown: true,
asPrimary: true,
items: [
{
title: "Title",
handler: sender => {}
}
]
}
},
layout: (make, view) => {
make.center.equalTo(view.super);
make.size.equalTo($size(120, 36));
}
}
]
});
To add Pull-Down menus to navButtons
, simply use the menu
parameter:
$ui.render({
props: {
navButtons: [
{
title: "Title",
symbol: "checkmark.seal",
menu: {
title: "Context Menu",
asPrimary: true,
items: [
{
title: "Title",
handler: sender => {}
}
]
}
}
]
}
});
Note that, the parameter asPrimary
indicates whether its a primary action.
The latest JSBox version has supported Dark Mode, not only for the app itself, but also provided some easy-to-use APIs for creating dark-mode-perfect scripts.
This chapter talks about how does it work, and how to make your scripts Dark Mode ready.
theme
specifies the appearance preference, its possible values are light
/ dark
/ auto
, stand for light mode, dark mode and system default.
It can be a global setting like this:
$app.theme = "auto";
Alternatively, uses the value in the config.json file:
{
"settings": {
"theme": "auto"
}
}
Other than set that globally, you can also set a certain value for each screens, in its props
:
$ui.push({
props: {
"theme": "light"
}
});
We can also override theme for a certain view like this:
$ui.render({
views: [
{
type: "view",
props: {
"theme": "light"
}
}
]
});
Note, to avoid breaking your existing scripts, the default value of
theme
would belight
. If you'd like to adapt dark mode, turn it toauto
, then adjust your colors.
Default controls have different colors under different themes, please refer to the latest UIKit-Catalog demo for more information.
In order support different colors for light and dark, you can now create dynamic colors like this:
const dynamicColor = $color({
light: "#FFFFFF",
dark: "#000000"
});
It can also be simplified as:
const dynamicColor = $color("#FFFFFF", "#000000");
Colors can be nested, it can use colors generated by the $rgba(...)
method:
const dynamicColor = $color($rgba(0, 0, 0, 1), $rgba(255, 255, 255, 1));
Besides, if you want to provide colors for the pure black theme, you can do:
const dynamicColor = $color({
light: "#FFFFFF",
dark: "#141414",
black: "#000000"
});
Dynamic colors show different colors in light mode or dark mode, no need to observe theme changes and switch them manually.
Also, there are some semantic colors, you can use them directly.
Similarly, you may need to provide dynamic images for light or dark mode, like this:
const dynamicImage = $image({
light: "light-image.png",
dark: "dark-image.png"
});
This image chooses different resources for light and dark mode, it switches automatically, can be simplified as:
const dynamicImage = $image("light-image.png", "dark-image.png");
Besides, images can also be nested, such as:
const lightImage = $image("light-image.png");
const darkImage = $image("dark-image.png");
const dynamicImage = $image(lightImage, darkImage);
Note, this doesn't work for SF Symbols and remote resources. For SF Symbols, please provide dynamic color for
tintColor
to achieve that
Theme changes will emit a themeChanged
event for all views:
$ui.render({
views: [
{
type: "view",
layout: $layout.fill,
events: {
themeChanged: (sender, isDarkMode) => {
// Update UI if needed
}
}
}
]
});
This provides a chance to change some UI details dynamically, such as changing its alpha
value, or changing its borderColor
(it doesn't support dynamic colors).
In iOS 13 and above, type: "blur" supports more styles. Some of those styles are designed for both light mode and dark mode, you can use $blurStyle for that.
Please refer to Apple's documentation for more information.
WebView has its own mechanism for Dark Mode, please refer to WebKit docs for more information.
Just a tip: for WebViews in JSBox, you need to set props: opaque
to false
, this can avoid the whitescreen for the initial loading.
In general, there are three things you should do:
theme
to auto
$color(light, dark)
and $image(light, dark)
to create dynamic assetsthemeChanged
to update UI detailsIn order to demonstrate how it works clearly, we prepared an example project for you: https://github.com/cyanzhong/jsbox-dark-mode
As the mechanism continues to improve, we may provide more APIs in the future, which should make your life easier. Besides, the default value of theme
is just a temporary solution for the transition period, it might be auto
in the future, to make scripts that only use default controls support dark mode by default.
Starts from v1.49.0, we provide better event handling strategy for views, action can be connected after view is created. All iOS built-in control events are supported.
Single tap action is triggered:
button.whenTapped(() => {
});
Double tap action is triggered:
button.whenDoubleTapped(() => {
});
Customizable touch event is triggered:
button.whenTouched({
touches: 2,
taps: 2,
handler: () => {
}
});
Above code will be executed for two fingers quickly tap twice.
If the button is a Runtime object (ocValue), here are two solutions:
button.jsValue().whenTapped(() => {
});
Or, use $block instead:
button.$whenTapped($block("void, void", () => {
}));
Add custom event handlers:
textField.addEventHandler({
events: $UIEvent.editingChanged,
handler: sender => {
}
});
Note that: this only available on components like button
, text
, input
, since they are UI controls, but it won't work on image
like components. Full list of UI events can be found here: $UIEvent
Remove existing event handlers:
textField.removeEventHandlers($UIEvent.editingChanged);
Of course, above code can be used in Runtime environment:
textField.$addEventHandler({
events: $UIEvent.editingChanged,
handler: $block("void, id", sender => {
})
});
Gesture recognization on mobile is the most important innovation in last 10 years!
Let's see Steve introduces the 1st generation iPhone: https://www.youtube.com/watch?v=x7qPAY9JqE4.
iOS supports various gestures, such as tap, pinch, pan etc, currently JSBox supports few of them.
Triggers when tap gesture received:
tapped: function(sender) {
}
Triggers when long press gesture received:
longPressed: function(info) {
let sender = info.sender;
let location = info.location;
}
Triggers when double tap gesture received:
doubleTapped: function(sender) {
}
When touch event is triggered:
touchesBegan: function(sender, location, locations) {
}
When touch is moving:
touchesMoved: function(sender, location, locations) {
}
When touch event finished:
touchesEnded: function(sender, location, locations) {
}
When touch event cancelled:
touchesCancelled: function(sender, location, locations) {
}
In JSBox, build user interface is very easy, the only thing you need to provide is a JavaScript object, we actually based on UIKit, but it's much easier, works like magic.
Unlike React Native and Weex, you don't need to know HTML
and CSS
, JavaScript is enough to create delightful UI.
PS: Your can run without any user interface, absolutely.
Like we mentioned before, we can create a button with following code:
$ui.render({
views: [
{
type: "button",
props: {
title: "Button"
},
layout: function(make, view) {
make.center.equalTo(view.super)
make.width.equalTo(64)
},
events: {
tapped: function(sender) {
$ui.toast("Tapped")
}
}
}
]
})
This is very small but included all concept we needed to create UI: type
, props
, layout
and events
.
To specify view's type, such as button
and label
, they are work different.
It means properties, or attributes of a view. For example title
in button, different view has different props, we will explain all of them later.
In short, layout is used to determine the location and size of a view.
JSBox uses Auto Layout as layout system, you don't need to fully understand how it works, because we provided a solution based on a third party framework: Masonry.
Masonry is very easy to use, we have several examples later.
PS: we are considering support more layout systems in the future, for example Flexbox.
Provide events here, for example tapped
in button, we will show different events supported by different views soon.
It's an array to provided its subviews (children):
{
views: [
]
}
Yes, a view system is actually works like a tree, trees have theirs children.
Layout is a series of constraints to determine the size and position of views
Layout is very important in a UI system, different platform has different design, for example Auto Layout in iOS, and Flexbox which is used widely in web applications.
In short: size & position.
PS: This part is a little bit complicated, but it is also very important.
In the early iOS, due to the limitation of screen, iOS only provides frame based layout. It's based on absolute position and size, so you can set frame
in props
as well (not recommend).
Because frame layout is clumsy:
Use auto layout as much as you can, as an iOS developer I will tell you auto layout is much better than frame layout.
When you are using frame layout, you can use events->layoutSubview to retrieve super view's frame:
$ui.render({
views: [
{
type: "view",
layout: $layout.fill,
events: {
layoutSubviews: function(view) {
console.log(`frame: ${JSON.stringify(view.frame)}`)
}
}
}
]
})
We are trying to describe auto layout with most simple words:
Create some constraints in a view tree, to describe relationship between each views, for example super view, children and siblings
Constraints need to be complete, for example:
We don't know the width of this view, but this view's layout is complete.
No matter how the screen looks, we can always have a view, the relationship is clear enough:
layout: function(make) {
make.height.equalTo(40)
make.left.top.right.inset(10)
}
You can use view
itself by:
layout: function(make, view) {
make.height.equalTo(40)
make.width.equalTo(view.height)
}
In case you want to get the width/height, or view.super
.
Basically it is a value to describe a relationship of a property.
The property could be top/bottom/left/right, the relationship could be equalTo/offset etc.
In short, constraint is property and relationship combined.
Like we mentioned before, height
and left
are relationship, here are more examples, you can refer: Masonry to check out all of them.
Prop | Description |
---|---|
width | width |
height | height |
size | size |
center | center |
centerX | x center |
centerY | y center |
left | left side |
top | top side |
right | right side |
bottom | bottom side |
leading | leadin |
trailing | trailing |
edges | 4 edges |
Please note sometimes we need to support RTL (Right to Left) languages, consider use leading/trailing in that case.
Describe relationship between two views, such as equalTo
, offset
:
Relation | Description |
---|---|
equalTo(object) | == |
greaterThanOrEqualTo(object) | >= |
lessThanOrEqualTo(object) | <= |
offset(number) | offset by |
inset(number) | inset by |
insets($insets) | edge insets |
multipliedBy(number) | multiplied by |
dividedBy(number) | divided by |
priority(number) | set priority |
We can create constraints with chainable syntax:
layout: function(make) {
make.left.equalTo($("label").right).offset(10)
make.bottom.inset(0)
make.size.equalTo($size(40, 40))
}
This view is 40x40pt, align to the bottom of its super view, and at the right of label
with a small offset.
We have to know, layout is not an easy task, there's no best solution for all situations. Auto Layout has its disadvantages too.
However, understand concept above is enough to handle most cases, please check documents when you need:
This document is still under construction, we will provide more examples soon.
Sometimes, auto resizing is enough for you, you can use view's flex
property:
$ui.render({
views: [
{
type: "view",
props: {
bgcolor: $color("red"),
frame: $rect(0, 0, 0, 100),
flex: "W"
}
}
]
});
This creates a view which has the same width as its parent, and the height is 100. The flex
property is a string, acceptable characters are:
L
: UIViewAutoresizingFlexibleLeftMarginW
: UIViewAutoresizingFlexibleWidthR
: UIViewAutoresizingFlexibleRightMarginT
: UIViewAutoresizingFlexibleTopMarginH
: UIViewAutoresizingFlexibleHeightB
: UIViewAutoresizingFlexibleBottomMarginFor example, using "LRTB" to describe UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin
.
In the UI system of JSBox, there are still something we need to talk
Pop out the most front view from the stack.
Pop to the root view.
Get a view instance by id, same as $(id)
.
You can also get view by type, only if this type is distinct.
Present an alert, usually used to show a message or options:
$ui.alert({
title: "Hello",
message: "World",
actions: [
{
title: "OK",
disabled: false, // Optional
style: $alertActionType.default, // Optional
handler: function() {
}
},
{
title: "Cancel",
style: $alertActionType.destructive, // Optional
handler: function() {
}
}
]
})
You can create an alert with title, message and actions, if no actions
is provided, a default OK
will be shown.
title
and message
are optional, and the minimal implementation is:
$ui.alert("Haha!")
Yes, you could simply provide a string, it will work also.
Present an action sheet, all parameters are same as $ui.alert
.
PS: This control can only be used in main application, don't use it in app extensions.
Present a menu, it can be used to provide a series of options:
$ui.menu({
items: ["A", "B", "C"],
handler: function(title, idx) {
},
finished: function(cancelled) {
}
})
Present a popover, provides two different styles.
Style 1, it can be filled with some simple options (string array):
const {index, title} = await $ui.popover({
sourceView: sender,
sourceRect: sender.bounds, // default
directions: $popoverDirection.up, // default
size: $size(320, 200), // fits content by default
items: ["Option A", "Option B"],
dismissed: () => {},
});
In this way, fill options with items
property, it returns a Promise.
Style 2, it can be filled with custom views
:
const popover = $ui.popover({
sourceView: sender,
sourceRect: sender.bounds, // default
directions: $popoverDirection.any, // default
size: $size(320, 200), // fits screen width by default
views: [
{
type: "button",
props: {
title: "Button"
},
layout: (make, view) => {
make.center.equalTo(view.super);
make.size.equalTo($size(100, 36));
},
events: {
tapped: () => {
popover.dismiss();
}
}
}
]
});
Create custom UI with views
property, returns the popover itself, you can close it by calling its dismiss
method.
The sourceView
and sourceRect
specifies where to present the popover, and sourceRect
default to sourceView.bounds, directions
defines the permitted arrow directions.
Please refer to the demo project we provided for more information: https://gist.github.com/cyanzhong/313b2c8d138691233658f1b8a52f02c6
Show a toast message at top of the root view.
$ui.toast("Hey!")
There is an optional parameter to set the stay seconds:
$ui.toast("Hey!", 10)
This toast will be dismissed after 10 seconds.
You can clear toast with $ui.clearToast();
Similar to toast
, but the bar color is green, indicates success:
$ui.success("Done");
Similar to toast
, but the bar color is yellow, indicates warning:
$ui.warning("Be careful!");
Similar to toast
, but the bar color is red, indicates error:
$ui.error("Something went wrong!");
Show a loading indicator:
$ui.loading(true)
You can also display a message here:
$ui.loading("Loading...")
Display a progress bar, the range of number is [0, 1]:
$ui.progress(0.5)
Also, an optional message is supported:
$ui.progress(0.5, "Downloading...")
Preview a url quickly:
$ui.preview({
title: "URL",
url: "https://images.apple.com/v/ios/what-is/b/images/performance_large.jpg"
})
Param | Type | Description |
---|---|---|
title | string | title |
url | string | url |
html | string | html content |
text | string | text content |
Create a view manually, the object should be a view like $ui.render:
const canvas = $ui.create({
type: "image",
props: {
bgcolor: $color("clear"),
tintColor: $color("gray"),
frame: $rect(0, 0, 100, 100)
}
});
Note that, since there's no super view yet, you cannot use layout
method at that moment.
Instead, you should do it after the view is added to its super:
const subview = $ui.create(...);
superview.add(subview);
subview.layout((make, view) => {
});
Get current window of $ui.render.
Get the most front view controller of the app.
Get or set the most front view's title.
Select icon:
var icon = await $ui.selectIcon();
We use render and push to draw views on the screen
Here's how $ui.render
works, create a page:
$ui.render({
props: {
id: "label",
title: "Hello, World!"
},
views: [
{
type: "label",
layout: function(make, view) {
make.center.equalTo(view.super)
make.size.equalTo($size(100, 100))
}
}
]
})
Because root view is also a view, it supports props
as well, and you can set the title of the root view.
Other props:
Prop | Type | Description |
---|---|---|
theme | string | theme: light/dark/auto |
title | string | title |
titleColor | $color | title color |
barColor | $color | bar color |
iconColor | $color | icon color |
debugging | bool | shows view debugging tool |
navBarHidden | bool | whether hide navigation bar |
statusBarHidden | bool | whether hide status bar |
statusBarStyle | number | 0 for black, 1 for white |
fullScreen | bool | whether present as full screen mode |
formSheet | bool | whether present as form sheet (iPad only) |
pageSheet | bool | whether present as page sheet (iOS 13) |
bottomSheet | bool | whether present as bottom sheet (iOS 15) |
modalInPresentation | bool | whether to prevent dismiss gesture |
homeIndicatorHidden | bool | whether hide home indicator for iPhone X series |
clipsToSafeArea | bool | whether clips to safe area |
keyCommands | array | external keyboard commands |
Starting from v1.36.0, you can render a page with $ui.render("main.ux") which is generated from UI editor.
Create a page, everthing is exactly same as $ui.render, but it pushes a new root view above previous view.
That's how native view navigation works, you can use it to implement detail views
logic.
Starting from v1.36.0, you can push a page with $ui.push("detail.ux") which is generated from UI editor.
Get a view with id:
const label = $("label");
If id is not provided, will search with type.
If you provided multiple views with same type, you need to provide id for each view.
Currently, $ui.render and $ui.push support below life cycle callback:
events: {
appeared: function() {
},
disappeared: function() {
},
dealloc: function() {
}
}
As you can imagine, these will be called when page is appeared, disappeared and removed.
You can observe keyboard height changes with:
events: {
keyboardHeightChanged: height => {
}
}
You can detect shake event with:
events: {
shakeDetected: function() {
}
}
Define keys with keyCommands
:
$ui.render({
props: {
keyCommands: [
{
input: "I",
modifiers: 1 << 20,
title: "Discoverability Title",
handler: () => {
console.log("Command+I triggered.");
}
}
]
}
});
modifiers is a mask, values could be:
Type | Value |
---|---|
Caps lock | 1 << 16 |
Shift | 1 << 17 |
Control | 1 << 18 |
Alternate | 1 << 19 |
Command | 1 << 20 |
NumericPad | 1 << 21 |
For instance, (1 << 20 | 1 << 17) stands for hold Command + Shift . |
Explain JSBox view system
view
is the base component of all views:
$ui.render({
views: [
{
type: "view",
props: {
bgcolor: $color("#FF0000")
},
layout: function(make, view) {
make.center.equalTo(view.super)
make.size.equalTo($size(100, 100))
},
events: {
tapped: function(sender) {
}
}
}
]
})
Render a red rectangle on the screen.
Prop | Type | Read/Write | Description |
---|---|---|---|
theme | string | rw | light, dark, auto |
alpha | number | rw | alpha |
bgcolor | $color | rw | background color |
cornerRadius | number | rw | corner radius |
smoothCorners | boolean | rw | use continuous curve for corners |
radius | number | w | corner radius (deprecated, use cornerRadius ) |
smoothRadius | number | w | smooth corner radius (deprecated, use smoothCorners ) |
frame | $rect | rw | frame |
size | $size | rw | size |
center | $point | rw | center |
flex | string | rw | auto resizing flexible mask |
userInteractionEnabled | boolean | rw | user interaction enable |
multipleTouchEnabled | boolean | rw | multiple touch support |
super | view | r | super view |
prev | view | r | previous view |
next | view | r | next view |
window | view | r | window |
views | array | r | subviews |
clipsToBounds | boolean | rw | clip subviews |
opaque | boolean | rw | opaque |
hidden | boolean | rw | hidden |
contentMode | $contentMode | rw | Refer |
tintColor | $color | rw | tint color |
borderWidth | number | rw | border width |
borderColor | $color | rw | border color |
circular | bool | rw | whether a circular shape |
animator | object | r | animator |
snapshot | object | r | create snapshot |
info | object | rw | bind extra info |
intrinsicSize | $size | rw | intrinsic content size |
isAccessibilityElement | bool | rw | whether an accessible element |
accessibilityLabel | string | rw | accessibility label |
accessibilityHint | string | rw | accessibility hint |
accessibilityValue | string | rw | accessibility value |
accessibilityCustomActions | array | rw | accessibility custom actions |
Note: you can't use next
in layout functions, because the view hierarchy hasn't been generated.
Create custom navigation buttons:
$ui.render({
props: {
navButtons: [
{
title: "Title",
image, // Optional
icon: "024", // Or you can use icon name
symbol: "checkmark.seal", // SF symbols are supported
handler: sender => {
$ui.alert("Tapped!")
},
menu: {
title: "Context Menu",
items: [
{
title: "Title",
handler: sender => {}
}
]
} // Pull-Down menu
}
]
}
})
Learn more about Pull-Down menus, refer to Pull-Down Menus.
Other than setting title with title
, you can also change the title view with titleView
:
$ui.render({
props: {
titleView: {
type: "tab",
props: {
bgcolor: $rgb(240, 240, 240),
items: ["A", "B", "C"]
},
events: {
changed: sender => {
console.log(sender.index);
}
}
}
},
views: [
]
});
Trigger layout method manually, arguments are exactly the same as the layout
function in its view definition:
view.layout((make, view) => {
make.left.top.right.equalTo(0);
make.height.equalTo(100);
});
Update a view's layout:
$("label").updateLayout(make => {
make.size.equalTo($size(200, 200))
})
Note that, updateLayout
can only be used for existing constraints, or it won't work.
Similar to updateLayout, but remake costs more performance, try to use update as much as you can.
Add a view to another view's hierarchy, refer $ui.render(object)
to see how to create a view, it can also be a view instance that is created with $ui.create(...)
.
Get a subview with specific identifier.
Remove a view from its super view's hierarchy.
Insert a new view below an existing view:
view.insertBelow(newView, existingView);
Insert a new view above an existing view:
view.insertAbove(newView, existingView);
Insert a new view at a specific index:
view.insertAtIndex(newView, 4);
Move self to super's front:
existingView.moveToFront();
Move self to super's back:
existingView.moveToBack();
Trigger layouting of a view, you can use this during animations.
Layout process will not be done immediately after views are created, calling this can trigger an additional layout loop, to make frame and size available.
Mark a view as needs layout, it will be applied in the next drawing cycle.
Forces layout early before next drawing cycle, can be used with setNeedsLayout
:
view.setNeedsLayout();
view.layoutIfNeeded();
Resize the view to its best size based on the current bounds.
Scale a view (0.0 ~ 1.0):
view.scale(0.5)
Create snapshot with scale:
const image = view.snapshotWithScale(1)
Rotate a view:
view.rotate(Math.PI)
ready
event is supported for all views, it will be called when view is ready:
ready: function(sender) {
}
Observe tap gesture:
tapped: function(sender) {
}
The sender is the source of event, usually means the view itself.
You can also use this syntax:
tapped(sender) {
}
This has same effect.
Detect tap events from Apple Pencil:
pencilTapped: function(info) {
var action = info.action; // 0: Ignore, 1: Switch Eraser, 2: Switch Previous, 3: Show Color Palette
var enabled = info.enabled; // whether the system reports double taps on Apple Pencil to your app
}
For iPadOS 13.4 (and above) with trackpad, this is called when pointer enters the view:
hoverEntered: sender => {
sender.alpha = 0.5;
}
For iPadOS 13.4 (and above) with trackpad, this is called when pointer exits the view:
hoverExited: sender => {
sender.alpha = 1.0;
}
Detect dark mode changes:
themeChanged: (sender, isDarkMode) => {
}
Refer Component to see how to use other controls.
The memory and user interaction are very limited on iOS today widget.
Therefore, not all scripts can be executed on widget correctly.
Here is a list for unavailable APIs:
API | Description |
---|---|
$ui.action | Present action sheet |
$ui.preview | Preview content |
$message.* | Send message |
$photo.take | Take photo |
$photo.pick | Pick photo |
$photo.prompt | Ask user to get a photo |
$share.sheet | Present share sheet |
$text.lookup | Lookup in dictionary |
$picker.* | Present pickers |
$qrcode.scan | Scan QRCode |
$input.speech | Speech to text |
Please be careful when you are using above APIs.
When you want to get input from user, please use $input.text
instead of input
component.
Although there are some limitations, we still need to interact with users.
Here are some useful APIs you could consider:
API | Description |
---|---|
$ui.alert | Present alert |
$ui.menu | Show popup menu |
$ui.toast | Show toast message |
$ui.loading | Show loading state |
$input.text | Input text |
$widget provides API to interact with widgets.
Get or set the height of widget:
// Get
const height = $widget.height;
// Set
$widget.height = 400;
Get current display mode (show less/show more):
const mode = $widget.mode; // 0: less 1: more
Observe mode changes:
$widget.modeChanged = mode => {
}