The Line Between

2019-05-14✪Permalink

I’m currently reading The Gulag Archipelago by Aleksandr Solzhenitsyn (the abridged version). While a gruelling read at times, it’s certainly a worthwhile one. His writing manages to hold together within it so much dark and light. Sometimes it even makes you laugh:

Marx … declared with equal conviction that the one and only means of correcting offenders … was not solitary contemplation, not moral soul-searching, not repentance, and not languishing … but productive labor. He himself had never in his life taken a pick in hand. To the end of his days he never pushed a wheelbarrow, mined coal, felled timber, and we don’t even know how his firewood was split – but he wrote that down on paper, and the paper did not resist.

But it was this passage that I was recently struck by:

Gradually it was disclosed to me that the line separating good and evil passes not through states, nor between classes, nor between political parties either – but right through every human heart – and through all human hearts. This line shifts. Inside us, it oscillates with the years. And even within hearts overwhelmed by evil, one small bridgehead of good is retained. And even in the best of all hearts, there remains … an unuprooted small corner of evil.

I think this is a lesson we need to re-learn in our present times. Increasingly we seem to police one another’s speech and thought against the modern heresies, and we are only too quick to excommunicate. The motivation is what it has always been: to root out evil. But the mistake is also what it has always been: to measure the demarcation according to affiliations – good and evil defined by what party you support, whether you voted leave or remain, by your moral or religious beliefs.

The struggle is one in which we all need to be engaged, but each of us needs to recognise that we have an allegiance to both sides.

Cacio e Pepe

2019-05-11✪Permalink

The dishes I most love to cook are the deceptively simple ones. Typically they contain very few ingredients, but require care and attention to master. It also helps if they’re considered a classic of their cuisine. I don’t know what it says about me, but I’ve always been drawn to those areas of culture where there is considered to be a right way of doing things.

I recently discovered a dish which very much ticks all of these boxes: the Roman speciality cacio e pepe, literally cheese and black pepper. It consists of just three ingredients: spaghetti, pecorino cheese, and black pepper1. The inherent problem that this dish presents is that its sauce is a blend of cheese and water, two substances which don’t in the normal course of events prefer to intermingle. Done wrong – and I did so many times – you end up with lumps of cheese with the consistency of wet chewing gum.

The key to avoiding this is to understand the importance of the starchy pasta water. The starch helps the water and the cheese to mix together properly to make the proper sauce, and a higher concentration really helps.

Here is my reasonably reliable recipe:

  • Boil a pot of water. Use less than you would usually use for pasta. Add salt.
  • Grate the pecorino cheese. It needs to be very finely grated, almost a powder. Use the side of a box grater which has protruding spiky holes.
  • Once the water has boiled, add the spaghetti and cook roughly half way to al dente.
  • Heat a frying pan, grind in lots of black pepper and toast briefly.
  • Add the spaghetti to the pan, along with some of the water. Slowly add water as it reduces. This increases the concentration of starch in the water.
  • Add some water to the grated cheese in a bowl, stir to form a smooth paste.
  • When the pasta is ready and the water is mostly reduced, remove from the heat and stir in the cheese. If all goes well they should blend together to form a creamy sauce.

One of the best YouTube videos for this recipe I came across was this one by a rather engaging Frenchman named Alex.

Buon appetito!

  1. Five if you include water and salt. 

Grader+

2019-04-20✪Permalink

Today I’m very excited to announce the release of my first ever iOS app, Grader+. It’s a simple utility app, primarily designed for teachers marking tests and exams. It’s available on the App Store now as a free download, with a $0.99 in-app purchase to unlock the premium features.

My motivation for creating it was twofold. Firstly, it’s an app that I wanted to exist so I could use it myself. The way I mark, I go through tests question by question, writing the number of marks for each question on the paper. At the end, I then need to add up the marks for each student to work out their total mark and then record it. Grader+ is designed to help with the adding up and recording part of that. Now you might think – as a maths teacher – that I would be good at adding up, but in practice I would frequently lose count and have to start again, especially when I had to stop and re-mark something I had missed along the way. Other options include using a calculator, and just pressing the plus button every time, but that doesn’t solve the recording problem; or recording individual marks directly into a spreadsheet, a technique that can be useful if you want to analyse the breakdown of marks question by question, but is also time-consuming and difficult to do on the go. What I wanted was a way to record an individual mark with a single tap, and then save each mark quickly and easily. That core functionality was the inspiration of this app, and then the other features grew from there.

Secondly, my motivation for wanting to build an iOS app from scratch was to begin to properly learn the Swift programming language and the ways it can be used to build apps. I also wanted to get to grips with Xcode, the Mac app one uses to actually put the parts of the app together. I learnt a lot of the fundamentals of iOS app design from this process, especially UI design – a part of programming that I had never done before. I’ll talk more below about some of the resources I’ve found most helpful.

Grader+ Features

Firstly, I want to run through the basic features of the app and how it’s intended to be used, and I think it makes sense to do this screen by screen.

Counter

The counter tab of the app is where marks can be entered. By default it counts up, but this can be changed in settings if you are used to beginning from a maximum score and taking marks off. Alongside the total so far, the counter display also shows the last few marks entered (to help you keep track) as well as percentages1, grades2, and ranking. Saving a score will prompt for a student name, which is then saved, along with the score, to the scores tab.

Scores

The scores tab is where scores are recorded, and can be configured in settings to display percentages and grades and to sort the results in various ways. There is also an option to display statistics for all of the scores. The first of the premium features is available on this screen in the share button, which allows you to export the scores to a CSV file for easy importing to a spreadsheet, which is where these things usually end up.

Grades

This screen allows you to create and edit grade boundaries. They can be be entered as percentages or as marks3. The percentage for each one is the minimum threshold at which a grade should be awarded.

Settings

I’ve mentioned most of the settings already, but one notable one I haven’t is the other premium feature: dark mode! Why? Because it looks cool.

Learning Swift, iOS, and Xcode

There are a lot of great resources out there for learning how to make iOS apps, but I wanted to mention a few of the ones I found most useful. As a total beginner, I wanted to see someone actually make an app from scratch in Xcode to see what the process involved, and I discovered a course from Stanford University called Developing iOS 11 Apps with Swift, available on iTunes U, as a podcast feed, and also on YouTube. I watched the entire series, completing some of the assignments as I went along, and the lecturer, Paul Hegarty, does a really good job of explaining the ideas and techniques. The course does assume knowledge of the concepts of object-oriented programming, so go and do some reading on that first if you’re not familiar.

When I got stuck, I would google my issue, and I frequently found the solutions on one of the following websites:

In particular, I would not have been able to have figured out in-app purchases without this great tutorial.


I hope you enjoy using the app. It’s available now on the App Store, and if you want to support its development, and the development of future apps, please do unlock the premium features and throw a dollar (or a pound) my way.

  1. Requires a maximum score to be entered in Settings 

  2. Requires grade boundaries to be entered in the Grades tab 

  3. The latter only if the app knows the maximum score 

The Fragility of iMessage Conversations

2019-03-14✪Permalink

A couple of times recently, I’ve had a family member or a friend come to me with a problem. At first it seems like no big deal: they accidentally deleted an iMessage conversation, and they just need to get it back. It might have some information they need, or it might have sentimental value. They look in vain for an undo button; dare I say it, they might even shake their phone. But to no avail. When I break the news to them that there is basically no solution, or that the solutions that do exist involve considerable inconvenience or drawbacks, they can be distraught, and rightly so.

Our text messages are the form of communication we have that most closely mirrors our everyday interactions with the people in our lives. The archive of these snippets of text, stretching out into the past, tells a story of the mundane and the intimate that is our lived experience. For some, the brevity of these messages makes them essentially ephemeral: a trail of crumbs we leave behind us, never returning to pick up. But for others, the unique record that they hold makes them incredibly precious, perhaps as precious as photographs, particularly when it comes to loved ones who have passed away.

With this kind of data, it’s our natural assumption that the more precious it is, the better it should be protected. Apple has made a lot of progress in this regard with services like iCloud Photo Library, which remove a lot of the worry of losing or breaking a device.1 However, it’s my contention that iMessage is where Apple is currently getting this the most wrong. Our iMessages conversations are highly valuable to us as irreplaceable personal data, but they are also far too easy to accidentally delete and very difficult to recover. A swipe and a couple of taps is all it takes.2

iCloud backup had previously solved the problem of losing your messages when you lose your phone. Since messages were stored in your iCloud backup, restoring a backup to a new device would restore all of your messages as well. Some kind of answer involving iCloud backups is what you will most often find online when you search for solutions to accidental deletion of conversations. But depending on when your last backup was, restoring your phone using a backup is likely to result in other data loss, since messages received after the time of the backup would not be contained within it. Other suggested solutions involve downloading third-party software which offers to take an unencrypted iTunes backup of your device and go digging around for your lost data. I won’t bother explaining why that’s a bad idea.

With iOS 12, the situation changed, or at least it could change. iOS 12 introduced the option – by default disabled – to store iMessages3 in iCloud (by which I mean iCloud storage not iCloud backup). In some ways, this makes things better. My full archive of messages is no longer preserved only in the iCloud backup of my oldest device. If I get a new device, or wipe and restore an old device, that archive is now synced in full. That sense of permanence, independent of a particular device, is what encouraged me to turn this option on and to recommend to others to do the same.

However, it also introduces a considerable downside. Deleting a conversation now results in it being deleted across all devices. And since it’s no longer stored in an iCloud backup, winding back the clock by restoring a backup does nothing. In 2019 I really think it’s unacceptable that an ordinary user can accidentally delete some incredibly important data and have no way to easily recover it.

So, what’s the solution to this problem? As I see it, there are two pretty simple ways Apple could improve the user experience in this regard. One is the humble undo button – even shake to undo would be better than nothing. Or they could go the way of iCloud Photo Library, and have a section where deleted conversations live for 30 days before permanent deletion. The other solution would be to have something on iCloud.com, which currently offers the ability to restore deleted files in iCloud Drive, contacts, calendars and reminders, and last but not least, Safari bookmarks!4 The fact that it is easier to restore an accidentally deleted bookmark than it is to restore all of the messages one has ever exchanged with one’s spouse, say, is just absurd.

Apple is generally pretty good at knowing the kind of digital belongings we value most and helping us to protect and make the most of them. iMessage is one where they are failing us. Let us restore them, let us back them up, and maybe let us export them to other formats without having to expose our unencrypted backups to third-party software.

  1. Insert the usual disclaimers about free iCloud storage being meagre and the importance of proper backups. 

  2. I’ve also seen people try to delete one message and hit the “Delete All” button; there is a confirmation dialogue, but it’s all too easy to breeze through without thinking. 

  3. I don’t know whether SMS are included in this. 

  4. Thank God that my bookmarks are safe from the capricious wrath of the delete button. 

Siri Shortcuts and Transport APIs

2019-03-11✪Permalink

I haven’t written anything about Shortcuts since it was released last September, so I thought it was about time I share a few of the shortcuts I’ve had fun making recently and which I’m finding myself using almost every day. All of them leverage the advantages that came with the transition from Workflow, in particular the ability to both trigger them from Siri, and for Siri to natively respond with the output of the shortcut using the show results action.

They’ve also given me the opportunity to try two things I’ve been meaning to play around with: the Transport for London API, and the wonderful JSON parsing app Jayson. The TfL API is an amazingly comprehensive web API that gives information about public transport services all over London. I wanted to use it to build shortcuts related to common journeys I take.1

Line Status Report

The first one I built to allow Siri to tell me the status of the tube line I live on. So that I could experiment with the various URL endpoints offered by the TfL API, I built a small utility shortcut which takes a URL and uses the Jayson “View JSON in Notification” action so that I can quickly see the results by dragging down on the notification that appears. Jayson is one of the apps that has really embraced the rich notifications that were introduced in iOS 12.

JSON Shortcut, JSON in rich notification, JSON in Jayson

Running this shortcut with the URL https://api.tfl.gov.uk/Line/Mode/tube returns eleven JSON objects, each corresponding to one of the tube lines in London. For example, here is the raw JSON returned for the Bakerloo Line:

{
  "$type":"Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities",
  "created":"2019-03-05T14:58:26.59Z",
  "crowding":{
    "$type":"Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities"
  },
  "disruptions":[

  ],
  "id":"bakerloo",
  "lineStatuses":[

  ],
  "modeName":"tube",
  "modified":"2019-03-05T14:58:26.59Z",
  "name":"Bakerloo",
  "routeSections":[

  ],
  "serviceTypes":[
    {
      "$type":"Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities",
      "name":"Regular",
      "uri":"\/Line\/Route?ids=Bakerloo&serviceTypes=Regular"
    }
  ]
}

Since I want to check the status of a particular line, the part I need here is the id key. Now I can query the URL https://api.tfl.gov.uk/Line/bakerloo/Status, which returns the following JSON data:

[
  {
    "$type" : "Tfl.Api.Presentation.Entities.Line, Tfl.Api.Presentation.Entities",
    "created" : "2019-03-05T14:58:26.59Z",
    "crowding" : {
      "$type" : "Tfl.Api.Presentation.Entities.Crowding, Tfl.Api.Presentation.Entities"
    },
    "disruptions" : [

    ],
    "id" : "bakerloo",
    "lineStatuses" : [
      {
        "$type" : "Tfl.Api.Presentation.Entities.LineStatus, Tfl.Api.Presentation.Entities",
        "created" : "0001-01-01T00:00:00",
        "id" : 0,
        "statusSeverity" : 10,
        "statusSeverityDescription" : "Good Service",
        "validityPeriods" : [

        ]
      }
    ],
    "modeName" : "tube",
    "modified" : "2019-03-05T14:58:26.59Z",
    "name" : "Bakerloo",
    "routeSections" : [

    ],
    "serviceTypes" : [
      {
        "$type" : "Tfl.Api.Presentation.Entities.LineServiceTypeInfo, Tfl.Api.Presentation.Entities",
        "name" : "Regular",
        "uri" : "\/Line\/Route?ids=Bakerloo&serviceTypes=Regular"
      }
    ]
  }
]

To get the human-readable status, I need to take the value from the lineStatuses key, then the first item of that array, then the value from the statusSeverityDescription key. In the shortcut I built to do this, I then have a few conditional statements to change the wording of the Siri’s response in the most common cases, so that it sounds nice and makes grammatical sense. So for example, if the API returns Good Service I have Siri respond with “There is currently a good service on the [line name]”, but if there are delays, she instead says “There are currently [type of delays] on the [line name]”. In other cases, I fall back to a default, “The current status of the [line name] is: [status]”. At some point, I should probably add some other conditionals for occasional statuses like Part Suspended.2

Line Status Report in Siri

Here’s the finished shortcut. The import question will ask you which line you want to use, and you just need to drag the one you want to the top of the list. Then you can add it to Siri with whatever phrase you like.

Arrivals Information

This isn’t the case on most of the London Underground network, but on the route I take to and from work, the trains are actually relatively infrequent. I have a ten minute walk from my work to the nearest station, and so ideally I want to time things so that I arrive a minute or two before my train arrives. I wanted to build a shortcut that would allow Siri to tell me how long it is until my train arrives. To do this, I used the URL endpoint https://api.tfl.gov.uk/StopPoint/{id}/Arrivals, with the id of the station near my work. I first had to find out what this id was, so I used the endpoint https://api.tfl.gov.uk/Line/{line id}/StopPoints to list all of the stations, along with their ids, on a given line. This is a case where Jayson really comes into its own. When you open some JSON data in the app, you can use the key button in the top right to display the value for a given key for each element of an array. So using the previous URL with the id for the Bakerloo line, and opening the data in Jayson, I can use the key button to display the values for the key commonName. This makes it much easier to find the station I want, and then drill down to identify its id.

Jayson key tool

Armed with this information, I can now build the query to get the arrivals information I need. But since not all the trains arriving at the station go in my direction, I need to filter the results I get. Each item in the array returned from the arrivals endpoint represents an approaching train, and each has a key called destinationNaptanId whose value is the station id of the terminus. I wanted to filter the results using this key to reduce the list to only those going in my direction.

In the Shortcuts app, working with lists can be a little awkward at times, and things like sorting and filtering are not natively supported. However, there’s a relatively elegant way to implement a filter operation using the Repeat with Each block. Within the repeat block, you add an if block, and within the if block you put a Get Variable action with Repeat Item as the variable name. In the Otherwise section of the if block, you put the Nothing action. This means that the if block passes through the input when it passes the condition and passes nothing when it doesn’t. It’s perhaps not intuitive that it works this way, but the output of a repeat block is actually a list of all the outputs produced at the end of each repetition – in this case, only the inputs which matched the condition.

Once I’ve filtered the results, I count how many there are. If zero, Siri responds with a default “No trains currently due”. If there are more than one, I take the first and find that value of of the key timeToStation which is given in seconds, divide by 60, and round it down to the nearest minute. This is then the value that Siri replies with using the Show Results action.3

Time to Next Train shortcut

Here’s a link to the finished shortcut.

Building these shortcuts was a lot of fun, and was made much easier by the wonderful Jayson app, without which it would have involved a lot of my poring over screeds and screeds of un-indented raw JSON data. I use these actions every day to get to and from work, and being able to ask Siri “When is the next train home?” and have her quickly reply is so much better than launching an app to enter the same journey information every time.

  1. In the API documentation, it says, “To use the Unified API, developers should register for an Application ID and Key. Append the app_id and app_key query parameters to your requests.” In practice I’ve found this isn’t necessary for the very low volume of requests I’ve been making. 

  2. One of the reasons I haven’t is that the Shortcuts app still doesn’t offer an else if option in conditional blocks, necessitating awkward nested blocks as a workaround. 

  3. In my version of this shortcut, I put in the line status shortcut at the end with a Run Shortcut action, so that Siri also tells me about any disruptions after telling me about when my train is. 

Unprofessional Development ↪︎

2018-08-24✪Permalink

I’m very excited to announce that my former colleague Luke Pearce and I have just released the pilot episode of our podcast called Unprofessional Development. The tag line is sometimes teaching, always learning, and that’s exactly what we’ll be discussing. We’re both maths teachers who want to continue to develop our knowledge and skills in teaching, but we both also have a passion for learning things outside of our jobs.

With the end of the summer holidays looming, this episode is entitled Back to School, and in that we discuss how best to teach the first lessons of the year. We hope that you enjoy it. You can find us in Apple Podcasts, Overcast, and lots of other places.

We also have a new twitter account @unprofdev, where you can send us feedback on the show and hear about future episodes.

Airtable API for Drafts

2018-08-08✪Permalink

I experimented last year with using Airtable as my markbook1. For some reason I just find entering data into spreadsheets so incredibly dull, and the results so incredibly uninspiring, but at the same time I fully recognise that recording student data is a valuable thing to do, both for my teaching itself, and for more mundane things like writing reports. When I came across Airtable, it struck me as much more appealing, both visually and functionally. It’s a cross between a spreadsheet and a database, and it supports a large variety of different kinds of data such as files, images, and more. Its use of coloured labels and image thumbnails also makes it a much more visual experience. It also comes with a native app for iOS with native controls like switches and buttons, which makes for a really nice experience.

But as nice as the app is, both on iPhone and iPad, Airtable is primarily a web application, and the iOS app lacks a lot of the features that the web interface has. On top of that, the web interface suffers from the curse of mobile Safari. Even if you hold down on the refresh button and hit “Request Desktop Site”, basic things like scrolling just don’t work.

There are a few features I would like to see coming to the iOS app, but for the most part, it’s just basic things that involve too many taps that I would like to see improve. For example, I have a table in my Airtable base that represents pieces of homework my students have done. When I set a new piece of homework, I want to create a whole bunch of rows with the same topic, but each associated with a different student. On the web app, you can do that quite easily by copying and pasting cells, but on iOS you need to create each new row one-by-one.

What I wanted was to be able to use the Airtable app as a visually appealing front end, but to have more power on the back end of the database to add and manipulate data from iOS. But instead of building the tools to manage my markbook, I decided instead to build the tools to build the tools. Why go to such effort? Isn’t this just adding additional layers of procrastination?2 you might quite reasonably ask – to which I would probably avoid the question and attempt to change the subject. However! I did have a few reasons for wanting to do this.

Firstly, I have been working on my programming skills3, and writing a programme to interact with a database on a server is an incredibly common programming task. It brings with it common challenges such as syncing data between the local client and the remote server, formulating requests to send to the server, and parsing data from its responses into a usable format. Secondly, I wanted to create something that others could make some use of. The tools I will make for my own purposes will be far too specific for that, but building a general-purpose system has more potential use cases for a larger number of people. Thirdly, it would make my own tools easier to maintain and modify as need be, since the meta-tools from which they are built abstract away much of the complexity.4

If you read my blog regularly, you will already know about the wonderful Drafts 55, and you will also know that it offers powerful scripting via JavaScript. Drafts is the perfect app for entering and processing data, and JavaScript is probably the language that I am most familiar with at the moment, so that’s where I decided to build it. A little while after I had begun, I saw a post from Greg Pierce, the developer of Drafts, asking for input on what services people most wanted integration with in the app. Drafts already supports a large number of services, basically on two possible levels. One is as Action Steps, which are little Workflow-style drag and drop blocks, with native UI switches and buttons and text fields. These have the advantage of being easy to use, and they are great for building simple actions, but they lack flexibility, as well as some of the basic things required for programming such as conditional statements, loops, and variables. The other way that apps and services can be integrated is via scripting, and here the list of integrations is much more extensive. Being part of a fully-featured JavaScript environment means that these integrations can be part of complex programmes.

There are basically two ways that Drafts can integrate with an app or service. One is via a URL scheme, where data is sent locally on the device from one app to another. The other is via a web API, where the Drafts app sends a message to a server on the web, which then responds in some way. This is the way that Airtable works. That server might also send that information back down to its own app on the same device, or on another device. Both of these abilities – to build URL schemes, and to send API requests – are both made possible within the Drafts JavaScript environment. What struck me about Greg’s post was how many different services people were asking for in the replies, and how many of them were perfectly possible but just waiting to be built. It occurred to me that instead of asking Greg to build them all himself, the number of integrations available might increase more quickly if users created them themselves.

User-built integrations are already perfectly possible to create, and in fact a few people have created them already. Special mention goes to Oliver Guerriat, who created two really useful – though not very well documented – ones which I don’t think enough people know about. One allows easy interaction with the Bear URL scheme, and the other adds easy interaction with many of Drafts own features6. I’d like to see more of these in the future, with documentation as good as that in Greg’s Scripting Reference. I wonder if Greg would even consider linking to some of these user-build integrations in the Drafts Scripting Reference as optional add-ons?

I wanted to try to make something like this myself, and my Airtable API is the result. Using the ideas about Object Oriented Programming that I’ve been learning about recently, I’ve created a set of classes and methods for interacting with a base in Airtable. This is very much beta software, so I am sure there are bugs, and I am sure that people will suggest additional features that would improve it. One thing I would like to add in the near future is file uploads and downloads.

Here’s a link to the script in the Action Directory, and here’s a link to the script in GitHub. To use this, include an “Include Action” action step before your own script. Here’s a template action you can use. You will also need to find out your base’s endpoint and API key from the Airtable API documentation.

Airtable API documentation

If you have any suggestions for changes or improvements, or if you find a bug, please let me know on Twitter or by Email. If you know your way around JavaScript and you want to make some changes, feel free to issue a pull request on GitHub. Below is the full documentation I’ve created. I’ve aimed to stick as closely to Greg’s style as possible.

I’m looking forward to trying this for a few other apps or services. I think next on the list might be Working Copy, an app that I absolutely love and which has an amazingly extensive URL scheme.

If you enjoyed this post, please check out my new Amazon recommendations page.


Airtable

Airtable is a web-based spreadsheet and database tool which can be used to organise a large variety of different kinds of data including text, images, files, and more. The scripting interfaces below are convenience wrappers that allow easy interaction with Airtable’s REST API.

While the Airtable API offers extensive read and write access to the data stored, it does not provide metadata about the structure of databases or the types of fields. Users will need to know this information in advance to properly interact with the database.

ATRecord

Represents a single record in an Airtable base.

Class Functions

  • create() -> ATRecord
    • Create a new record object.
  • selectRecords(Array of ATRecord objects, field, options) -> Array of ATRecord objects
    • Present a list of records to the user for them to select one or more
    • Parameters
      • Array of ATRecord objects: all records must have been added to a table and the table updated.
      • field [string or function] : A string denoting the name of the field which should be used to represent the records in the selection list. Alternatively, pass a function which takes each record and returns a string to display. This can be used to combine multiple fields together to create the labels for the selection list.
      • options [object]: a dictionary of options with the following available keys.
        • title [string] (optional): Title to display in the prompt.
        • message [string] (optional): Message to display in the prompt.
        • type [string] (optional): Valid values are “selectMultiple”, “selectOne”, and “selectButtons”.
        • filter [function] (optional): A function to filter the records displayed.

Properties

  • id [string, readonly]
    • The unique id of the record in the Airtable base. Undefined until the record is added to a table and the table is updated.
  • table [ATTable, readonly]
    • The table to which the record belongs.
  • createdTime [date, readonly]
    • The time that the record was created. Undefined until the record is added to a table and the table is updated.

Functions

  • getFieldValue(field) -> object
    • Takes a string with the name of the field, and returns the contents of that field.
  • setFieldValue(field, object)
    • Takes a string with the name of the field, and sets the contents of the field according to the object passed.
  • getLinkedRecords(field) -> Array of ATRecord objects
    • For a field which links to records in another table, this returns all of the linked records. The table containing the linked records must have been added to the base.
  • linkRecord(field, ATRecord)
    • For a field which links to records in another table, this adds a new linked record from the given field. Existing linked records are unaffected. Note that Airtable also supports linked fields which do not allow more than one linked record.
  • update() -> boolean
    • Pushes changes to the base for a record that has already been added to a table. Returns true if successful.

ATTable

Represents a table within an Airtable base.

Class Functions

  • create(name, ATBase) -> ATBase
    • Create a new table object with a given name and associated with a given base. Name must coincide exactly with an existing table on the web.

Properties

  • name [string, readonly]
  • base [ATBase]
  • records [Array of ATRecord objects]
    • All of the records associated with the table.
  • fields [Array of strings]
    • The names of the fields associated with records in the table.

Functions

  • addRecord(ATRecord)
    • Add a new record to the table. Will not be pushed to the web until update() is called.
  • selectRecords(field, options)
    • Equivalent to ATRecord.selectRecords(table.records, field, options).
  • update() -> boolean
    • Push changes to the base. Returns true if successful.

ATBase

Represents an individual Airtable base.

Class Functions

  • create(name) -> ATBase
    • Create new base object with given name.

Properties

  • name [string]
  • tables [Array of ATTable objects]
    • All of the tables associated with the base.

Functions

  • getRecordWithID(id) -> ATRecord
    • Takes the unique id of a record within an associated table and returns the record object.
  1. “gradebook” for you Americans out there 

  2. Spoiler: it’s layers of procrastination all the way down. 

  3. I’ve just started reading Code Complete by Steve McConnell, and I’ve already learnt a lot of good programming lessons. 

  4. Abstracting complexity is essentially what programming is. 

  5. See here for my previous posts about Drafts. 

  6. Mind-bendingly, it seems to make the creation of actions itself scriptable! 

A Note about Affiliate Links

2018-08-06✪Permalink

Last week came the sad news that Apple is ending their affiliate programme for apps. If you weren’t aware, this was a scheme that publishers could sign up to to generate special links to apps in the App Store. If users clicked on the links and subsequently purchased an app, some of that money would go to the publisher. Usually, Apple takes 30% and developers get 70%. In the case of affiliate links, users pay exactly the same, but the money was instead split 7%, 23%, 70% between publisher, Apple, and developer respectively. This 7% has now gone down to 0%.

I do think it’s a shame that Apple has taken this decision. It’s going to affect smaller blogs and more specialised sites more than bigger ones with advertising contracts and membership programmes. I also think it sends a message, intentional or otherwise, that Apple doesn’t value third-party editorial about the App Store, when I think a lot of its most devoted customers feel quite differently. I can’t count the number of apps I have downloaded and paid for because they were features on sites like MacStories.

I used affiliate links on PolyMaths when linking to apps, and while this only ever generated a trivial amount of money, I did like the idea that the site’s revenue had a theoretical probability of being greater than zero. I don’t really want to have ads on the site because I don’t like the idea of not having complete control over what appears there, so what I’ve decided to do instead is add a single, hopefully fairly unobtrusive Amazon recommendation in the masthead of the site. This links to a recommendations page, where you can find a number of products I’ve bought from Amazon and found useful, along with some brief reviews. If you click on any of the links and purchase the item, the site gets a percentage of that. I hope you find these useful too.

Fantastically Good Reminder Parser for Drafts 5

2018-07-11✪Permalink

Following my recent blog post about my Fantastically Good Event Parser, which to my great delight was featured on MacStories, I received quite a few requests to create something similar for Reminders. Fantastical, which inspired the event parser, also allows you to create reminders in the system Reminders app using natural language. The interface within the app that allows you to enter calendar events also allows you to create reminders by including words like reminder, task, or todo in the text entered.

I thought about replicating this functionality within my previous action, but this didn’t feel quite right in Drafts. Adding calendar events and setting a reminder are conceptually quite different kinds of action, so I decided to build a separate action just for reminders. Being separate, I also decided that requiring a keyword like reminder or todo didn’t make much sense for this use case, so I’m diverging slightly from the traditional Fantastical functionality here.

Otherwise, it works very similarly to Fantastical. Reminders can be entered in the form Thing Tuesday 5pm !!! /p to add a reminder called Thing with a due date of Tuesday, with a reminder at 5pm that day, with high priority, and stored in my Personal reminders list. Here are some details on how the parsing works:

  • The name of the reminder is whatever is left when the date, time, priority, and list name are removed. 1
  • The date and time can be entered in pretty much any normal format. Again, I’m using chrono.js behind the scenes here. Dates without times will default to having an alert at noon.2
  • Priority is optional and can be set as !, !!, or !!!.
  • The reminder list is set using a forward slash followed by one or more letters from the beginning of the name of the list. If a matching list is not found, or if no list is specified, the reminder will go into the system default reminders list.

I’m grateful to Greg Pierce, the developer of Drafts for recently adding some functionality to the app’s scripting engine in version 5.3 in response to my request. Without these changes, I wouldn’t have been able to make this action as fully functional as I wanted it to be. Being on the the beta channel in Slack, and now in the Drafts forum, I can honestly say that Greg is one of the most responsive developers I have come across. Even when he doesn’t plan to implement a feature request immediately (or at all), he explains his reasons clearly in a way that us non-developers can understand. Thanks again, Greg.

You can find my Fantastically Good Reminder Parser in the Action Directory.

  1. Note that I’ve decided not to automatically remove words like due or by, so if you include these they will be added to the name of the reminder. I’d suggest omitting them. 

  2. The default alarm time can be changed by tweaking the script. If there’s anyone who would like the parser to interpret dates given in the standard British format of dd/mm/yyyy, you can also change the US at the beginning of the third script step to GB

Fantastically Good Event Parser for Drafts 5

2018-06-27✪Permalink

I used to have an action in Drafts 5 for adding calendar events using Fantastical, the favoured calendar app of many because of its ability to interpret events entered in natural language. Fantastical allows you to enter things like Meeting on Thursday at 5pm and then generates a calendar event by parsing out the information you provided. Compared to tapping on dialog boxes and scrolling date pickers in other calendar apps, it feels fast and easy.

The way the action works is to take each line of the draft and run it through the Fantastical URL scheme. Sometimes I would run this action with a lot of different events1 which meant watching as my iPad madly bounced back and forth from Drafts to Fantastical. If I remembered, I would put the two apps in split view beforehand to speed things up, but even with the add=1 flag set in the URL scheme to avoid having to confirm each entry, Fantastical still shows an animation each time an event is entered, which slows things down.

The way apps like Fantastical actually integrate with the system calendar in iOS is via an API which allows direct manipulation of calendar events. You may have seen the Allow app to access the Calendar? prompt when first launching apps which use this. Drafts integrates this API into its scripting capabilities, and so it occurred to me recently that perhaps I could build a similar functionality within Drafts using JavaScript. This would allow me to use the system calendar app, which I prefer aesthetically over Fantastical, while retaining the ability to enter events in natural language.

What I’ve ended up creating has almost all of the same functionality as Fantastical, but since it does not rely on launching an external URL scheme, is considerably faster. You can enter multiple events, each on a different line, and have them all instantly added to your calendar without even launching another app. As with my Things Parser, I’ve leveraged the chrono.js library to do the natural language date parsing. My action supports the following things:

  • Dates and times entered in natural language2
  • Locations entered in the form at location or in location
  • Durations entered in the form 30 minutes or 2 hours3
  • Alerts entered in the form alert 30 minutes or alert 1 hour
  • The specific calendar where the event should be created in the form /family or simply /f4

Unfortunately, my script doesn’t yet support creating recurring events as Fantastical does. This currently isn’t built in to the scripting capabilities of the event object in Drafts, but I am hoping this might be added soon. When it is, I’ll be sure to update this action to support entering recurring events.

You can find my Fantastically Good Event Parser in the Action Directory.

  1. Each term, I used it to take a plain text version of my school timetable and add it to my calendar. 

  2. Events given with a date but without a time are assumed to be all-day events. Events with a given start time but no end time are assumed to be one hour long. 

  3. Note that the space is optional and that minutes and hours cannot be mixed. Minutes can be written as minutes, minute, mins, min, or m. Hours can be written as hours, hour, hrs, hr, h

  4. Your calendars will be searched for the one beginning with the string you’ve entered. If you have two calendars with the same first letter, such as tutoring and timetable, try being slightly more specific by writing /tu or /ti. If no calendar it specified, the event will be added to the default calendar.