JSBox Documentation

Updated 10102022-063910

This website is based on Docsify, hosted on the GitHub, pull requests are welcome.

JSBox APIs

Create powerful addins for JSBox with JavaScript, ES6 is supported, and we provide tons of APIs to interact with iOS directly

JSBox Node.js

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/

How to run code in JSBox

> 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

Open Source

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!

Contact us

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

Privacy Policy

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.

2021.11.26

2020.10.03

2020.03.13

2020.03.04

2020.01.29

2020.01.28

2019.12.28

2019.11.25

2019.11.21

2019.10.27

2019.09.02

2018.12.23

2018.11.09

2018.10.21

2018.10.05

2018.08.26

2018.08.11

2018.07.29

2018.07.22

2018.07.18

2018.07.08

2018.06.27

2018.06.17

2018.06.14

2018.06.08

2018.05.15

2018.04.18

2018.04.07

2018.04.06

2018.04.01

2018.03.25

2018.03.18

2018.03.11

2018.03.09

2018.02.28

2017.07.15

Tab 1

Tab 2

Tab 3

Tab 4

Launch Script from Other Apps

How to Use Code Editor

We can manage our scripts with $addin APIs

$addin.list

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;

$addin.categories

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;

$addin.current

Get current running addin:

const current = $addin.current;

$addin.save(object)

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.

$addin.delete(name)

Delete an installed addin:

$addin.delete("New Script")

A script named New Script will be deleted.

$addin.run(name)

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.

$addin.restart()

Restart current running script.

$addin.replay()

Replay the current running UI script.

$addin.compile(script)

Convert scripts to JSBox syntax:

const result = $addin.compile("$ui.alert('Hey')");

// result => JSBox.ui.alert('Hey')

$addin.eval(script)

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.

type: "blur"

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.

props

Prop Type Read/Write Description
style $blurStyle w effect style

type: "button"

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.

props

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

props: source

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",
  }
}

events: tapped

To support tap event on buttons, implement tapped:

events: {
  tapped: function(sender) {
    
  }
}

Pull-Down Menus

button supports Pull-Down Menus, refer to Pull-Down Menus for usage.

type: "canvas"

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.

ctx

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.

ctx.fillColor

fillColor is the color to fill a rect.

ctx.strokeColor

strokeColor is the color to draw a line.

ctx.font

font is the font to draw texts.

ctx.fontSize

fontSize is the font size when draw texts.

ctx.allowsAntialiasing

allowsAntialiasing allows antialiasing.

saveGState()

Save current state.

restoreGState()

Restore state.

scaleCTM(sx, sy)

Scale CTM.

translateCTM(tx, ty)

Translate CTM.

rotateCTM(scale)

Rotate CTM.

setLineWidth(width)

Set the line width of context.

setLineCap(lineCap)

Set the line cap of context: https://developer.apple.com/documentation/coregraphics/cglinecap

setLineJoin(lineJoin)

Set the line join of context: https://developer.apple.com/documentation/coregraphics/cglinejoin

setMiterLimit(miterLimit)

Set the miter limit of context: https://developer.apple.com/documentation/coregraphics/cgcontext/1456499-setmiterlimit

setAlpha(alpha)

Set the alpha value of context.

beginPath()

Begin a path.

moveToPoint(x, y)

Move a point to (x, y).

addLineToPoint(x, y)

Add a line to point (x, y).

addCurveToPoint(cp1x, cp1y, cp2x, cp2y, x, y)

Add a curve to point (x, y), the curvature is controlled by (cp1x, cp1y) and (cp2x, cp2y).

addQuadCurveToPoint(cpx, cpy, x, y)

Add a curve to point (x, y), the curvature is controlled by (cpx, cpy).

closePath()

Close the path.

addRect(rect)

Add a rect.

addArc(x, y, radius, startAngle, endAngle, clockwise)

Add an arc, centered at (x, y), starts from startAngle, ends with endAngle.

addArcToPoint(x1, y1, x2, y2, radius)

Add an arch between (x1, y1) and (x2, y2).

fillRect(rect)

Fill the rect.

strokeRect(rect)

Stroke the rect.

clearRect(rect)

Clear the rect.

fillPath()

Fille the path.

strokePath()

Stroke the path.

drawPath(mode)

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

drawImage(rect, image)

Draw an image to rect.

drawText(rect, text, attributes)

Draw a text to rect:

ctx.drawText($rect(0, 0, 100, 100), "Hey!", {
  color: $color("red"),
  font: $font(30)
});

type: "chart"

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.

Dynamic Plotting

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.

chart.dispatchAction(args)

Trigger actions:

chart.dispatchAction({
  type: "dataZoom",
  start: 20,
  end: 30
});

chart.getWidth(handler)

Get width of the chart:

let width = await chart.getWidth();

chart.getHeight(handler)

Get height of the chart:

let height = await chart.getHeight();

chart.getOption(handler)

Get options of the chart:

let options = await chart.getOption();

chart.resize($size)

Resize the chart:

chart.resize($size(100, 100));

chart.showLoading()

Shows loading animation:

chart.showLoading();

chart.hideLoading()

Hides loading animation:

chart.hideLoading();

chart.clear()

Clear current chart:

chart.clear();

event: rendered

rendered will be called after rendered:

events: {
  rendered: () => {

  }
}

event: finished

finished will be called when finish:

events: {
  finished: () => {

  }
}

WebView

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

type: "code"

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");
        }
      }
    }
  ]
});

props: language

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"
}

props: theme

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"
}

props: adjustInsets

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.

props: keys

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.

type: "date-picker"

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.

props

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)

events: changed

changed will be called if the date has changed:

changed: function(sender) {
  
}

type: "gallery"

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.

props

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

events

changed will be called when page changes:

changed: function(sender) {
  
}

Retrieve subviews

You can retrieve subviews with methods as below:

const views = $("gallery").itemViews; // All views
const view = $("gallery").viewWithIndex(0); // The first view

Scroll to a page

If you want to scroll to a page with animation, do this:

$("gallery").scrollToPage(index);

type: "gradient"

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

props

Prop Type Read/Write Description
colors array rw colors
locations array rw locations
startPoint $point rw start point
endPoint $point rw end point

type: "image"

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.

props

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

props: source

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",
  }
}

Zoomable

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.

alwaysTemplate

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.

alwaysOriginal

It's similar to alwaysTemplate, but it returns an image with the original rendering mode, tintColor will be ignored.

resized($size)

Get a resized image:

const resizedImage = image.resized($size(60, 60));

resizableImage(args)

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"
});

type: "input"

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.

props

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

focus()

Get the focus, show keyboard.

blur()

Blur the focus, dismiss keyboard.

events: changed

changed will be called when text changed:

changed: function(sender) {

}

events: returned

returned will be called once the return key pressed:

returned: function(sender) {

}

events: didBeginEditing

didBeginEditing will be called after editing began:

didBeginEditing: function(sender) {

}

events: didEndEditing

didEndEditing will be called after editing ended:

didEndEditing: function(sender) {
  
}

Customize keyboard toolbar

You can customize toolbar as below:

$ui.render({
  views: [
    {
      type: "input",
      props: {
        accessoryView: {
          type: "view",
          props: {
            height: 44
          },
          views: [

          ]
        }
      }
    }
  ]
});

Customize keyboard view

You can customize keyboard as below:

$ui.render({
  views: [
    {
      type: "input",
      props: {
        keyboardView: {
          type: "view",
          props: {
            height: 267
          },
          views: [

          ]
        }
      }
    }
  ]
});

$input.text(object)

Instead of create a text field, you could also use $input.text:

$input.text({
  type: $kbType.number,
  placeholder: "Input a number",
  handler: function(text) {

  }
})

$input.speech(object)

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.

type: "label"

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.

props

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

type: "list"

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 & footer

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.

Static cells

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.

props

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

props: actions

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.

object($indexPath)

Returns the row data at indexPath:

const data = tableView.object($indexPath(0, 0));

insert(object)

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(object)

Delete a row at indexPath or index:

// object could be indexPath or index
tableView.delete(0)

cell($indexPath)

Returns the cell at indexPath (might be null):

const cell = tableView.cell($indexPath(0, 0));

events: rowHeight

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
  }
}

events: sectionTitleHeight

We could specify section title height dynamically by making the sectionTitleHeight a function:

sectionTitleHeight: (sender, section) => {
  if (section == 0) {
    return 30;
  } else {
    return 40;
  }
}

events: didSelect

didSelect will be called once a row selected:

didSelect: function(sender, indexPath, data) {

}

events: didLongPress

didLongPress will be called when cell is long pressed:

didLongPress: function(sender, indexPath, data) {

}

events: forEachItem

Iterate all items:

forEachItem: function(view, indexPath) {
  
}

Auto sizing cells

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
    }
  ]
});

Long press rows

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

Paging

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.

setEditing

Set editing state programmatically:

$("list").setEditing(false)

scrollTo

Scroll to a specific indexPath programmatically:

$("list").scrollTo({
  indexPath: $indexPath(0, 0),
  animated: true // Default is true
})

One more thing

List views are subclasses of scroll view, they have all abilities inherited from scroll view.

type: "lottie"

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");

lottie.play(args?)

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 });

lottie.pause()

Pause the animation:

$("lottie").pause();

lottie.stop()

Stop the animation:

$("lottie").stop();

lottie.update()

Force update current frame:

$("lottie").update();

lottie.progressForFrame(frame)

Convert frame to progress:

let progress = $("lottie").progressForFrame(0);

lottie.frameForProgress(progress)

Convert progress to frame:

let frame = $("lottie").frameForProgress(0.5);

props

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

type: "map"

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.

props

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.

type: "markdown"

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

type: "matrix"

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.

props

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 & footer

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.

Dynamic header and footer

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.

object($indexPath)

Returns the item data at indexPath:

const data = matrix.object($indexPath(0, 0));

insert(object)

Insert new data to matrix:

// Either indexPath or index is fine
matrix.insert({
  indexPath: $indexPath(0, 0),
  value: {

  }
})

delete(object)

Delete a item at indexPath or index:

// object could be either indexPath or index
matrix.delete($indexPath(0, 0))

cell($indexPath)

Returns the cell at indexPath:

const cell = matrix.cell($indexPath(0, 0));

events: didSelect

didSelect will be called once item selected:

didSelect: function(sender, indexPath, data) {

}

events: didLongPress

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.

events: forEachItem

Iterate all items:

forEachItem: function(view, indexPath) {
  
}

events: highlighted

Customize highlighted visual:

highlighted: function(view) {

}

events: itemSize

Provide dynamic item size:

itemSize: function(sender, indexPath) {
  var index = indexPath.item + 1;
  return $size(40 * index, 40 * index);
}

scrollTo

Scroll to a specific indexPath programmatically:

$("matrix").scrollTo({
  indexPath: $indexPath(0, 0),
  animated: true // Default to true
})

Auto sizing cells

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
    }
  ]
});

Long press rows

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.

type: "tab" & type: "menu"

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.

type: "picker"

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.

props

Prop Type Read/Write Description
data object r all selected items
selectedRows object r all selected rows

events: changed

changed will be called once the selected item changed:

changed: function(sender) {
  
}

$picker.date(object)

We provided an easy way to popup a date picker with $picker.date(object).

$picker.data(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.

$picker.color(object)

Select a color using the built-in color picker:

const color = await $picker.color({
  // color: aColor
});

type: "progress"

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%.

props

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

type: "runtime"

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.

type: "scroll"

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.

Content Size

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.

props

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

beginRefreshing()

Begin the refreshing animation.

endRefreshing()

End the refreshing animation.

resize()

Resize the content size of itself.

updateZoomScale()

Re-calculate the best scale for zoomable views, you may need to call this after screen rotation changes.

scrollToOffset($point)

Scroll to an offset with animation:

$("scroll").scrollToOffset($point(0, 100));

events: pulled

pulled will be called once user triggered a refresh:

pulled: function(sender) {
  
}

events: didScroll

didScroll will be called while scrolling:

didScroll: function(sender) {

}

events: willBeginDragging

willBeginDragging will be called while dragging:

willBeginDragging: function(sender) {
  
}

events: willEndDragging

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);
}

events: didEndDragging

didEndDragging will be called after dragging ended:

didEndDragging: function(sender, decelerate) {

}

events: willBeginDecelerating

willBeginDecelerating will be called before decelerating:

willBeginDecelerating: function(sender) {

}

events: didEndDecelerating

didEndDecelerating will be called after decelerating ended:

didEndDecelerating: function(sender) {

}

events: didEndScrollingAnimation

didEndScrollingAnimation will be called after decelerating ended (program triggered):

didEndScrollingAnimation: function(sender) {

}

events: didScrollToTop

didScrollToTop will be called once it did reach top:

didScrollToTop: function(sender) {

}

Auto Layout

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

type: "slider"

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.

props

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

events: changed

changed will be called once the value changed:

changed: function(sender) {

}

type: "spinner"

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.

props

Prop Type Read/Write Description
loading boolean rw loading state
color $color rw color
style number rw 0 ~ 2 for styles

start()

Equals to spinner.loading = true.

stop()

Equals to spinner.loading = false.

toggle()

Equals to spinner.loading = !spinner.loading.

type: "stack"

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

Overview

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.

props: axis

The axis property determines the stack’s orientation, either vertically or horizontally.

Possible values:

- $stackViewAxis.horizontal
- $stackViewAxis.vertical

props: distribution

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

props: alignment

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

props: spacing

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

props: isBaselineRelative

The isBaselineRelative property determines whether the vertical spacing between views is measured from the baselines, should be a boolean value.

props: isLayoutMarginsRelative

The isLayoutMarginsRelative property determines whether the stack view lays out its arranged views relative to its layout margins, should be a boolean value.

Change a view dynamically

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

Add a view

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

Remove a view from a stack:

const stackView = $("stackView");
stackView.stack.remove(existingView);

// existingView can be retrieved with id

Insert a view

Insert a new view into a stack, with index:

const stackView = $("stackView");
stackView.stack.insert(newView, 2);

Set spacing after a view

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 spacing after a view

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

Demo

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

type: "stepper"

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.

props

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

events: changed

changed will be called once the value changed:

changed: function(sender) {
  
}

type: "switch"

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.

props

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

events: changed

changed will be called after the state changed:

changed: function(sender) {
  
}

type: "text"

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.

props

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

focus()

Get the focus, show keyboard.

blur()

Blur the focus, dismiss keyboard.

events: didBeginEditing

didBeginEditing will be called after editing began:

didBeginEditing: function(sender) {

}

events: didEndEditing

didEndEditing will be called after editing ended:

didEndEditing: function(sender) {
  
}

events: didChange

didChange will be called once text changed:

didChange: function(sender) {

}

events: didChangeSelection

didChangeSelection will be called once selection changed:

didChangeSelection: function(sender) {

}

text is a subclass of scroll, it works like a scroll view.

styledText

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

Customize keyboard toolbar

You can customize toolbar as below:

$ui.render({
  views: [
    {
      type: "input",
      props: {
        accessoryView: {
          type: "view",
          props: {
            height: 44
          },
          views: [

          ]
        }
      }
    }
  ]
});

Customize keyboard view

You can customize keyboard as below:

$ui.render({
  views: [
    {
      type: "input",
      props: {
        keyboardView: {
          type: "view",
          props: {
            height: 267
          },
          views: [

          ]
        }
      }
    }
  ]
});

type: "video"

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

video.pause()

Pause the video:

$("video").pause()

video.play()

Play the video:

$("video").play()

video.toggle()

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.

type: "web"

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
}

Load local resources

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.

props

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

goBack()

Go back.

goForward()

Go forward.

reload()

Reload current page.

reloadFromOrigin()

Reload from origin.

stopLoading()

Stop loading.

eval(object)

Evaluate JavaScript:

webView.eval({
  script: "var sum = 1 + 2",
  handler: function(result, error) {

  }
})

exec(script)

Similar to eval, but this one is an async function:

const {result, error} = await webView.exec("1 + 1");

events: didClose

didClose will be called after closed:

didClose: function(sender) {

}

events: decideNavigation

decideNavigation can be used to intercept requests:

decideNavigation: function(sender, action) {
  if (action.requestURL === "https://apple.com") {
    return false
  }
  return true
}

events: didStart

didStart will be called after started:

didStart: function(sender, navigation) {

}

events: didReceiveServerRedirect

didReceiveServerRedirect will be called after received server redirect:

didReceiveServerRedirect: function(sender, navigation) {

}

events: didFinish

didFinish will be called after load finished:

didFinish: function(sender, navigation) {

}

events: didFail

didFail will be called after load failed:

didFail: function(sender, navigation, error) {

}

events: didSendRequest

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
}

JavaScript Injection

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.

$notify(event, message)

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()

CSS Injection

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.

webView.notify

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.

JSBox script on action extension

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.

Limitations

Action extension also has some Limitations, not so much like today widget.

For now, there are basically two main restrictions:

$context

We could use $context to fetch external data, that's very important to make an action extension.

Please refer to Method to see more details.

$context.query

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.

$context.text

Returns a text (user shared a text):

const text = $context.text;

$context.textItems

Returns all text items.

$context.link

Returns a link (user shared a link):

const link = $context.link;

$context.linkItems

Returns all link items.

$context.image

Returns an image (user shared an image):

const image = $context.image;

$context.imageItems

Returns all image items.

$context.safari.items

Returns all Safari items.

const items = $context.safari.items;

Returns:

{
  "baseURI": "",
  "source": "",
  "location": "",
  "contentType": "",
  "title": "",
  "selection": {
    "html": "",
    "text": "",
    "style": ""
  }
}

$context.data

Returns a binary data (user shared a file):

const data = $context.data;

$context.dataItems

Returns all binary files.

$context.allItems

Returns all items, ignores the type.

Basically, if we want to get external parameters, use $context APIs.

$context.clear()

Clear all data in the context object, including query and Action Extension objects:

$context.clear();

$context.close()

Close current action extension, the script stops running.

Constant Table

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.

$env

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)
};

$align

iOS text alignment type:

const $align = {
  left: 0,
  center: 1,
  right: 2,
  justified: 3,
  natural: 4
};

$contentMode

iOS view content mode:

const $contentMode = {
  scaleToFill: 0,
  scaleAspectFit: 1,
  scaleAspectFill: 2,
  redraw: 3,
  center: 4,
  top: 5,
  bottom: 6,
  left: 7,
  right: 8,
};

$btnType

iOS button type:

const $btnType = {
  custom: 0,
  system: 1,
  disclosure: 2,
  infoLight: 3,
  infoDark: 4,
  contactAdd: 5,
};

$alertActionType

Alert item style:

const $alertActionType = {
  default: 0, cancel: 1, destructive: 2
};

$zero

Zero values:

const $zero = {
  point: $point(0, 0),
  size: $size(0, 0),
  rect: $rect(0, 0, 0, 0),
  insets: $insets(0, 0, 0, 0)
};

$layout

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)
  }
};

$lineCap

iOS line cap:

const $lineCap = {
  butt: 0,
  round: 1,
  square: 2
};

$lineJoin

iOS line join:

const $lineJoin = {
  miter: 0,
  round: 1,
  bevel: 2
};

$mediaType

UTI Types:

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"
};

$imgPicker

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
  }
};

$kbType

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
};

$assetMedia

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
  }
};

$pageSize

$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
};

$UIEvent

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,
};

$stackViewAxis

Axis values for stack view:

const $stackViewAxis = {
  horizontal: 0,
  vertical: 1,
};

$stackViewDistribution

Distribution values for stack view:

const $stackViewDistribution = {
  fill: 0,
  fillEqually: 1,
  fillProportionally: 2,
  equalSpacing: 3,
  equalCentering: 4,
};

$stackViewAlignment

Alignment values for stack view:

const $stackViewAlignment = {
  fill: 0,
  leading: 1,
  top: 1,
  firstBaseline: 2,
  center: 3,
  trailing: 4,
  bottom: 4,
  lastBaseline: 5,
};

$stackViewSpacing

Spacing values for stack view:

const $stackViewSpacing = {
  useDefault: UIStackViewSpacingUseDefault,
  useSystem: UIStackViewSpacingUseSystem,
};

$popoverDirection

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),
};

$scrollDirection

Scroll directions for matrix views:

const $scrollDirection = {
  vertical: 0,
  horizontal: 1,
};

$blurStyle

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

$widgetFamily

Check layout type of a home screen widget:

const $widgetFamily = {
  small: 0,
  medium: 1,
  large: 2,
  xLarge: 3, // iPadOS 15
};

Data Types

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.

Trade-off

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.

Methods

As we mentioned before, we provided a series of methods.

$rect(x, y, width, height)

Create a rectangle:

const rect = $rect(0, 0, 100, 100);

$size(width, height)

Create a size:

const size = $size(100, 100);

$point(x, y)

Create a point:

const point = $point(0, 0);

$insets(top, left, bottom, right)

Create an edge insets:

const insets = $insets(10, 10, 10, 10);

$color(string)

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"
});

$rgb(red, green, blue)

Create a color with red, green, blue values.

The range of each number is 0 ~ 255:

const color = $rgb(100, 100, 100);

$rgba(red, green, blue, alpha)

Create a color with red, green, blue and alpha channel:

const color = $rgba(100, 100, 100, 0.5);

$font(name, size)

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/

$range(location, length)

Create a range:

const range = $range(0, 10);

$indexPath(section, row)

Create an indexPath, to indicates the section and row:

const indexPath = $indexPath(0, 10);

$data(object)

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]
})

$image(object, scale)

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);

$icon(code, color, size)

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.

Object

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

$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

$desc returns description of an object:

let desc = $desc(object);
console.log(desc);

Console related APIs

Intro

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

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.

How to Use Console

JSBox has builtin archiver/unarchiver module

$archiver.zip(object)

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) {
    
  }
})

$archiver.unzip(object)

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"
});

$browser.exec

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) {

  }
})

Handy shortcut

You can use Promise with super neat style:

var result = await $browser.exec("return 1 + 1;");

How to access variables in native

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.

$detector.date(string)

Retrieve all dates from a string:

const dates = $detector.date("2017.10.10");

$detector.address(string)

Retrieve all addresses from a string:

const addresses = $detector.address("");

$detector.link(string)

Retrieve all links from a string:

const links = $detector.link("http://apple.com hello http://xteko.com");

$detector.phoneNumber(string)

Retrieve all phone numbers from a string:

const phoneNumbers = $detector.phoneNumber("18666666666 hello 18777777777");

$editor

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.

$editor.text

Get or set all text in the code editor:

const text = $editor.text;

$editor.text = "Hey!";

$editor.view

Returns the text view of the current running editor:

const editorView = $editor.view;
editorView.alpha = 0.5;

$editor.selectedRange

Get or set selected range in the code editor:

const range = $editor.selectedRange;

$editor.selectedRange = $(0, 10);

$editor.selectedText

Get or set selected text in the code editor:

const text = $editor.selectedText;

$editor.selectedText = "Hey!";

$editor.hasText

Returns true when the editor has text:

const hasText = $editor.hasText;

$editor.isActive

Check whether the code editor is active:

const isActive = $editor.isActive;

$editor.canUndo

Check whether undo action can be taken:

const canUndo = $editor.canUndo;

$editor.canRedo

Check whether redo action can be taken:

const canRedo = $editor.canRedo;

$editor.save()

Save changes in the current editor:

$editor.save();

$editor.undo()

Perform undo action in the current editor:

$editor.undo();

$editor.redo()

Perform redo action in the current editor:

$editor.redo();

$editor.activate()

Activate the current editor:

$editor.activate()

$editor.deactivate()

Deactivate the current editor:

$editor.deactivate()

$editor.insertText(text)

Insert text into the selected range:

$editor.insertText("Hello");

$editor.deleteBackward()

Remove the character just before the cursor:

$editor.deleteBackward();

$editor.textInRange(range)

Get text in a range:

const text = $editor.textInRange($range(0, 10));

$editor.setTextInRange(text, range)

Set text in a range:

$editor.setTextInRange("Hey!", $range(0, 10));

Extended APIs

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.

$text

Handle text easily, such as base64 encode/decode and much more.

$share

Share media (text/image etc) to social network services.

$qrcode

QRCode related features, such as encode/decode and scan.

$browser

Simulate a browser environment, so you can leverage the ability of BOM and DOM.

$detector

Some functions to handle common data detection easily, similar to regular expressions.

Used to schedule a notification or cancel a scheduled notification

$push.schedule(object)

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.

$push.cancel(object)

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: ""})

$push.clear()

Clear all scheduled notifications (notifications that registered before build 462 will be ignored):

$push.clear()

JSBox has ability to handle QRCode

$qrcode.encode(string)

Encode a string to QRCode image:

const image = $qrcode.encode("https://apple.com");

$qrcode.decode(image)

Decode a string from QRCode image:

const text = $qrcode.decode(image);

$qrcode.scan(function)

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

$share.sheet(object)

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.wechat(object)

Share content to WeChat:

$share.wechat(image)

The object will be detected automatically, either image or text is correct.

$share.qq(object)

Share content to QQ:

$share.qq(image)

The object will be detected automatically, either image or text is correct.

$share.universal(object)

Deprecated, please use $share.sheet instead.

Provides a lot of utility functions to handle text

$text.uuid

Generates a UUID string:

const uuid = $text.uuid;

$text.tokenize(object)

Text tokenize:

$text.tokenize({
  text: "我能吞下玻璃而不伤身体",
  handler: function(results) {

  }
})

$text.analysis(object)

// TODO: Text analysis

$text.lookup(string)

Lookup text in system builtin dictionaries.

$text.lookup("apple")

$text.speech(object)

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

$text.ttsVoices

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

$text.base64Encode(string)

Base64 encode.

$text.base64Decode(string)

Base64 decode.

$text.URLEncode(string)

URL encode.

$text.URLDecode(string)

URL decode.

$text.HTMLEscape(string)

HTML escape.

$text.HTMLUnescape(string)

HTML unescape.

$text.MD5(string)

MD5.

$text.SHA1(string)

SHA1.

$text.SHA256(string)

SHA256.

$text.convertToPinYin(text)

Get Chinese PinYin of a string.

$text.markdownToHtml(text)

Convert Markdown text to HTML text.

$text.htmlToMarkdown(object)

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>");

$text.decodeData(object)

Convert data to a string:

const string = $text.decodeData({
  data: file,
  encoding: 4 // default, refer: https://developer.apple.com/documentation/foundation/nsstringencoding
});

$text.sizeThatFits(object)

Calculate text bounding size dynamically:

const size = $text.sizeThatFits({
  text: "Hello, World",
  width: 320,
  font: $font(20),
  lineSpacing: 15, // Optional
});

$xml

JSBox provides a simple XML/HTML parser, which is very easy to use, it supports xPath and CSS selector for node querying.

$xml.parse(object)

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
});

Document

$xml.parse() returns XML Document object:

let version = doc.version;
let rootElement = doc.rootElement;

Element

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

Element.firstChild(object)

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
});

Element.children(object)

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();

Element.enumerate(object)

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) => {

  }
});

Element.value(object)

Document and Element object can query value with attribute and namespace:

let value = doc.rootElement.value({
  "attribute": "attribute",
  "namespace": "namespace", // Optional
});

Document.definePrefix(object)

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

Design Principle

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.

Shared Folder

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.

iCloud Drive

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.

Inbox

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.

Absolute Paths

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

iCloud Drive

There are three types of operations:

$drive.open

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.

$drive.save

Save a file using Document Picker:

$drive.save({
  data: $data({string: "Hello, World!"}),
  name: "File Name",
  handler: function() {

  }
})

$drive.read

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");

$drive.xxx

Besides, every $file API has an equivalent $drive API:

JSBox provides some APIs to store/fetch files safely

$file.read(path)

Read a file:

const file = $file.read("demo.txt");

$file.download(path) -> Promise

This method ensures a iCloud drive file is downloaded before reading it:

const data = await $file.download("drive://.test.db.icloud");

$file.write(object)

Write a file:

const success = $file.write({
  data: $data({string: "Hello, World!"}),
  path: "demo.txt"
});

$file.delete(path)

Delete a file:

const success = $file.delete("demo.txt");

$file.list(path)

Get all file names in a folder:

const contents = $file.list("download");

$file.copy(object)

Copy a file:

const success = $file.copy({
  src: "demo.txt",
  dst: "download/demo.txt"
});

$file.move(object)

Move a file:

const success = $file.move({
  src: "demo.txt",
  dst: "download/demo.txt"
});

$file.mkdir(path)

Make a directory (folder):

const success = $file.mkdir("download");

$file.exists(path)

Check if a file exists:

const exists = $file.exists("demo.txt");

$file.isDirectory(path)

Checkc if a path is directory:

const isDirectory = $file.isDirectory("download");

$file.merge(args)

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, ...

$file.absolutePath(path)

Returns the absolute path of a relative path:

const absolutePath = $file.absolutePath(path);

$file.rootPath

Returns the root path of the documents folder (in absolute path style):

const rootPath = $file.rootPath;

$file.extensions

Returns all installed scripts:

const extensions = $file.extensions;

shared://

Access shared folder like we mentioned before:

const file = $file.read("shared://demo.txt");

drive://

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

$app.theme

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.

$app.minSDKVer

Set the minimal available version of JSBox:

$app.minSDKVer = "3.1.0"

$app.minOSVer

Set the minimal available version of iOS:

$app.minOSVer = "10.3.3"

PS: Numbers are separated by ., version comparation goes from left to right.

$app.tips(string)

Give user a nice hint, the effect looks like $ui.alert, but only once for an addin's whole life:

$app.tips("Hey!")

$app.info

Returns the info of JSBox itself:

{
  "bundleID": "app.cyan.jsbox",
  "version": "3.0.0",
  "build": "9527",
}

$app.idleTimerDisabled

Disable auto dimming of the screen:

$app.idleTimerDisabled = true

$app.close(delay)

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

$app.isDebugging

Check whether it is debugging:

if ($app.isDebugging) {
  
}

$app.env

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) {

}

$app.widgetIndex

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

$app.autoKeyboardEnabled

Manage scroll views automatically, to avoid the keyboard hides text fields:

$app.autoKeyboardEnabled = true

$app.keyboardToolbarEnabled

Display a toolbar at the top of keyboard:

$app.keyboardToolbarEnabled = true

$app.rotateDisabled

Set to true to disable screen rotating:

$app.rotateDisabled = true

$app.openURL(string)

Open a URL or a URL Scheme, for example open WeChat:

$app.openURL("weixin://")

$app.openBrowser(object)

Open a URL with external browsers:

$app.openBrowser({
  type: 10000,
  url: "https://apple.com"
})
Type Browser
10000 Chrome
10001 UC
10002 Firefox
10003 QQ
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

$app.openExtension(string)

Open another JSBox script, for example:

$app.openExtension("demo.js")

$app.listen(object)

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() {

  }
});

$app.notify(object)

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

Hint

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.

$cache.set(string, object)

Write to cache:

$cache.set("sample", {
  "a": [1, 2, 3],
  "b": "1, 2, 3"
})

$cache.setAsync(object)

Write to cache (async):

$cache.setAsync({
  key: "sample",
  value: {
    "a": [1, 2, 3],
    "b": "1, 2, 3"
  },
  handler: function(object) {

  }
})

$cache.get(string)

Read from cache:

$cache.get("sample")

$cache.getAsync(object)

Read from cache (async):

$cache.getAsync({
  key: "sample",
  handler: function(object) {

  }
})

$cache.remove(string)

Delete a cache:

$cache.remove("sample")

$cache.removeAsync(object)

Delete a cache (async):

$cache.removeAsync({
  key: "sample",
  handler: function() {
    
  }
})

$cache.clear()

Delete all cached objects:

$cache.clear()

$cache.clearAsync(object)

Delete all cached objects (async):

$cache.clearAsync({
  handler: function() {

  }
})

Clipboard is very important for iOS data sharing, JSBox provides various interfaces

$clipboard.text

// Get clipboard text
const text = $clipboard.text;
// Set clipboard text
$clipboard.text = "Hello, World!"

$clipboard.image

// Get clipboard image data
const data = $clipboard.image;
// Set clipboard image data
$clipboard.image = data

$clipboard.items

// Get all items from clipboard
const items = $clipboard.items;
// Set items to clipboard
$clipboard.items = items

$clipboard.phoneNumbers

Get all phone numbers from clipboard.

$clipboard.phoneNumber

Get the first phone number from clipboard.

$clipboard.links

Get all links from clipboard.

$clipboard.link

Get the first link from clipboard.

$clipboard.emails

Get all emails from clipboard.

$clipboard.email

Get the first email from clipboard.

$clipboard.dates

Get all dates from clipboard.

$clipboard.date

Get the first date from clipboard.

$clipboard.setTextLocalOnly(string)

Set text to clipboard, but ignore Universal Clipboard.

$clipboard.set(object)

Set clipboard by type and value:

$clipboard.set({
  "type": "public.plain-text",
  "value": "Hello, World!"
})

$clipboard.copy(object)

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

$clipboard.clear()

Clear all items in clipboard.

Retrieve some useful information of the device, such as language, device model etc

$device.info

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
  }
}

$device.ssid

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.

$device.networkType

Returns the network type:

const networkType = $device.networkType;
Value Type
0 None
1 Wi-Fi
2 Cellular

$device.space

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"
    }
  }
}

$device.taptic(number)

Generate a Taptic Engine Feedback:

$device.taptic(0)
Param Type Description
level number 0 ~ 2

$device.wlanAddress

Get WLAN address:

const address = $device.wlanAddress;

$device.isDarkMode

Check whether device is dark mode:

if ($device.isDarkMode) {
  
}

$device.isXXX

Check screen type quickly:

const isIphoneX = $device.isIphoneX;
const isIphonePlus = $device.isIphonePlus;
const isIpad = $device.isIpad;
const isIpadPro = $device.isIpadPro;

$device.hasTouchID

Check whether Touch ID is supported:

const hasTouchID = $device.hasTouchID;

$device.hasFaceID

Check whether Face ID is supported:

const hasFaceID = $device.hasFaceID;

$device.isJailbroken

Check whether device is jailbroken:

const isJailbroken = $device.isJailbroken;

$device.isVoiceOverOn

Check whether VoiceOver is running:

const isVoiceOverOn = $device.isVoiceOverOn;

Foundation

In this part we introduce some basic APIs in JSBox, including app/device/cache/network etc.

$device

Device related APIs.

$app

Application related APIs.

$system

Operating System related APIs.

$http

HTTP client, create requests like HTTP GET/POST.

$cache

Disk Cache and Memory Cache, all in one.

$thread

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.

$keychain.set(key, value, domain)

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.

$keychain.get(key, domain)

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.

$keychain.remove(key, domain)

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.

$keychain.clear(domain)

Delete all keychain items:

const succeeded = $keychain.clear("my.domain");

domain is required.

$keychain.keys(domain)

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

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).

$app.strings

There are only 2 steps to localize a text:

For 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

$http.request(object)

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:

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
  }
}

$http.get(object)

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.

$http.post(object)

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.

$http.download(object)

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)
  }
})

$http.upload(object)

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

$http.startServer(object)

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:

Please understand above contents by using your knowledge of HTTP.

$http.stopServer()

Stop a web server:

$http.stopServer()

$http.shorten(object)

Shorten a link:

$http.shorten({
  url: "https://apple.com",
  handler: function(url) {

  }
})

$http.lengthen(object)

Expand a link:

$http.lengthen({
  url: "http://t.cn/RJZxkFD",
  handler: function(url) {

  }
})

$network.ifa_data

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
  }
}

$network.interfaces

Returns all network interfaces on the current device:

const interfaces = $network.interfaces;

// E.g. { 'en0/ipv4': 'x.x.x.x' }

$network.startPinging(object)

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

$network.stopPinging()

Stop pinging.

$network.proxy_settings

Port of CFNetworkCopySystemProxySettings

The easiest way to create user preferences

prefs.json

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
});

Definition

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

In 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.

type

Currently, below types are supported:

Types like string, number or integer are relatively easy to use, I'm going to show you some exceptions.

type: "password"

Works the same as type: "string", used for sensitive data like passwords.

type: "slider"

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.

type: "list"

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.

type: "script"

Sometimes, you may want some customizable behaviors, like this:

{
  "title": "TEST",
  "type": "script",
  "value": "require('scripts/test').runTest();"
}

type: "child"

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

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");

$prefs.all()

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

$prefs.edit(node)

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

$system.brightness

Get/Set the brightness of screen:

$system.brightness = 0.5

$system.volume

Get/Set the volume of speaker (0.0 ~ 1.0):

$system.volume = 0.5

$system.call(number)

Make a phone call, similar to $app.openURL("tel:number").

$system.sms(number)

Send a text message, similar to $app.openURL("sms:number").

$system.mailto(email)

Send an email, similar to $app.openURL("mailto:email").

$system.facetime(number)

Create a FaceTime session, similar to $app.openURL("facetime:number").

$system.makeIcon(object)

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

$thread.background(object)

Run on background thread:

$thread.background({
  delay: 0,
  handler: function() {

  }
})

$thread.main(object)

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(() => {
  
});

$delay(number, function)

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();

$wait(sec)

It's similar to $delay but Promise is supported:

await $wait(2);

alert("Hey!");

$timer.schedule(object)

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

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");

$delay(number, function)

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();

$rect(x, y, width, height)

Create a rectangle:

const rect = $rect(0, 0, 100, 100);

$size(width, height)

Create a size:

const size = $size(100, 100);

$point(x, y)

Create a point:

const point = $point(0, 0);

$insets(top, left, bottom, right)

Create an edge insets:

const insets = $insets(10, 10, 10, 10);

$color(string)

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"
});

$rgb(red, green, blue)

Create a color with red, green, blue values.

The range of each number is 0 ~ 255:

const color = $rgb(100, 100, 100);

$rgba(red, green, blue, alpha)

Create a color with red, green, blue and alpha channel:

const color = $rgba(100, 100, 100, 0.5);

$font(name, size)

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/

$range(location, length)

Create a range:

const range = $range(0, 10);

$indexPath(section, row)

Create an indexPath, to indicates the section and row:

const indexPath = $indexPath(0, 10);

$data(object)

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"
});

$image(object, scale)

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);

$icon(code, color, size)

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.

$accessibilityAction(title, handler)

Create a UIAccessibilityCustomAction, for instance:

{
  type: "view",
  props: {
    isAccessibilityElement: true,
    accessibilityCustomActions: [
      $accessibilityAction("Hello", () => alert("Hello"))
    ]
  }
}

$objc_retain(object)

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.

$objc_relase(object)

Release a Runtime object manually:

$objc_release(manager)

$get_protocol(name)

Get an Objective-C Protocol:

const p = $get_protocol(name);

$objc_clean()

Clean all Objective-C definitions:

$objc_clean();

Built-in Functions

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 $.

Home Screen Widgets

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.

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.

Configuring Widgets

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.

Widget Parameter

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.

Example Scripts

To make your learning curve smoother, we have created some sample projects for reference:

We will improve this repository later to provide more examples.

Layout System

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.

type: "hstack"

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.

type: "vstack"

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.

type: "zstack"

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.

type: "spacer"

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.

type: "hgrid"

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

type: "vgrid"

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.

Handy Methods

We added some new methods and constants for home screen widgets to the $widget module for ease of use.

$widget.setTimeline(object)

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.

$widget.reloadTimeline()

Trigger timeline refresh manually, the system can decide whether to refresh or not:

$widget.reloadTimeline();

$widget.inputValue

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

$widget.family

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;

$widget.displaySize

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;

$widget.isDarkMode

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.

$widget.alignment

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"...

$widget.horizontalAlignment

Return the horizontalAlignment constants for layout:

const horizontalAlignment = $widget.horizontalAlignment;
// leading, center, trailing

You can also use string literals, such as "leading", "center"...

$widget.verticalAlignment

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"...

$widget.dateStyle

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"...

$env.widget

Check if it's running in a home screen widget environment:

if ($app.env == $env.widget) {
  
}

Properties

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.

Properties for All Views

props: frame

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.

props: position

Specify the position of the view:

props: {
  position: $point(0, 0) // {"x": 0, "y": 0}
}

props: offset

Specifies the view's position offset:

props: {
  offset: $point(-10, -10) // {"x": -10, "y": -10}
}

props: padding

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}
}

props: layoutPriority

Sets the priority by which a parent layout should apportion space to this child (default: 0):

props: {
  layoutPriority: 1
}

props: cornerRadius

Apply corner radius:

props: {
  cornerRadius: 10
}

When smooth corners are needed:

props: {
  cornerRadius: {
    value: 10,
    style: 1 // 0: circular, 1: continuous
  }
}

props: border

Create a border:

props: {
  border: {
    color: $color("red"),
    width: 2
  }
}

props: clipped

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
  }
}

props: opacity

Change the opacity of the view:

props: {
  opacity: 0.5
}

props: rotationEffect

Rotate the view with an angle in radians:

props: {
  rotationEffect: Math.PI * 0.5
}

props: blur

Apply a Gaussian blur:

props: {
  blur: 10
}

props: color

Set the foreground color, such as text color:

props: {
  color: $color("red")
}

props: background

Fill the background, it can be color, image, or gradient:

props: {
  background: {
    type: "gradient",
    props: {
      colors: [
        $color("#f9d423", "#4CA1AF"),
        $color("#ff4e50", "#2C3E50"),
      ]
    }
  }
}

props: link

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.

props: widgetURL

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.

Properties for Text Views

props: bold

Use bold fonts:

props: {
  bold: true
}

props: font

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)
}

props: lineLimit

Limit the maximum number of lines:

props: {
  lineLimit: 1
}

props: minimumScaleFactor

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.

Properties for Image Views

props: resizable

Specifies whether the image can be scaled:

props: {
  resizable: true
}

props: scaledToFill

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
}

props: scaledToFit

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
}

props: accessibilityHidden

Whether to disable VoiceOver:

props: {
  accessibilityHidden: false
}

props: accessibilityLabel

Set VoiceOver label:

props: {
  accessibilityLabel: "Hey"
}

props: accessibilityHint

Set VoiceOver hint:

props: {
  accessibilityHint: "Double tap to open"
}

Timeline

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.

$widget.setTimeline(object)

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.

entries

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.

policy

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 and policy, JSBox provides a default implementation of an hourly refresh for each script.

render

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.

Default Implementation

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.

In-app Preview

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.

Best Practice for Network Requests

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

View

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.

Syntax

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.

type: "text"

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

type: "image"

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

type: "color"

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.

type: "gradient"

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.

type: "divider"

Create a divider, E.g.:

{
  type: "divider",
  props: {
    background: $color("blue")
  }
}

Scripts on Keyboard

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

Try it out

https://github.com/cyanzhong/xTeko/tree/master/extension-demos/emoji-key

Try it out

$keyboard.insert(string)

Insert a string into current editing context:

$keyboard.insert("Hey!")

$keyboard.delete()

Delete selected text or delete backward:

$keyboard.delete()

$keyboard.moveCursor(number)

Move cursor by an offset:

$keyboard.moveCursor(5)

$keyboard.playInputClick()

Play sound effect of keyboard clicking:

$keyboard.playInputClick()

$keyboard.hasText

Check whether current input context has text:

const hasText = $keyboard.hasText;

$keyboard.selectedText

Get current selected text (iOS 11 only):

const selectedText = $keyboard.selectedText;

$keyboard.textBeforeInput

Get text before input:

const textBeforeInput = $keyboard.textBeforeInput;

$keyboard.textAfterInput

Get text after input:

const textAfterInput = $keyboard.textAfterInput;

$keyboard.getAllText(handler)

Get all text (iOS 11 only):

$keyboard.getAllText(text => {

});

$keyboard.next()

Switch to next keyboard:

$keyboard.next()

$keyboard.send()

Simulate send action in the keyboard:

$keyboard.send()

$keyboard.dismiss()

Dismiss the keyboard:

$keyboard.dismiss()

$keyboard.barHidden

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.

$keyboard.height

Get or set keyboard height:

let height = $keyboard.height;

$keyboard.height = 500;

Privacy Policy

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

$audio.play(object)

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;

Events

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

Image Processing

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

$imagekit.render(options, handler)

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.

$imagekit.info(image)

Get image information:

const info = $imagekit.info(source);
// width, height, orientation, scale, props

$imagekit.grayscale(image)

Get grayscaled image:

const output = $imagekit.grayscale(source);

$imagekit.invert(image)

Invert colors:

const output = $imagekit.invert(source);

$imagekit.sepia(image)

Apply sepia filter:

const output = $imagekit.sepia(source);

$imagekit.adjustEnhance(image)

Enhance image automatically:

const output = $imagekit.adjustEnhance(source);

$imagekit.adjustRedEye(image)

Red-eye adjustment:

const output = $imagekit.adjustRedEye(source);

$imagekit.adjustBrightness(image, value)

Adjust brightness:

const output = $imagekit.adjustBrightness(source, 100);
// value range: (-255, 255)

$imagekit.adjustContrast(image, value)

Adjust contrast:

const output = $imagekit.adjustContrast(source, 100);
// value range: (-255, 255)

$imagekit.adjustGamma(image, value)

Adjust gamma value:

const output = $imagekit.adjustGamma(source, 4);
// value range: (0.01, 8)

$imagekit.adjustOpacity(image, value)

Adjust opacity:

const output = $imagekit.adjustOpacity(source, 0.5);
// value range: (0, 1)

$imagekit.blur(image, bias)

Apply gaussian blur:

const output = $imagekit.blur(source, 0);

$imagekit.emboss(image, bias)

Emboss effect:

const output = $imagekit.emboss(source, 0);

$imagekit.sharpen(image, bias)

Sharpen:

const output = $imagekit.sharpen(source, 0);

$imagekit.unsharpen(image, bias)

Unsharpen:

const output = $imagekit.unsharpen(source, 0);

$imagekit.detectEdge(source, bias)

Edge detection:

const output = $imagekit.detectEdge(source, 0);

$imagekit.mask(image, mask)

Crop an image with mask:

const output = $imagekit.mask(source, mask);

$imagekit.reflect(image, height, fromAlpha, toAlpha)

Create an up-down reflected image, from height position, change alpha value from fromAlpha to toAlpha:

const output = $imagekit.reflect(source, 100, 0, 1);

$imagekit.cropTo(image, size, mode)

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

$imagekit.scaleBy(image, value)

Resize an image with scale:

const output = $imagekit.scaleBy(source, 0.5);

$imagekit.scaleTo(image, size, mode)

Resize an image to a specific size:

const output = $imagekit.scaleTo(source, $size(100, 100), 0);
// mode:
//   - 0: scaleFill
//   - 1: scaleAspectFit
//   - 2: scaleAspectFill

$imagekit.scaleFill(image, size)

Resize an image using scaleFill mode:

const output = $imagekit.scaleFill(source, $size(100, 100));

$imagekit.scaleAspectFit(image, size)

Resize an image using scaleAspectFit mode:

const output = $imagekit.scaleAspectFit(source, $size(100, 100));

$imagekit.scaleAspectFill(image, size)

Resize an image using scaleAspectFill mode:

const output = $imagekit.scaleAspectFill(source, $size(100, 100));

$imagekit.rotate(image, radians)

Rotate an image (it may change the size):

const output = $imagekit.rotate(source, Math.PI * 0.25);

$imagekit.rotatePixels(image, radians)

Rotate an image (keeps the image size, some contents might be clipped):

const output = $imagekit.rotatePixels(source, Math.PI * 0.25);

$imagekit.flip(image, mode)

Flip an image:

const output = $imagekit.flip(source, 0);
// mode:
//   - 0: vertically
//   - 1: horizontally

$imagekit.concatenate(images, space, mode)

Concatenate images with space:

const output = $imagekit.concatenate(images, 10, 0);
// mode:
//   - 0: vertically
//   - 1: horizontally

$imagekit.combine(image, mask, mode)

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

$imagekit.rounded(image, radius)

Get an image with rounded corners:

const output = $imagekit.rounded(source, 10);

$imagekit.circular(image)

Get a circular image, it will be centered and clipped if the source image isn't a square:

const output = $imagekit.circular(source);

$imagekit.extractGIF(data) -> Promise

Extract GIF data to frames:

const {images, durations} = await $imagekit.extractGIF(data);
// image: all image frames
// durations: duration for each frame

$imagekit.makeGIF(source, options) -> Promise

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.

$imagekit.makeVideo(source, options) -> Promise

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

$pdf.make(object)

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.

$pdf.toImages(data)

Render PDF as an image array:

const images = $pdf.toImages(pdf);

$pdf.toImage(data)

Render PDF as a single image:

const image = $pdf.toImage(pdf);

JSBox provided a series of APIs to interact with photos

$photo.take(object)

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.

$photo.pick(object)

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

$photo.prompt(object)

Ask user take a photo or pick a photo:

$photo.prompt({
  handler: function(resp) {
    const image = resp.image;
  }
})

Get metadata of photo

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.

$photo.scan()

Open documentation camera (iOS 13 only):

const response = await $photo.scan();
// response.status, response.results

$photo.save(object)

Save a photo to photo library:

// data
$photo.save({
  data,
  handler: function(success) {

  }
})
// image
$photo.save({
  image,
  handler: function(success) {

  }
})

$photo.fetch(object)

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

$photo.delete(object)

Delete photos in photo library:

$photo.delete({
  count: 3,
  handler: function(success) {

  }
})

The parameters are same as $photo.fetch.

Convert image object to binary data

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.

$quicklook

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.

$server

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);

$server.start(object)

Start a simple HTTP server:

const server = $server.start({
  port: 6060,
  path: "assets/website",
  handler: () => {
    $app.openURL("http://localhost:6060/index.html");
  }
});

server.stop()

Stop the web server.

server.listen(events)

Observer server events:

server.listen({
  didStart: server => {
    $delay(1, () => {
      $app.openURL(`http://localhost:${port}`);
    });
  },
  didConnect: server => {},
  didDisconnect: server => {},
  didStop: server => {},
  didCompleteBonjourRegistration: server => {},
  didUpdateNATPortMapping: server => {}
});

server.addHandler(object)

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.

handler.response

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

server.clearHandlers()

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.

WebSocket

JSBox provides WebSocket like interfaces, it creates socket connection between client and server.

$socket.new(object)

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
});

socket.listen(object)

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");
  }
});

socket.open()

Open WebSocket.

socket.close(object)

Close WebSocket.

socket.close({
  code: 1000, // Optional, see: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
  reason: "reason", // Optional
});

socket.send(object)

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
});

socket.ping(data)

const object = socket.ping(data);
const result = object.result;
const error = object.error;

socket.readyState

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

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

color represents a color, it can be generated by $color(hex):

const color = $color("#00eeee");

color.hexCode

Returns hex code of a color:

const hexCode = color.hexCode;
// -> "#00eeee"

color.components

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

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

group is object that returned from $contact.fetchGroup:

Prop Type Read/Write Description
identifier string r identifier
name string rw name

data

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

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

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

alwaysTemplate

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.

alwaysOriginal

It's similar to alwaysTemplate, but it returns an image with the original rendering mode, tintColor will be ignored.

resized($size)

Returns a resized image:

const resized = image.resized($size(100, 100));

jpg(number)

Returns a data with jpeg format, the number means compress quality (0 ~ 1):

const jpg = image.jpg(0.8);

colorAtPixel($point)

Get color at a pixel:

const color = image.colorAtPixel($point(0, 0));
const hexCode = color.hexCode;

averageColor

Get average color of image:

const avgColor = image.averageColor;

orientationFixedImage

Get orientation fixed image:

const fixedImage = image.orientationFixedImage;

indexPath

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

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

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

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

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

next()

Move to next result.

close()

Close result set.

indexForName(string)

Get index for a column name.

nameForIndex(number)

Get name for a column index.

get(object)

Get value for a name or index.

isNull(object)

Check whether null for a name or index.

request

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

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

file request contains all properties like request, these are special:

Prop Type Read/Write Description
temporaryPath string r temporary file path

multipart request

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

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

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

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

Built-in Modules

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.

Installation

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.

Package Sharing

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.

JSBox Package

Starting from v1.9.0, JSBox supports both JavaScript file and package format.

There're so many advantages:

Format Design

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.

assets

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

scripts

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.

strings

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.

config.json

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

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('path')

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.

$include('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.

Path

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.

$file

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.

VSCode

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.

Method list

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.

$thread

API Type
main required
background required

$http

API Type
request required
get required
post required
download required
upload required
shorten required
lengthen required
startServer required

$push

API Type
schedule optional

$drive

API Type
save required
open required

$cache

API Type
setAsync required
getAsync required
removeAsync required
clearAsync required

$photo

API Type
take required
pick required
save optional
fetch required
delete optional

$input

API Type
text required
speech required

$ui

API Type
animate optional
alert required
action required
menu required

$message

API Type
sms optional
mail optional

$calendar

API Type
fetch required
create optional
save optional
delete optional

$reminder

API Type
fetch required
create optional
save optional
delete optional

$contact

API Type
pick required
fetch required
create optional
save optional
delete optional
fetchGroups required
addGroup optional
deleteGroup optional
updateGroup optional
addToGroup optional
removeFromGroup optional

$location

API Type
select required

$ssh

API Type
connect required

$text

API Type
analysis required
tokenize required
htmlToMarkdown required

$qrcode

API Type
scan required

$pdf

API Type
make required

$quicklook

API Type
open optional

$safari

API Type
open optional

$archiver

API Type
zip required
unzip required

$browser

API Type
exec required

$picker

API Type
date required
data required
color required

$keyboard

API Type
getAllText required

Promise

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.

Two different ways

We have 2 different ways to handle an asynchronous function:

That'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.

Some handy shortcuts

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

JavaScript is a powerful and flexible programming language, to read this document, we suppose you have basic knowledge of JavaScript.

Resources

Similar Projects

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:

The only thing you need to know is JavaScript, you don't need to understand what's MVVM, what's React...

Basic Concept

Above 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

Hello, World!

// Show alert
$ui.alert("Hello, World!")
// Log to console
console.info("Hello, World!")

Preview Clipboard Text

$ui.preview({
  text: JSON.stringify($clipboard.items)
})

HTTP Request

$http.get({
  url: 'https://docs.xteko.com',
  handler: function(resp) {
    const data = resp.data;
  }
})

Create a button

$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.

Objective-C Blocks

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.

$block

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)

$defc

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

$define(object)

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:

Now you can use it this way:

$objc("MyHelper").invoke("alloc.init").invoke("instanceMethod")
$objc("MyHelper").invoke("classMethod")

It works like a native class.

$delegate(object)

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.

Basic Concept

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.

Why we need runtime?

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.

Methods

Example

This 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

invoke(methodName, arguments ...)

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();

selector

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.

$objc_retain(object)

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.

$objc_relase(object)

Release a Runtime object manually:

$objc_release(manager)

Syntactic Sugar

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.

Rules

This syntax has very simple rules:

For 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

Auto generated classes

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

Methods

Note:

Therefore, there are 2 methods provided to convert them:

Example

This 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.

Safari Extension

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.

JavaScript Web APIs

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

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

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

$calendar.fetch(object)

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

$calendar.create(object)

Create a calendar item:

$calendar.create({
  title: "Hey!",
  startDate: new Date(),
  hours: 3,
  notes: "Hello, World!",
  handler: function(resp) {

  }
})

$calendar.save(object)

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
    })
  }
})

$calendar.delete(object)

Delete a calendar item:

$calendar.delete({
  event,
  handler: function(resp) {
    
  }
})

Manage contact items in Contacts.app

$contact.pick(object)

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.

$contact.fetch(object)

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) {

  }
})

$contact.create(object)

Create contact item:

$contact.create({
  givenName: "Ying",
  familyName: "Zhong",
  phoneNumbers: {
    "Home": "18000000000",
    "Office": "88888888"
  },
  emails: {
    "Home": "log.e@qq.com"
  },
  handler: function(resp) {

  }
})

$contact.save(object)

Save modified contact item:

$contact.save({
  contact,
  handler: function(resp) {

  }
})

$contact.delete(object)

Delete contacts:

$contact.delete({
  contacts: contacts
  handler: function(resp) {
    
  }
})

$contact.fetchGroups(object)

Fetch all groups:

var groups = await $contact.fetchGroups();

console.log("name: " + groups[0].name);

$contact.addGroup(object)

Create a new group with name:

var group = await $contact.addGroup({"name": "Group Name"});

$contact.deleteGroup(object)

Delete a group:

var groups = await $contact.fetchGroups();

$contact.deleteGroup(groups[0]);

$contact.updateGroup(object)

Save updated group:

var group = await $contact.fetchGroups()[0];
group.name = "New Name";

$contact.updateGroup(group);

$contact.addToGroup(object)

Add a contact to a group:

$contact.addToGroup({
  contact,
  group
});

$contact.removeFromGroup(object)

Remove a contact from a group:

$contact.removeFromGroup({
  contact,
  group
});

Native SDK

In this part we are going to introduce some native SDKs to you.

With these APIs, we can talk to iOS native APIs directly.

$message

Send text message or mail using native interface.

$calendar

Manage calendar items.

$reminder

Manage reminder items.

$contact

Manage your contacts.

$location

Fetch/Track user location.

$motion

Track motion data from sensors.

$push

Schedule local push notifications.

$safari

Open website in safari

Above is a brief introduction, examples are coming soon.

Fetch/Track GPS information easily

$location.available

Check whether location service is available:

let available = $location.available;

$location.fetch(object)

Fetch location:

$location.fetch({
  handler: function(resp) {
    const lat = resp.lat;
    const lng = resp.lng;
    const alt = resp.alt;
  }
})

$location.startUpdates(object)

Track user location updates:

$location.startUpdates({
  handler: function(resp) {
    const lat = resp.lat;
    const lng = resp.lng;
    const alt = resp.alt;
  }
})

$location.trackHeading(object)

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;
  }
})

$location.stopUpdates()

Stop updates.

$location.select(object)

Select a location from iOS built-in Map:

$location.select({
  handler: function(result) {
    const lat = result.lat;
    const lng = result.lng;
  }
})

$location.get()

Get the current location, similar to $location.fetch but uses async await.

const location = await $location.get();

$location.snapshot(object)

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

$message.sms(object)

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

$message.mail(object)

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

$motion.startUpdates(object)

Start updates:

$motion.startUpdates({
  interval: 0.1,
  handler: function(resp) {

  }
})

Learn more about the data CMDeviceMotion.

$motion.stopUpdates()

Stop updates.

Manage reminder items in Reminders.app

$reminder.fetch(object)

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)

$reminder.create(object)

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.

$reminder.save(object)

Similar to $calendar.save:

$reminder.save({
  event,
  handler: function(resp) {

  }
})

$reminder.delete(object)

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

$safari.open(object)

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.

$safari.items

Get items in Safari when you are using Action Extension:

const items = $safari.items; // JSON format

$safari.inject(script)

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

$safari.addReadingItem(object)

Add item to Safari reading list:

$safari.addReadingItem({
  url: "https://sspai.com",
  title: "Title", // Optional
  preview: "Preview text" // Optional
})

Run JSBox script

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.

Specify view height

By default, the view height is 320, you can change it by:

$intents.height = 180;

Parameters

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.

What's Shortcuts

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:

JSBox's Shortcuts features

For now, JSBox supports Shortcuts as below:

Note

JavaScript support for Shortcuts

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.

Present JSBox views

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).

Parameters

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;

Shortcuts voice command

You can launch JSBox scripts with Siri voice command, it can execute code or present views.

Do it with these two ways:

SQLite

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

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.

SQLite Browser

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.

SQLite in multi-thread

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();

db.beginTransaction()

For a SQLite connection, beginTransaction() starts a transaction.

db.commit()

For a SQLite connection, commit() commits a transaction.

db.rollback()

For a SQLite connection, rollback() rollbacks a transaction.

$sqlite.open(path)

Open a SQLite connection with file path:

const db = $sqlite.open("test.db");

$sqlite.close(db)

Close a SQLite connection:

const db = $sqlite.open("test.db");

//...
$sqlite.close(db); // Or db.close();

Update

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.

Query

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.channel

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

channel.execute(object)

Execute script:

channel.execute({
  script: "ls -l /var/lib/",
  timeout: 0,
  handler: function(result) {
    console.log(`response: ${result.response}`)
    console.log(`error: ${result.error}`)
  }
})

channel.write(object)

Execute command:

channel.write({
  command: "",
  timeout: 0,
  handler: function(result) {
    console.log(`success: ${result.success}`)
    console.log(`error: ${result.error}`)
  }
})

channel.upload(object)

Upload local file to remote:

channel.upload({
  path: "resources/notes.md",
  dest: "/home/user/notes.md",
  handler: function(success) {
    console.log(`success: ${success}`)
  }
})

channel.download(object)

Download remote file to local:

channel.download({
  path: "/home/user/notes.md",
  dest: "resources/notes.md",
  handler: function(success) {
    console.log(`success: ${success}`)
  }
})

Secure Shell

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.

$ssh.connect(object)

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.

$ssh.disconnect()

Disconnect all SSH sessions connected by JSBox:

$ssh.disconnect()

session.sftp

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

sftp.connect()

Create SFTP connection:

await sftp.connect();

sftp.moveItem(object)

Move a file:

sftp.moveItem({
  src: "/home/user/notes.md",
  dest: "/home/user/notes-new.md",
  handler: function(success) {
    
  }
})

sftp.directoryExists(object)

Check whether a directory exists:

sftp.directoryExists({
  path: "/home/user/notes.md",
  handler: function(exists) {

  }
})

sftp.createDirectory(object)

Create a directory:

sftp.createDirectory({
  path: "/home/user/folder",
  handler: function(success) {

  }
})

sftp.removeDirectory(object)

Delete a directory:

sftp.removeDirectory({
  path: "/home/user/folder",
  handler: function(success) {

  }
})

sftp.contentsOfDirectory(object)

List all files in a directory:

sftp.contentsOfDirectory({
  path: "/home/user/folder",
  handler: function(contents) {

  }
})

sftp.infoForFile(object)

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

sftp.fileExists(object)

Check whether a file exists:

sftp.fileExists({
  path: "/home/user/notes.md",
  handler: function(exists) {

  }
})

sftp.createSymbolicLink(object)

Create symbolic link:

sftp.createSymbolicLink({
  path: "/home/user/notes.md",
  dest: "/home/user/notes-symbolic.md",
  handler: function(success) {

  }
})

sftp.removeFile(object)

Delete a file:

sftp.removeFile({
  path: "/home/user/notes.md",
  handler: function(success) {

  }
})

sftp.contents(object)

Get a file (binary):

sftp.contents({
  path: "/home/user/notes.md",
  handler: function(file) {

  }
})

sftp.write(object)

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) {
    
  }
})

sftp.append(object)

Append file (binary) to remote:

sftp.append({
  file,
  path: "/home/user/notes.md",
  handler: function(success) {
    
  }
})

sftp.copy(object)

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

UIView animation

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

Chainable

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

Context Menu

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));
      }
    }
  ]
});

SF Symbols

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

destructive

For important actions that may be dangerous, you can use the destructive style:

{
  title: "Title",
  destructive: true,
  handler: sender => {}
}

Sub Menus

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.

Inline Menu

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 => {}
    }
  ]
}

list & matrix

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.

Pull-Down Menus

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.

Dark Mode

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

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 be light. If you'd like to adapt dark mode, turn it to auto, then adjust your colors.

Default controls have different colors under different themes, please refer to the latest UIKit-Catalog demo for more information.

Dynamic Colors

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.

Dynamic Images

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

events: themeChanged

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).

Blur Effect

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

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.

Embrace Dark Mode

In general, there are three things you should do:

In 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.

Event handling

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.

view.whenTapped(handler)

Single tap action is triggered:

button.whenTapped(() => {
  
});

view.whenDoubleTapped(handler)

Double tap action is triggered:

button.whenDoubleTapped(() => {
  
});

view.whenTouched(args)

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", () => {

}));

view.addEventHandler(args)

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

view.removeEventHandlers(events)

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

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.

events: tapped

Triggers when tap gesture received:

tapped: function(sender) {

}

events: longPressed

Triggers when long press gesture received:

longPressed: function(info) {
  let sender = info.sender;
  let location = info.location;
}

events: doubleTapped

Triggers when double tap gesture received:

doubleTapped: function(sender) {

}

events: touchesBegan

When touch event is triggered:

touchesBegan: function(sender, location, locations) {

}

events: touchesMoved

When touch is moving:

touchesMoved: function(sender, location, locations) {

}

events: touchesEnded

When touch event finished:

touchesEnded: function(sender, location, locations) {

}

events: touchesCancelled

When touch event cancelled:

touchesCancelled: function(sender, location, locations) {

}

Basic Concept

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.

Example

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.

type

To specify view's type, such as button and label, they are work different.

props

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.

layout

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.

events

Provide events here, for example tapped in button, we will show different events supported by different views soon.

views

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 System

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)}`)
        }
      }
    }
  ]
})

Basic Concept

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.

What's constraint?

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.

Properties

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.

Relations

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:

Masonry Apple.

This document is still under construction, we will provide more examples soon.

Flex

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:

For example, using "LRTB" to describe UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin.

In the UI system of JSBox, there are still something we need to talk

$ui.pop()

Pop out the most front view from the stack.

$ui.popToRoot()

Pop to the root view.

$ui.get(id)

Get a view instance by id, same as $(id).

You can also get view by type, only if this type is distinct.

$ui.alert(object)

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.

$ui.action(object)

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.

$ui.menu(object)

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) {
    
  }
})

$ui.popover(object)

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

$ui.toast(message)

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();

$ui.success(string)

Similar to toast, but the bar color is green, indicates success:

$ui.success("Done");

$ui.warning(string)

Similar to toast, but the bar color is yellow, indicates warning:

$ui.warning("Be careful!");

$ui.error(string)

Similar to toast, but the bar color is red, indicates error:

$ui.error("Something went wrong!");

$ui.loading(boolean)

Show a loading indicator:

$ui.loading(true)

You can also display a message here:

$ui.loading("Loading...")

$ui.progress(number)

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...")

$ui.preview(object)

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

$ui.create(object)

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) => {

});

$ui.window

Get current window of $ui.render.

$ui.controller

Get the most front view controller of the app.

$ui.title

Get or set the most front view's title.

$ui.selectIcon()

Select icon:

var icon = await $ui.selectIcon();

We use render and push to draw views on the screen

$ui.render(object)

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.

$ui.push(object)

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.

$(id)

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.

Life Cycle

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.

Keyboard Height Changes

You can observe keyboard height changes with:

events: {
  keyboardHeightChanged: height => {

  }
}

Shake event

You can detect shake event with:

events: {
  shakeDetected: function() {

  }
}

Support external keyboard

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

type: "view"

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.

props

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.

navButtons

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.

titleView

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: [

  ]
});

layout(function)

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);
});

updateLayout(function)

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.

remakeLayout(function)

Similar to updateLayout, but remake costs more performance, try to use update as much as you can.

add(object)

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(id)

Get a subview with specific identifier.

remove()

Remove a view from its super view's hierarchy.

insertBelow(view, other)

Insert a new view below an existing view:

view.insertBelow(newView, existingView);

insertAbove(view, other)

Insert a new view above an existing view:

view.insertAbove(newView, existingView);

insertAtIndex(view, index)

Insert a new view at a specific index:

view.insertAtIndex(newView, 4);

moveToFront()

Move self to super's front:

existingView.moveToFront();

moveToBack()

Move self to super's back:

existingView.moveToBack();

relayout()

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.

setNeedsLayout()

Mark a view as needs layout, it will be applied in the next drawing cycle.

layoutIfNeeded()

Forces layout early before next drawing cycle, can be used with setNeedsLayout:

view.setNeedsLayout();
view.layoutIfNeeded();

sizeToFit()

Resize the view to its best size based on the current bounds.

scale(number)

Scale a view (0.0 ~ 1.0):

view.scale(0.5)

snapshotWithScale(scale)

Create snapshot with scale:

const image = view.snapshotWithScale(1)

rotate(number)

Rotate a view:

view.rotate(Math.PI)

events: ready

ready event is supported for all views, it will be called when view is ready:

ready: function(sender) {
  
}

events: tapped

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.

events: pencilTapped

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
}

events: hoverEntered

For iPadOS 13.4 (and above) with trackpad, this is called when pointer enters the view:

hoverEntered: sender => {
  sender.alpha = 0.5;
}

events: hoverExited

For iPadOS 13.4 (and above) with trackpad, this is called when pointer exits the view:

hoverExited: sender => {
  sender.alpha = 1.0;
}

events: themeChanged

Detect dark mode changes:

themeChanged: (sender, isDarkMode) => {
  
}

Refer Component to see how to use other controls.

JSBox script on widget

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.

Some APIs are very useful on widget

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

$widget provides API to interact with widgets.

$widget.height

Get or set the height of widget:

// Get
const height = $widget.height;
// Set
$widget.height = 400;

$widget.mode

Get current display mode (show less/show more):

const mode = $widget.mode; // 0: less 1: more

$widget.modeChanged

Observe mode changes:

$widget.modeChanged = mode => {
  
}