iOS 16 introduced new lock screen widgets! Rather that installing various third party widdgets, I want to create my own using Scriptable. Here is a simple “universal” widget to launch an application or a Shortcut (bonus: or even the home screen too!)
This is a tutorial on how to create a lock screen widget with Scriptable, which is free and does not collect any data. You will need Scriptable from the AppStore plus JavaScript experience! Here I go through a few things I tried... but if you want the TL;DR - just skip to the fourth script.
Aside: A gripe with the Apple’s lock screen widgets: the battery widget (circular), the fitness widgets (rings), the weather widget (guage) and other Apple widgets all look way too different! Can’t we have nice consistent circular widgets?
Lock Screen Widget to Launch an Application
Scritable has a great feature in that it can launch a URL from a lock screen widget - in essence this is a lock screen “launcher”, that’s all it is!
To configure this:
- customize your Wallpaper > lock screen from Settings (or any other way)
- then add a circular Scriptable widget,
- and finally, tap on the newly added widget
- this brings up a configuration page, where the When Interacting =
Open URL
option can be set:
When tapped, the widget can launch a deep link / universal link via a URL / URI Scheme to run the associated application (I’m aware I’m using these terms incorrectly as if they were interchangeable). In my case, WhatsApp’s URL Scheme is whatsapp://
but you can search the Internet for many other applications. Not voching for this list, but for example, TechRegister’s “Always-Updated List of iOS App URL Scheme Names & Paths for Shortcuts”
But we are getting ahead of ourselves: we first need a Scriptable script, which render the widget to display in the lock screen.
Script 1: Blank widget
For a very clean lock screen, simply use an invisible widget. This is also a great spacer if you want, e.g. to right align a single widget.
Simply use a minimum scaffold like so:
let widget = new ListWidget()
if (config.runsInAccessoryWidget)
Script.setWidget(widget)
else
widget.presentSmall()
//widget.presentAccessoryCircular()
Tips:
- When run from the lock screen,
runsInAccessoryWidget
is true andsetWidget()
is called - without the latter Scriptable displays an error message in the widget! - On the other hand,
widget.presentSmall()
shows the widget when run from from Scriptable itself - this can be dispensed with, but is helpful for testing. - I am aware there is
widget.presentAccessoryCircular()
is better for testing - IHMO the function does not render greyscale and transparency the way it should be.
Script 2: Text widget
For a widget to display text, the minimum code is:
let widget = new ListWidget()
let stack = widget.addStack()
stack.addText(Script.name).centerAlignText()
//stack.addText(Script.name)
if (config.runsInAccessoryWidget)
Script.setWidget(widget)
else
widget.presentSmall()
Tips:
- I don’t even need to hardcode the text - one can simply display the script name instead, using
Script.name
. - Funnily enough, it’s possible to use emojis in the text which render in full color!
Script 3: Widget with an in-line Icon
I figured why not just keep the code self-contained, with an “embedded” icon instead? Sure! Just encode the icon Base64 (text) first.
let widget = new ListWidget()
let stack = widget.addStack()
//stack.setPadding(8, 8, 8, 8)
//stack.backgroundColor = Color.gray()
let data = Data.fromBase64String("...base64...")
let image = Image.fromData(data)
stack.addImage(image)
if (config.runsInAccessoryWidget)
Script.setWidget(widget)
else
widget.presentSmall()
To convert an icon to its Base64 representation:
- On macOS, in Terminal, run
base64 -i image.png
. - On macOS, iOS or iPadOS, use the Shortcuts App
Base64 Encode
action.
I won’t post the icon nor its Base64 representation due to copyright, but my final lock screen widget output is as below. The Scriptable widget is the rightmost:
Regarding icons
For more details on the lock screen circular widgets, refer to Apple’s human interface guidelines for widgets.
Some notes regarding icons:
- Icons should be in greyscale, as system will only lock screen widgets in greyscale. I use a greyscale PNG.
- Icons are 76x76 pts. Assuming 2x or 3x retina display, icons need to be at least 152x152 or 228x228 pixels to remain sharp.
- The system will mask icons so that they are circular - anything at the corners will therefore be truncated.
- Opacity (transparency) controls how much of the background wallpaper will be visible (though blurred).
- Documentation states that “brighter gray values provide more contrast, and darker values provide less contrast.”
- Since many icons are black on transparent, just invert them so that the icon is solid white instead.
A few more tips:
- Unless you are in Dark Mode, running the script in Scriptable renders a white icon on a white background - in which case, nothing is visible! Uncomment then
backgroundColor
line to resolve this for easier testing. However, this is not required when running as a lock screen widget. - My icon is right to the edge of the space given - if you want to programmatically add padding, use
setPadding()
.
BTW I did try SFSymbol
in Scriptable - unfortuantely, I cannot find a way to easily convert a symbol to an image
with a White foreground instead of Black.
Script 4: Widget that loads a PNG icon file
The previous script requires effort to encode icons in every Script but also means there are no dependencies to distribute. Alternatively...
Here is how to dynamically load a PNG image. The PNG needs to be in the iCloud Drive Scriptable folder (use the Files application > under locations, iCloud Drive):
let widget = new ListWidget()
let stack = widget.addStack()
let param = args.widgetParameter
if (param?.endswith(".png")) {
let files = FileManager.iCloud()
let file = filesdocumentsDirectory() + "/" + param
files.downloadFileFromiCloud(file)
let image = f.readImage(file)
stack.addImage(image)
} else {
let text = stack.addText(param ?? Script.name())
text.font = Font.headline()
text.centerAlignText()
}
if (config.runsInAccessoryWidget)
Script.setWidget(widget)
else
widget.presentSmall()
This time, make sure that after you add the widget, either:
- Configure the Parameter field with an image filename of your choice which must end with
.png
. - Or just specify text to display, leaving it empty (null) will default to the Scriptable script name, and a space will be an icon-less (invisible) widget.
Tips:
- It’s possible to use the
FileManager.local()
folder instead - I use the iCloud folder simply because that is were Scriptable stores all.js
scripts. - Note the Scriptables
readImage
documentation states that:
The function will error if the file does not exist or if it exists in iCloud but has not been download. Use
fileExists(filePath)
to check if a file exists anddownloadFileFromiCloud(filePath)
to download the file. Note that it is always safe to calldownloadFileFromiCloud(filePath)
, even if the file is stored locally on the device.
Launch a Shortcut
How about running Shortcuts from widgets? Sure! There is an URL Scheme to do just that, as documented in Apple’s Shortcuts User guide
Just use shortcuts://run-shortcut?name=[name]&input=[text|clipboard]&text=[text]
where name is the exact name of the Shortcut. Remember to URL encode, e.g. ` (space) is encoded as
%20. I do not use
inputand
text` parameters myself, but read the documentation to understand what they are for.
Alas, the Scriptable CallbackURL()
function is not supported in a widget, otherwise I could use the x-callback-url
with Shortcuts to return values from Shortcuts and display them in a widget.
Bonus: Launch SpringBoard (Home Screen)
One final “launcher” - how about launching SpringBoard instead? Often this is referred to as “exit to iOS Home Screen”, but in this contents its more accurately tap-to-unlock, as opposed to swipe-up-to-unlock.
I hate swiping from the very bottom of the lock screen to unlock my phone, since I cannot easily reach it with my thumb one-handed. I would rather have an widget... Scriptable has granted my wish!
Enter an undocumented Scriptable function and a Scriptable script with just this single line:
App.close()
This time, When Interacting is set to Run Script
:
There will be a flash as the phone unlocks, opens Scriptable, runs runs the script above, which then forces Scriptable to close. If Reduce Motion is turned on, this is less noticeable.
BTW, obviously this script will not show any icon in the widget, but now you know a few methods to draw text or icons, this is left as an excercise for the reader!
Have fun!
Small update 14 Dec 2022: Bugs!