Help with fm.downloadFileFromiCloud

Hi all,

I have been using Simonbs’s code for launcher widget in the Widget example thread. I am just having difficulty with the widget, as it stops working after a few hours as the icon pictures need redownloading in iCloud.

I have tried adding fm.downloadFileFromiCloud but am unable to get it to work the original code from simonbs is below.

Any help would be appreciated!


const IMAGE_NAME = "IMAGE_NAME"
const NAME = "NAME"
const URL = "URL"

const actions1 = [
  newAction(
    "Text Gro",
    "whatsapp.png",
    "sms://PHONE-NUMBER"
  ),
  newAction(
    "Inbox",
    "things.PNG",
    "things:///add?show-quick-entry=true&use-clipboard=replace-title"
  )
]
const actions2 = [
  newAction(
    "New Mail",
    "spark.png",
    "readdle-spark://compose"
  ),
  newAction(
    "Hack",
    "slack.png",
    "slack://channel?team=TPK2S68PN&id=CPWLL7J0P"
  )
]

let g = new LinearGradient()
g.locations = [0, 1]
g.colors = [
  new Color("#37474f"),
  new Color("#455a64")
]

let w = new ListWidget()
w.backgroundGradient = g
w.setPadding(0, 10, 0, 10)
w.addSpacer()
addRow(w, actions1)
w.addSpacer()
addRow(w, actions2)
w.addSpacer()
w.presentMedium()

function addRow(w, actions) {
  let s = w.addStack()
  for (let i = 0; i < actions.length; i++) {
    let a = actions[i]
    let image = getImage(a[IMAGE_NAME])
    let name = a[NAME]
    let container = s.addStack()
    container.layoutHorizontally()
    container.centerAlignContent()
    container.url = a[URL]
    container.addSpacer()
    
    let wimg = container.addImage(image)
    wimg.imageSize = new Size(50, 50)
    wimg.cornerRadius = 11
    container.addSpacer(8)

    let wname = container.addText(name)
    wname.font = Font.semiboldRoundedSystemFont(17)
    wname.textColor = Color.white()
    
    if (i < actions.length - 1) {
      container.addSpacer()
    }
    container.addSpacer()
  }
}

function newAction(name, imageName, url) {
  return {
    IMAGE_NAME: imageName,
    NAME: name,
    URL: url
  }
}

function getImage(imageName) {
  let fm = FileManager.iCloud()
  let dir = fm.documentsDirectory()
  let filePath = fm.joinPath(dir, "/imgs/launcher/" + imageName)
  return fm.readImage(filePath)
}

modify the getImage function like so

You were going into the right direction with fm.downloadFileFromiCloud(), but I totally understand you that it is not obvious where to add it.

I will walk you through two possibilities.

The first and most obvious would be to add it to the getImage() function.

async function getImage(imageName) {
  let fm = FileManager.iCloud()
  let dir = fm.documentsDirectory()
  let filePath = fm.joinPath(dir, "/imgs/launcher/" + imageName)
  await fm.downloadFileFromiCloud(filePath)
  return fm.readImage(filePath)
}

Since the download function returns a Promise, we need to await it to know it has finished downloading. We also need to declare the function getImage() as being asynchronous (also called async) because we await a Promise in it. To do this I’ve added async keyword in front of the first line. This in turn makes the function itself return a Promise, so we need to await it where we use it:

async function addRow(w, actions) {
  let s = w.addStack()
  for (let i = 0; i < actions.length; i++) {
    let a = actions[i]
    let image = await getImage(a[IMAGE_NAME])
    // I've removed the rest of this function because it stays the same

After adding the await for the getImage() function call, we again need to declare the current function addRow() as being async and we again add await wherever we use addRow():

let w = new ListWidget()
w.backgroundGradient = g
w.setPadding(0, 10, 0, 10)
w.addSpacer()
await addRow(w, actions1)
w.addSpacer()
await addRow(w, actions2)
w.addSpacer()
w.presentMedium()

Since we have now reached the top level and therefore are not in a function, there is no need to repeat this procedure of adding await and async.

This will work as it should, but let’s step back for a second and think through what the code does in terms of loading images.

The script does:

  • create a widget
  • add all predefined “rows”
  • each row can contain multiple images
  • for each image, it calls fm.downloadFileFromiCloud and waits until it has finished downloading

There is a small problem in the last bullet point. The script waits until the image has been downloaded and then continues to start downloading the next one. Well, it isn’t really a problem, because the script still finishes eventually and displays the widget, but we could write the code a little better, potentially making the script faster and more readable.

What do I mean by more readable? If you look at the last change

await addRow(w, actions1)

and you’ve forgotten how the function addRow() is implemented, you will probably wonder: why do I have to await the function? Adding a row to a widget should not take that long… Of course this is answered when looking at the code of the function, but that takes time to find the function and read the code.

So let’s do it with a different approach. What if we don’t load the image when we want to use it, but before hand? That would look something like this (I’ve left in the declaration of the actions to give you a point of reference):

const actions1 = [
  newAction(
    "Text Gro",
    "whatsapp.png",
    "sms://PHONE-NUMBER"
  ),
  newAction(
    "Inbox",
    "things.PNG",
    "things:///add?show-quick-entry=true&use-clipboard=replace-title"
  )
]
const actions2 = [
  newAction(
    "New Mail",
    "spark.png",
    "readdle-spark://compose"
  ),
  newAction(
    "Hack",
    "slack.png",
    "slack://channel?team=TPK2S68PN&id=CPWLL7J0P"
  )
]

// get all image names in an array
// since this a nested array, we need to flatten it in the end
// if you add more rows, you need to add them to this array
const images = [actions1, actions2].map(row => 
  row.map(item => item.IMAGE_NAME)
).flat()
// download them
let fm = FileManager.iCloud();
await Promise.all(
  images.map(img => 
    fm.downloadFileFromiCloud(
      fm.joinPath(fm.documentsDirectory(), "/imgs/launcher/" + img)
    )
  )
)

As you can see, it is not only much less to edit and to look out for (= much less error prone), but also much quicker to read and to understand, even without the comments. It is potentially quicker than the first method because all images are downloaded at the same time. We still need to download all images and wait for the last one to finish, but this is done in parallel.
If you add more rows, you need to add them to the array where indicated, so it’s a little redundant. This can also be fixed, however it wasn’t part of the question.

1 Like

Thank you for the help and for explaining how this could be achieved, this is perfect for a JS Novice like myself.