cordova

Adding Apple Watch to a MAF App

Posted on Updated on

Here are some steps for adding iOS 9+ Apple Watch OS 2+ support to an Oracle Mobile Application Framework (MAF) App.

Set up and deploy the MAF app

1. Download and unzip the following Cordova plugin zip from git: https://github.com/leecrossley/cordova-plugin-apple-watch/archive/master.zip (more info at https://github.com/leecrossley/cordova-plugin-apple-watch)

2. Using JDeveloper, create a MAF app and select the unzipped plugin folder in the maf-application.xml (in the “Additional Plugins” section)

3. Create an AMX page (using amx:clientListener in amx:commandButtons to trigger the plugin APIs); this can be just placeholder content for now

4. Deploy the app to the simulator to generate an Xcode application in the JDev app’s “deploy” folder. We will be editing this generated output from the deploy folder so be careful to note that generated output will be lost if you re-deploy from JDev in the future.

5. Quit JDev

Prepare Xcode

6. Open the Xcode project from the MAF app’s deploy folder (Oracle_ADFmc_Container_Template.xcodeproj).

7. Create a new Target – WatchKit App (ensure “Notification Scene”, “Glance Scene”, and “Complication” are checked and activate the new scheme when prompted)

8. Open the Interface.storyboard from the newly-added target

Create the Watch App’s UI

9. Drag a Label from the Object Library onto the Interface Controller Scene’s Interface Controller and use the Attributes Inspector to style the label if desired (e.g. Size – Width and Height = Relative to Container and value 0.7)

Drag a Button from the Object Library onto the same scene below that Label.

10. From the watch extension folder in Xcode, open the InterfaceController.swift file in a new window or use the “ven diagram” (assistant editor) icon for split-screen editing.

11. While holding down the Control key, drag the label from the scene hierarchy that was added in step 9 into the class of step 10 to generate an Outlet variable:

@IBOutlet weak var helloWorldLabel: WKInterfaceLabel!

Repeat with the button to generate:

@IBOutlet weak var someButton: WKInterfaceButton!

and then also type in:

@IBAction func someButtonAction()
{
  print("someButtonAction")
}

Finally, Control-drag the hollow circle from the someButtonAction margin onto the button in the scene to connect the action to that button.

12. If you want to create a page-based app, drag additional Interface Controller scenes from the Object Library and connect them via Control-dragging the seque line from the selected scene to the new scene (and then in the seque panel, choose “next page” as the relationship)

Make live data to test

13. In the InterfaceController.swift file, add this temporary code to the end of the awakeWithContext method so we can verify the app functions before connecting it to the MAF app via the Cordova plugin:

// Create a timer to refresh the time every second
let timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(InterfaceController.updateLabel), userInfo: nil, repeats: true)
 timer.fire()

14. Add this additional function to the class to support the temproary timer code:

func updateLabel()
{
  print("updateLabel")
  let time = NSDate()
  let formatter = NSDateFormatter()
  let timeZone = NSTimeZone(name: "UTC")
  formatter.timeZone = timeZone
  formatter.dateFormat = "h:mm:ss a"
  var formattedString = formatter.stringFromDate(time)
  formattedString = formattedString.stringByReplacingOccurrencesOfString(" p", withString: "PM", options: [], range: nil)
  formattedString = formattedString.stringByReplacingOccurrencesOfString(" a", withString: "AM", options: [], range: nil)
  self.helloWorldLabel?.setText("UTC \(formattedString)")
}

15. Run the WatchKit target to verify everything was created properly.

16. Stop the app in Xcode

Configure the Cordova Plugin APIs and, in turn, MMWormhole

17. Go to Oracle_ADFmc_Container_Template – all 3 targets: the phone, the Watch Target, and the Watch Extension Target – Capabilities – App Groups and turn it on. Ensure that all 3 checkmarks are checked. Your Xcode identity and Bundle Identifier must allow you to use app groups. This is a requirement of the MMWormhole framework (you’ll get an error and crash when running the app if you don’t do this). The “App Groups” must have an entry with a value that you will later use in your Swift code (step 23) and JavaScript code (step 25).

18. There is no need to manually add MMWormhole (https://github.com/mutualmobile/MMWormhole) to the Xcode project because the Cordova extension already has it included. However, you will need to select all of the MMWormhole*.m files in Xcode from the Plugins folder and then add (checkbox) target membership for the watch extension.

19. By default, the watch extension will not be able to see those MMWormhole Objective-C objects so you need to create a bridging header. Go to Oracle_ADFmc_Container_Template – Choose the Watch Target Extension – Build Settings – Swift Compiler Code Generation – Objective-C Bridging Header and specify the value:

Oracle_ADFmc_Container_Template/Plugins/Bridging-Header.h

20. In the Navigator, select the Plugins folder and choose File – New – File – Header File and name it Bridging-Header.h and choose the Watch Target Extension target and then use this as the file content:

#import "MMWormhole.h"
#import "MMWormholeSession.h"

21. In the watch extension InterfaceController.swift file, comment out the existing 2 lines for the timer in the awakeWithContext method (from step 13).

22. Add the following to the awakeWithContext method of the watch extension InterfaceController.swift file:

print("awakeWithContext")
let watchConnectivityListeningWormhole = MMWormholeSession.sharedListeningSession()
watchConnectivityListeningWormhole.activateSessionListening()
watchConnectivityListeningWormhole.listenForMessageWithIdentifier("from_phone_queue", listener:
{
  (messageObject) -> Void in
    if let message: AnyObject = messageObject
    {
      print("from_phone_queue")
      // handle your message here
      let messageValue: String! = message.objectForKey("selectionString") as! String
      self.helloWorldLabel?.setText("Phone \(messageValue)")
    }
})

23. Add this to the someButtonAction method of the watch extension InterfaceController.swift file:

// Make sure this appGroupId value is
// specified in the App Groups config from
// step #17
let appGroupId = "group.com.yourfoo.yourbar"
let wormhole = MMWormhole(applicationGroupIdentifier: appGroupId, optionalDirectory: nil, transitingType: .SessionContext)
let diceRoll = Int(arc4random_uniform(6) + 1) // 1 through 6
wormhole.passMessageObject("rolled \(diceRoll)", identifier: "from_watch_queue")

24. Clean then build to make sure the MMWormhole objects compile, otherwise the above steps need to be revisited.

25. Edit the app’s AMX page so that it has these components to interact with the Cordova plugin APIs:

<amx:verbatim id="v1">
<![CDATA[
window.handleMessageFromWatch = function(msg)
{
  console.log("handleMessageFromWatch");
  var outputTextElement =
    document.getElementById("ot2");
  if (!outputTextElement)
    console.log("Unable to find ot2");
  outputTextElement.textContent =
    "Watch " + msg;
};

window.handleAWInit = function(clientEvent)
{
  console.log("handleAWInit");
  var appGroupId = "group.com.yourfoo.yourbar";
  var queueName = "from_watch_queue";
  applewatch.init(
  function successHandler(appGroupId)
  {
    console.log("init Success", appGroupId);
  },
  function errorHandler(foo)
  {
    console.log("init Failure", foo);
  },
  appGroupId);

  applewatch.addListener(
    queueName,
    window.handleMessageFromWatch);
};

window.handleAWSendMessage = function(clientEvent)
{
  console.log("handleAWSendMessage");
  // The value of queueName must match a value
  // used in a corresponding listener in Swift
  // or Objective-C within the watch app using
  // the method
  // MMWormhole.listenForMessageWithIdentifier
  // in order for the watch to receive the
  // sent message.
  var diceRoll = // 1 through 6
    Math.floor(Math.random() * (7 - 1)) + 1;
  var message = "rolled " + diceRoll;
  var queueName = "from_phone_queue";
  applewatch.sendMessage(
    message,
    queueName,
    function successHandler(foo)
    {
      console.log("send Success", foo);
    },
    function errorHandler(foo)
    {
      console.log("send Failure", foo);
    });
};
]]>
</amx:verbatim>
<amx:commandButton id="cb1" text="Init">
  <amx:clientListener id="cl1" type="action"
    method="window.handleAWInit"/>
</amx:commandButton>
<amx:commandButton id="cb2" text="Send Message">
  <amx:clientListener id="cl2" type="action"
    method="window.handleAWSendMessage"/>
</amx:commandButton>

25. Run the app.

If you get an error about “The operation couldn’t be completed. (LaunchServicesError error 0.)” then look for the hidden error details in this file:

~/Library/Logs/CoreSimulator/CoreSimulator.log

In the phone app, click “Init” then “Send Message” to see a message appear in the label in the watch app. In the watch app, click the button to see a message appear in the phone app.

Final Notes

Remember that you are working in the deploy folder of JDeveloper so the next time you deploy, you’ll lose all your edits to the Xcode project. While this process involves editing files of the “deploy” folder of the MAF app, you’ll need to take careful notes on what changes you make in the deployed folder in case you need to re-deploy in the future (and you will need to) since that would require you to perform these steps again. Perhaps there is some way to bundle all the WatchKit content into a Cordova plugin so you could completely avoid having to mess around with the deploy folder (I have not researched this but if you have an have a blog post to share with detailed instructions, please let me know so I can add a link to it in this post).

A real app wouldn’t expose an “init” button to the user so you’d want to move that init call into a “showpagecomplete” handler like the one seen in the MAF developer guide. Typically a JavaScript feature include file would be where this code would go (just like the custom component code seen in the CompGallery public sample app).

Advertisements

HTML Layout Direction

Posted on

Not every language uses a layout direction of left-to-right (LTR). Some use right-to-left (RTL). This is also known as the “reading direction” or “writing direction”.

At this time, browsers do not automatically apply dir=”ltr” nor dir=”rtl” to the HTML element for the appropriate layout direction. Adding this will change things like HTML tables re-ordering their cells horizontally and can be used in CSS files to apply alternative background images, flipped padding, margins, etc. using CSS selectors like this:

.foo {
  position: absolute;
  top: 10px;
}
html[dir=ltr] .foo {
  left: 10px;
}
html[dir=rtl] .foo {
  right: 10px;
}

There is no HTML5 API to to use the layout direction that comes from system settings. Strangely this HTML5 doc tells developers that they must manually add the direction even though in my opinion, the default should be that it comes from the system settings:
http://www.w3.org/International/questions/qa-html-dir

There is a dir=”auto” option:
http://www.w3.org/International/tests/repo/results/the-dir-attribute-auto#dirauto
but it has a few issues:

  1. Microsoft doesn’t support it.
  2. This is dependent on your page’s content instead of the system settings.
  3. This will prevent you from making CSS attribute selectors that resolve to “ltr” and “rtl” (it’ll be “auto” instead which defeats the purpose). There is a proposed CSS4 :dir(…) pseudo-class which would resolve auto to “ltr” and “rtl” but it is currently experimental and browser support is very weak.

Android 4.2 added support for right-to-left (RTL) layout:
http://android-developers.blogspot.com/2013/03/native-rtl-support-in-android-42.html

iOS also has its own mechanism for this:
https://developer.apple.com/library/ios/documentation/MacOSX/Conceptual/BPInternational/SupportingRight-To-LeftLanguages/SupportingRight-To-LeftLanguages.html

I was not able to find any Microsoft documentation that says whether UWP has anything equivalent to Android and iOS.

Unfortunately these disparate mechanisms are not abstracted into a unified, built-in Cordova API, the Cordova globalization plug-in, nor from what I can tell, any 3rd party plug-in.

This means you’ll either have to write your own Cordova plug-in using platform-specific code or alternatively add your own JavaScript code that uses the locale’s language code to determine whether you need to add dir=”ltr” or dir=”rtl” on your page’s HTML element.

Here is some sample JavaScript code to run when your page initially loads:

// Apply a "dir" attribute to the HTML element:
var bcp47Tag = ...; // e.g. "en-us", etc.
var scriptIndex = bcp47Tag.indexOf("-");
var language =
  bcp47Tag.substring(
    0,
    scriptIndex == -1 ? undefined : scriptIndex).toLowerCase();
if (language == "ar" || // Arabic
 language == "dv" || // Divehi
 language == "ha" || // Hausa
 language == "he" || // Hebrew
 language == "iw" || // Hebrew (legacy code)
 language == "ps" || // Pashto, Pushto
 language == "fa" || // Persian (Farsi)
 language == "ur" || // Urdu
 language == "ji" || // Yiddish (legacy code)
 language == "yi") // Yiddish
{
  document.documentElement.setAttribute("dir", "rtl");
}
else
{
  document.documentElement.setAttribute("dir", "ltr");
}