Relative Date Picker

While working on a shortcut to allow me to reschedule a bunch of work related reminders to a specific day in the near future, I became frustrated with the default Shortcuts date picker.

I find the spinning wheels annoying and the lack of a day of the week indicator makes it difficult to select a date of say “next Thursday” without reference to a calendar.

This led me to create a Relative Date Picker shortcut that presents the interface shown below and allows near dates to be selected with a single tap.

I’ve shared the shortcut here in case others should find it useful.

Relative Date Picker

It is designed as a utility to be called from other shortcuts so I’ve also uploaded a couple of calling examples.

The first simply uses the Relative Date Picker with its default settings.

Call RDP 1

The second uses an optional input dictionary to configure the following…

  • Menu prompt
  • Base date for the picker
  • Number of days to show

Call RDP 2

A few highlights of the implementation details…

  • The menu is created as a list of contacts, as this seems to be the only way to get a nice looking menu natively in Shortcuts without calling out to another app (which I was trying to avoid as it often seems to cause issues when running in a widget).
  • The graphics were created in OmniGraffle, exported as PNG files with transparent backgrounds and Base64 encoded using another shortcut before being embedded in a dictionary.
  • The shortcut calls itself in order to implement the “Earlier” and "Later’ functions (as a result if you rename it you also need to change the “Shortcut Name” entry in the config dictionary near the top of the shortcut).
5 Likes

Very nice! I think I’ll recreate this in Scriptable as well, I share your frustrations with the system date picker

Just had the time to create in Scriptable, code is below. Not as pretty as yours :slight_smile:

/** e.g. Sun, Jan 26 */
const DDDMMMDD = date =>
  date.toLocaleDateString(undefined, {
    weekday: 'short',
    month: 'short',
    day: '2-digit',
  });
/** Set date to time 00:00 */
const stripTime = date => {
  const dateClone = new Date(date);
  dateClone.setHours(0, 0, 0, 0);
  return dateClone;
};
const addDays = (date, days) => {
  const dateClone = new Date(date);
  dateClone.setDate(date.getDate() + days);
  return dateClone;
};
const daysBetween = (d1, d2) => {
  const difference = stripTime(d1) - stripTime(d2);
  return Math.floor(difference / 86400000);
};
const isSameDay = (d1, d2) =>
  d1.getFullYear() === d2.getFullYear() &&
  d1.getMonth() === d2.getMonth() &&
  d1.getDate() === d2.getDate();
/** Get inclusive range of numbers */
const range = (start, end) =>
  [...new Array(end - start + 1).keys()].map(i => i + start);

/** */
const pickDate = async ({
  title = 'Select date',
  message,
  daysToInclude = 8,
  startDate,
} = {}) => {
  const NOW = new Date();
  const parsedStartDate = startDate || NOW;
  const datesIncluded = [
    parsedStartDate,
    ...range(1, daysToInclude - 1).map(daysToAdd =>
      addDays(parsedStartDate, daysToAdd)
    ),
  ].map(stripTime);

  const commonCallOpts = { title, message, daysToInclude };
  const allButtonsMetadata = [
    {
      label: '← Earlier',
      onSelect: async () =>
        await pickDate({
          ...commonCallOpts,
          startDate: addDays(parsedStartDate, -daysToInclude),
        }),
    },
    ...datesIncluded.map(date => {
      const isToday = isSameDay(NOW, date);
      const isTomorrow = isSameDay(addDays(NOW, 1), date);
      return {
        label:
          (isToday && 'Today') ||
          (isTomorrow && 'Tomorrow') ||
          [
            DDDMMMDD(date),
            ' (',
            date > NOW ? '+' : '',
            daysBetween(date, NOW),
            'd)',
          ].join(''),
        date,
      };
    }),
    {
      label: 'Later →',
      onSelect: async () =>
        await pickDate({
          ...commonCallOpts,
          startDate: addDays(parsedStartDate, daysToInclude),
        }),
    },
  ];

  const prompt = new Alert();
  prompt.title = title;
  if (message) prompt.message = message;
  allButtonsMetadata.forEach(({ label }) => prompt.addAction(label));
  prompt.addCancelAction('Cancel');

  const buttonIndex = await prompt.presentSheet();
  if (buttonIndex === -1) return null;
  const { date, onSelect } = allButtonsMetadata[buttonIndex];
  if (onSelect) return await onSelect();
  else return date;
};
1 Like

That’s nice.

Certainly quicker to go forward and back between weeks than the Shortcuts version, although as you say not quite as visually attractive.

Definitely useful to have both in the tool bag.

1 Like

This is a great. I have been able to use it to create a very useful shortcut to allow me to display upcoming matches i have booked. I have one slight issue with it, if the menu display is too large for the screen it corrupts the output. Ie if i use 7 days in the Num Days it works fine but if I change that to 21 (to save me pressing Later a couple of times) it displays correctly until i scroll the menu, then the events i have entered under ORG: in the vCard are displayed in random dates. Is this something you have seen before. Any help much appreciated.
Peter

I have found the solution to my problem. It involves putting in a blank on ORG: if there is no value. This stops the random text being put into this field. To test this if you take your example and increase the number of days to 28, the Earlier has no entry under it but if you scroll down to the Later and then back up again a number of days appears under it. By forcing a blank in the ORG: fixes this issue. Many thanks for a great shortcut, been very helpful.

Thanks for your kind words.

As far as I can tell the behaviour you were seeing is a feature/bug in the “Choose from list” action when dealing with contact items.

I’m guessing this was introduced in iOS 15 as I don’t recall seeing this when I originally wrote the shortcut.

Pleased you found a viable workaround, although I guess it does degrade the appearance a little.

Hopefully the bug will get fixed at some point.