Skip to content

Telerik-Verified-Plugins/AppleWatch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

66 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AppleWatch Cordova plugin

An AppleWatch plugin which requires no native code as the Watch UI is built with plain old JavaScript.

UI widgets

Let's start off with the fun stuff: how do we create Watch UI widgets with JavaScript?

Common properties

All widgets support the following properties, which will mostly not be repeated below.

{
  'alpha': 0.1, // barely visible, default is 1
  'width': 100, // pixels
  'height': 50  // pixels
}

Switch

Switch

var fooSwitchOn = true;
var barSwitchOn = false;

function onFooSwitchChanged(changedTo) {
  fooSwitchOn = changedTo;
  console.log("Foo switch changed to " + changedTo);
}

'switch': {
  'title': 'Want foo?',
  'on': fooSwitchOn, // the initial state of the switch when rendering
  'color': '#CC0000', // red in this case
  'callback': 'onFooSwitchChanged' // optional, but pretty useful
},
'switch2': {
  'title': 'And bar?',
  'on': barSwitchOn,
  'color': '#02779E'
}

Slider

Slider

var sliderValue = 50;

function onSliderChanged(val) {
  sliderValue = val;
  console.log("Slider changed to: " + val);
}

'slider': {
  'steps': 20, // of 100, so each step is 5 in this case
  'value': sliderValue, // of 100, making this essentially a percentage
  'color': '#CC0000', // the color of the dots, red in this case
  'callback': 'onSliderChanged',
  'hideValue': false // default false, shows the current value below the slider
}

Map

Map

'map': {
  'center': {
    // Amersfoort, The Netherlands
    'lat': 52.1851552,
    'lng': 5.3996181
  },
  'zoom': 4.1, // 0.001 is streetlevel, 4 fits the entire Netherlands
  'annotations': [ // up to 5 annotations (custom pins), any more are ignored (play with the zoom value to make them all fit)
    {
      'pinColor': 'green', // green | red | purple
      'lat': 52.1851,
      'lng': 5.3996
    },
    {
      'pinColor': 'red',
      'lat': 51.751,
      'lng': 8.4
    },
    {
      'pinColor': 'purple',
      'lat': 50.2251,
      'lng': 4.7196
    }
  ]
}

Menu

Menu

This menu is triggered by a Force Touch, which is pretty neat!

function onContextMenuPlay() {
  console.log("User wants to play :)");
}

'contextMenu': {
  // configure up to 4 items (any more will be ignored)
  'items': [
    {
      'title': 'Play',
      'iconNamed': 'play', // see the table below
      'callback': 'onContextMenuPlay'
    },

    {
      'title': 'Resume',
      'iconNamed': 'resume',
      'callback': 'onContextMenuResume'
    }
  ]
}

The iconNamed property must be one of:

iconNamed image
accept
add
block
decline
info
maybe
more
mute
pause
play
repeat
resume
share
shuffle
speaker
trash

Image

Image

Currently the image must reside in the www folder of your app, or you must provide a base64 encoded image. In the future we may add support for loading images from other locations (like the Internet).

'image': {
  // by not passing width and height the image is shown full size
  'src': 'www/img/logo.png' // a file inside the www folder, if a 'data' attribute is passed as well, the src will not be used
  'data': 'data:image/png;base64,R0lGODlhDAAMALMBAP8AAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAUKAAEALAAAAAAMAAwAQAQZMMhJK7iY4p3nlZ8XgmNlnibXdVqolmhcRQA7' // base64 encoded png
}

Label

Label

The label at the top is called label, you can add a second one (with a different style perhaps) by adding a label2.

'label': {
  'value': 'A gorgeous blue header',
  'color': '#1884C4',
  'font': { // optional
    'size': 10
  }
},
'label2': { // optional, max 2 lines
  'value': 'With a white message, served @ ' + new Date(),
  'color': '#FFFFFF',
  'font': {
    'size': 8
  }
},
}

Table

Table

This widget gives you more control over the layout, because a table may contain any number of rows, one or two columns, and a label, and image, or both inside each row.

function onTableRowSelected(index) {
  console.log('Selected table row index: ' + index);
}

'table': {
  'callback':'onTableRowSelected',
  'rows': [

    // first row
    {
      'type': 'OneColumnRowType', // see the table below
      // this element defines properties for the entire row
      'group': {
        'backgroundColor': '#1884C4',
        'cornerRadius': 8
      },
      'label': {
        'value':'  images!' // unlike in HTML, multiple spaces have effect
      },
      'imageLeft': {
        'data': 'data:image/png;base64,R0lGODlhDAAMALMBAP8AAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAUKAAEALAAAAAAMAAwAQAQZMMhJK7iY4p3nlZ8XgmNlnibXdVqolmhcRQA7',
        'width': 25,
        'height': 30
      },
      'imageRight': {
        'src': 'www/img/logo.png',
        'width': 25,
        'height': 30
      }
    },

    // second row
    {
      'type': 'OneColumnSelectableRowType',
      'group': {
        'backgroundColor': '#7884C4',
        'cornerRadius': 8
      },
      'label': {
        'value':'2nd row, no img'
      }
    },

    // third row
    {
      'type': 'TwoColumnsRowType',
      'col1label': {
        'value': '50%',
        'color': '#FFA500',
        'font': {
          'size': 16
        }
      },
      'col2image': {
        'src': 'www/img/logo.png',
        'width': 25,
        'height': 30
      }
    }
  ]
}

The row's type attribute must be one of:

type Description
OneColumnRowType A readonly row which can contain images and a label.
TwoColumnsRowType A readonly row with two evenly distributed columns. Each column can contain a centered label and/or image.
OneColumnSelectableRowType Same as OneColumnRowType but the entire row is 'clickable'. Once that's done, a callback specified on the table level will be invoked. Don't add this to a glance since those are readonly.
Need something else? This element is easily extensible, so please let us know what you want to see here..

Button

Button

All types of buttons can be styled with a bunch of properties. Those buttons above are rendered by this code:

// the red one
'userInputButton': {
  'width': 60, // default full width, see the orange buttons
  'height': 30,
  'title': {
    'value': 'Vote',
    'color': '#FFFFFF',
    'font': {
      'size': 12
    }
  },
  'backgroundColor': '#CC0000',
  'alpha': 1 // which is the default
},

// the blue one
'actionButton': {
  'title': {
    'value': 'Refresh',
    'color': 'blaaaaaaa', // because this is rubbish, we fall back to the default (black)
    'font': {
      'size': 17
    }
  },
  'color': '#FFFFFF',
  'width':80,
  'height':44
},

// the orange one
'pushNavButton': {
  'title': {
    'value': 'Push nav'
  },
  'backgroundColor': '#FFA500'
}
UserInputButton

User input

The userInputButton can be used to get input from the user (duh).

function onUserInput(result) {
  if (result.type == "base64img") {
    console.log("An animated Emoji was selected");
    // you can show the image, for example add this: <img id="myImage" src=""/> and do this:
    document.getElementById("myImage").src = result.data;
  } else {
    // one of the pre-cooked anwers was picked, or a (non animated) Emoji, or even some fancy speech-to-text (dictation)
    feedback("User input: " + result.data);
  }
}

// the same button as (the red one) shown before , but without the layout bits
'userInputButton': {
  'inputMode': 'WKTextInputModeAllowAnimatedEmoji', // see the table below for options
  'suggestions': ['foo', 'bar', 'shaz'], // 0-3 possible suggestions
  'title': {
    'value': 'Vote',
  },
  'callback': 'onUserInput'
}

The inputMode attribute must be one of:

inputMode Description
WKTextInputModePlain Dictation/suggested text only. The emoji button will be hidden. If you don't provide suggestions the dictation interface will be shown directly.
WKTextInputModeAllowEmoji Dictation, suggested text, and non-animated emoji.
WKTextInputModeAllowAnimatedEmoji Dictation, suggested text, and both animated and non-animated emoji.
ActionButton

Of course you can do anything you like when a user pressed a button. Like fetching items from a remote server and navigating to a detail page on the watch.

function onRefreshButtonPressed() {
  console.log("Reloading the page");
  applewatch.loadApp(payload);
}

'actionButton': {
  'title': {
    'value': 'Refresh!',
  },
  'callback': 'onRefreshButtonPressed'
}
Navigation button

There are two styles of navigation: 'push' and 'modal'. The former performs a left-right slide animation and has a back button. The latter stacks the new page on top of the other with a slide animation from the bottom and has a done/close button.

These buttons can only be used on the first app page and will navigate to the AppDetail page when activated.

PushNavButton
'pushNavButton': {
  'backTitle': 'Go Back', // optional, only an arrow is shown if not provided
  'title': {
    'value': 'Push nav'
  }
}
ModalNavButton
'modalNavButton': {
  'closeTitle': 'Shut it', // optional, and it's recommended to use the default 'Cancel' because that's shown a short moment anyway
  'title': {
    'value': 'Modal nav'
  }
}

Loading a glance

Glances are readonly pages which can be accessed by swiping up from the watchface. Your app can push content to the glance at any time and the watch will display the latest state it received. Also, at the moment the glance is accessed by the user, the glance will request an update from your phone app by invoking the applewatch.callback.onLoadGlanceRequest method. If you don't provide an implementation the glance will remain black.

Glance

It's recommended that you do this on deviceready to configure the glance. Take a look at our demo if you want a complete example.

To create the UI shown above, do:

function onGlanceRequestsUpdate() {
  var payload = {
    'label': {
      'value': 'A gorgeous blue header',
      'color': '#1884C4',
      'font': {
        'size': 10
      }
    },
    'label2': {
      'value': 'With a white message, served @ ' + new Date(),
      'color': '#FFFFFF',
      'font': {
        'size': 8
      }
    },
    'image': {
      'src': 'www/img/logo.png',
      'alpha': 0.8
    }
  };
  applewatch.loadGlance(payload);
}

applewatch.callback.onLoadGlanceRequest = onGlanceRequestsUpdate;

And here's a more concise way to configure a glance. In this case we'll only show an image.

applewatch.callback.onLoadGlanceRequest = function() {
  applewatch.loadGlance({
    'image': {'src': 'www/img/logo.png'}
  });
}

Glances can't have buttons (they're readonly, remember), so tapping anywhere on the glance will launch your app.

The allowed widgets and its order are precooked in the storyboard. Unfortunately WatchKit doesn't allow dynamically created UI elements, but you can throw a lot of items on a storyboard and show/hide them at will. That's what we're doing.

A glance is divided in two sections and can't show more than one page (they don't scroll). The supported widgets are (in this rendering order, and they're all optional):

Top section
Variable name Widget type
label Label
label2 Label
Bottom section
Variable name Widget type
image Image
table Table
map Map

To be clear: this means you can't show a map above a table, but since the glance has very limited space you probably only want one of those at a time anyway. That being said if you need f.i. a label in the bottom section, let us know at the GitHub repository and we'll consider adding it.

Loading an app page

The basic idea is the same as the glance, but you have much more options.

So after deviceready has fired, you could do:

applewatch.callback.onLoadAppMainRequest = function() {
var payload = {
  'label': {
    'value': 'Blue header label'
  },
  // you get the idea
};

applewatch.loadAppMain(payload);

And to load content into the app detail page:

applewatch.callback.onLoadAppDetailRequest = function() {
  var payload = {..};
  applewatch.loadAppDetail(payload);
}

Page configuration

The pages have a little optional configuration to tweak your app even more:

var payload = {
  'group': { // the page wrapper. The defaults are probably best though.
    'backgroundColor':'#1884C4', // override the black background color
    'cornerRadius': 0 // make your app square (the default has radius 4-ish)
  },
  'title': 'My app', // shown at the top of the page. Note that the title color can't be tweaked except in the storyboard itself
  // .. any other stuff on your page
};
App main page
Variable name Widget type
label Label
image Image
label2 Label
table Table
switch Switch
switch2 Switch
slider Slider
map Map
userInputButton UserInputButton
actionButton ActionButton
pushNavButton PushNavButton
modalNavButton ModalNavButton

Note that since any unused variable is hidden we can easily add f.i. a 'label3' at the bottom of this stack, or add an 'actionButton2' below the other buttons. You're welcome to request features like those and we'll try to add them without making too much of a mess and remaining compatibility with current usage.

App detail page

The same as the main page, except for the last two items (the navigation buttons).

Navigating to an app page

Instead of updating the content of the current page with loadAppMain or loadAppDetail, you may want to programmatically navigate to a different page. You were already able to navigate from main to detail on the watch itself by adding a pushNavButton or modalNavButton, but the phone app can instantiate navigation as well.

// navigate from main to detail:
applewatch.navigateToAppDetail();

// to navigate back from detail to main:
applewatch.navigateToAppMain();

One usecase could be to have a table with selectable rows, and in the table callback function you'd navigate to the detail page. Once the detail page loads the applewatch.callback.onLoadAppDetailRequest function will fire so you can populate the detail page.

Notifications

The watch can receive remote and local notifications. These can have buttons which have a callback that invokes your phone app. To register for receiving those callbacks, configure these functions:

// local notifications
applewatch.callback.onLocalNotification = function(identifier) {..};

// remote notifications
applewatch.callback.onRemoteNotification = function(identifier) {..};

The identifier is the identifier specified in any custom button in your notification payload.

TODO elaborate and describe how to create a local notification.

Using your own storyboard

If you want more control over the layout, you can open the Interface.storyboard file in XCode, adjust it, and have the app load it by placing it here: www/custom-watchkit-storyboards/Interface.storyboard.

Installation

Prep

Install npm 'xcode' package because our hooks need it: npm install -g xcode

CLI

 cordova create applewatchtest
 cd applewatchtest
 cordova platform add ios

XCode

File > New > Target > Apple Watch: Language ObjC, Select Glance and Notifications > Finish > Activate

CLI

 cordova plugin add https://github.com/Telerik-Verified-Plugins/AppleWatch
 cordova prepare

XCode

Set CFBundleVersion (Bundle Version) and CFBundleShortVersionString (Bundle versions string, short) of all targets to the same value (use XCode's search feature and change all 3 .plist values)

At this point your builds should succeed

More XCode

App Groups: register an appgroup in your iOS member center (Identifiers > App Groups): we expect group., like group.io.cordova.hellocordova, then add it to your App ID. Now generate a provisioning profile with the new App ID and add it XCode (download, then double-click the file should do it). In XCode, go to your targets and add this app group to both the phone and watch app targets (Capabilities tab).

Tips:

Building in XCode 7 requires you to set bitcode to NO for all 3 targets. Go to 'Build Settings' and disable it 3 times. Will be fixed soon, when MMWormhole is updated.

For developer builds to work on a real Watch you'll need to register the Watch UDID and add it to the provisioning profile or you'll see 'Application verification failed' when installing the app on the Watch.

While you're in the iOS member center, also register an App Group (under Identifiers) and make sure you enable the App Group in the App ID you're using for your app.

If you want a quickstart, use demo/index.html

The simulator doesn't support local notifications

Debugging of both the app and the extension: http://www.fiveminutewatchkit.com/blog/2015/3/13/how-to-debug-an-ios-app-while-the-associated-watchkit-app-is-running

Notifications: http://natashatherobot.com/watchkit-actionable-notifications/

Kudos

Lee Crossley for his work on figuring out how to add and use the wormhole lib in a Cordova plugin