Since version 14 iOS supports different home screen widgets which can display different useful information for your installed apps.
Wouldn't it be cool if you can create your own widgets to display some custom information? Good news, with the great Scriptable iOS app you can create your own scripts and widgets for iOS using pure javascript.
The widget 📱
Reading some dev.to articles are an integral part of my daily routine, so I thought wouldn't it be cool if I get a random article directly on my home screen.
So I made a little script which fetches a random article out of the top ten articles of the day on dev.to based on the tags I have subscribed to.
After that, the article details like title, author, profile image and tags are displayed in a medium-sized widget. You can see the final widget in the following image.
Get the widget 🚀
If you want the widget on your home screen you will need to download the Scriptable iOS on your device. After that, you can create a new script inside the app and copy and paste the code from the following gist into your created script.
To fetch the dev.to articles you need to generate an API key in your dev.to settings and paste it at the beginning of the created script.
Now you can preview the widget by starting the script inside the Scriptable app.
Finally, you can add the widget to your home screen by adding a medium-sized Scriptable widget, after that you can configure the widget by long-pressing the empty widget and then choose the newly created script in the widget configuration.
I hope you like my widget, feel free to write to me if you encounter any issues or want to suggest some improvements.
const API_KEY = "paste API key here"; | |
const ARTICLE_PARAMS = [{ per_page: "10" }, { top: "1" }]; | |
let widget = await createWidget(); | |
// Check where the script is running | |
if (config.runsInWidget) { | |
// Runs inside a widget so add it to the homescreen widget | |
Script.setWidget(widget); | |
} else { | |
// Show the medium widget inside the app | |
widget.presentMedium(); | |
} | |
Script.complete(); | |
async function createWidget() { | |
// Create new empty ListWidget instance | |
let listwidget = new ListWidget(); | |
// Configure a new background gradient | |
let gradient = new LinearGradient(); | |
// Set the color stops at the beginning and at the end of the gradient | |
gradient.locations = [0, 1]; | |
// Set the colors of the different color stops | |
gradient.colors = [new Color("#232526"), new Color("#414345")]; | |
// Set the start and the end point of the gradient | |
// Points are defined as x and y position between 0 and 1 | |
gradient.startPoint = new Point(0.5, 0); | |
gradient.endPoint = new Point(0.5, 1); | |
// Add the gradient to the widget | |
listwidget.backgroundGradient = gradient; | |
// Get a random artice | |
const article = await getArticle(); | |
// Add the article url to the widge so it will be opened when the widget is clicked | |
listwidget.url = article.url; | |
// Add the heading of the article to the widget | |
let heading = listwidget.addText(article.title); | |
heading.font = Font.semiboldSystemFont(28); | |
heading.textColor = new Color("#ffffff"); | |
heading.minimumScaleFactor = 0.5; | |
// Add vertical spacing between the heading and the footer stack | |
listwidget.addSpacer(); | |
// Add the footer stack which holds the author information and the dev.to logo | |
const footerStack = listwidget.addStack(); | |
footerStack.bottomAlignContent(); | |
// Add a stack to the footerstack to hold the profile information | |
const profileStack = footerStack.addStack(); | |
profileStack.topAlignContent(); | |
// Get the article authors profile image and add it to the stack | |
const image = await getImage(article.user.profile_image_90); | |
let profileImage = profileStack.addImage(image); | |
// Configure the profile image | |
profileImage.imageSize = new Size(50, 50); | |
profileImage.cornerRadius = 25; | |
profileImage.borderWidth = 4; | |
profileImage.borderColor = new Color("#bfbfbf"); | |
// Add spacer between the profile image and the username stack | |
profileStack.addSpacer(10); | |
// Add stack for the username and the article tags | |
const nameStack = profileStack.addStack(); | |
nameStack.layoutVertically(); | |
// Add and configure the username | |
const username = nameStack.addText(article.user.name); | |
username.textColor = new Color("#ffffff"); | |
username.font = Font.lightSystemFont(17); | |
// Add and configure the article tags | |
const taglist = nameStack.addText( | |
prefixTags((tags = article.tag_list), (prefix = "#")) | |
); | |
taglist.textColor = new Color("#bfbfbf"); | |
taglist.font = Font.semiboldSystemFont(13); | |
// Add spacer between the username stack and the dev.to logo | |
footerStack.addSpacer(); | |
// Get the dev.to logo and add it to the logo stack | |
const devLogo = await getImage( | |
"https://d2fltix0v2e0sb.cloudfront.net/dev-ecosystem.png" | |
); | |
const logo = footerStack.addImage(devLogo); | |
// Configure the logo | |
logo.imageSize = new Size(30, 30); | |
// Return the created widget | |
return listwidget; | |
} | |
function constructQueryString(parameters) { | |
// Iterate over the parameters and construct the query string by joining them | |
return ( | |
"?" + | |
parameters | |
.map((param) => { | |
key = Object.keys(param)[0]; | |
return key + "=" + param[key]; | |
}) | |
.join("&") | |
); | |
} | |
async function getArticle() { | |
// Get the tags which the user has subscribed to | |
const tags = { tags: await getTags() }; | |
// Append the default query parameters with the tags of the user | |
const parameters = ARTICLE_PARAMS.concat(tags); | |
// Construct the query url | |
const url = "https://dev.to/api/articles" + constructQueryString(parameters); | |
// Initialize new request | |
const request = new Request(url); | |
// Add authentication header to the request | |
request.headers = { "api-key": API_KEY }; | |
// Execute the request and parse the response as json | |
const response = await request.loadJSON(); | |
// Choose a random article | |
const article = response[Math.floor(Math.random() * response.length)]; | |
// Return the article | |
return article; | |
} | |
async function getImage(url) { | |
// Initialize new request | |
const request = new Request(url); | |
// Add authentication header to the request | |
request.headers = { "api-key": API_KEY }; | |
// Execute the request and parse the response as image | |
const image = await request.loadImage(); | |
// return the image | |
return image; | |
} | |
async function getTags() { | |
// Initialize new request | |
const request = new Request("https://dev.to/api/follows/tags"); | |
// Add authentication header to the request | |
request.headers = { "api-key": API_KEY }; | |
// Execute the request and parse the response as json | |
const response = await request.loadJSON(); | |
// Get the names of the returned tags and join them to a string | |
const tags = response.map((tag) => tag.name).join(); | |
// return the created string | |
return tags; | |
} | |
function prefixTags(tags, prefix) { | |
return tags | |
.map((tag) => prefix + tag.trim()) | |
.join(" "); | |
} |
Top comments (8)
Have you had much experience with the scriptable app?
No it was the first time I created a widget with Scriptable but the API is very straightforward.
I plan to publish another article in the next few days on how to create iOS widgets with Scriptable if you are interested in that.
In would be very interested! I found the Scriptable app last week but was intimidated to code on the phone. Are you able to develop on a desktop?
The answer is it depends, when you use macOS you can use the Scriptable Mac Beta to code on desktop. Another solution could be the iCloud sync of Scriptable --> edit the script on your desktop inside the iCloud folder and test the automatic synced script on your iPhone (never tested the second solution myself)
Thankfully I develop on a Mac! I will have to try that beta app out, thanks!
It took a while because I had a few stressful weeks but now the article is finally ready.
FYI
Create your own iOS widget with JavaScript
Matthias ・ Jan 26 ・ 6 min read
Thank you so much for sharing! It works great!
Hey Nick,
thanks for the kind reply, I'm glad you like it