What is a JSON feed? Learn more

JSON Feed Viewer

Browse through the showcased feeds, or enter a feed URL below.

Now supporting RSS and Atom feeds thanks to Andrew Chilton's feed2json.org service

CURRENT FEED

And now it’s all this

I just said what I said and it was wrong. Or was taken wrong.

JSON


An unzipping shortcut

Permalink - Posted on 2018-11-11 19:30

I’ve been planning to write a post about the new Apple products for over a week, but I keep getting distracted. Today, I went to Apple’s PR pages for the MacBook Air, the Mac mini, and the iPad Pro to download images and went off on another tangent. As usual, I will inflict that tangent on you.

Apple provides the product images as zipped archives, so when I clicked on the link in the press release, I was confronted with this “what do I do?” screen in Safari.

Zip file in Safari

The efficient thing would have been to walk ten feet over to my iMac and download the zip files there, where they can be expanded with almost no thought. But I took the procrastinator’s way out, deciding to solve the problem of dealing with zip files on iOS once and for all.

In the past, I’ve tried out a few zipping/unzipping apps, and they’ve all sucked, with user interfaces that are clumsy to navigate and look like something out of Windows 3.1. What I wanted was a clean, one-click solution similar to what we have on a Mac. A shortcut, if you will…

I went to the Shortcuts Gallery and searched on “zip,” “unzip,” and “archive.” There was a shortcut for zipping up a bunch of files and putting them into an email message, but nothing for unzipping and saving. I also couldn’t find anything by Googling. So I made my own.

Unzip to iCloud shortcut

As you can see, there’s not much to it. You can make it yourself or download it.

You run it as an Action Extension by bringing up the Share Sheet and selecting the Shortcuts Action. After you choose it from your list of Extension shortcuts, you’ll be presented with a Files-like location picker, from which you can choose where the expanded archive will be saved.

Choose where to save the expanded archive

Even though “iCloud Drive” is the Service chosen in Step 2 of the shortcut, it can also save locally and into Dropbox. I suspect it can use services like Box and Google Drive, too, but I don’t use those services and haven’t tested them.

There are a few thing to keep in mind:

  • The Files-like interface isn’t Files-like enough to give you the opportunity to create a new folder. You must save the expansion in an existing folder.
  • It crashed the first time I tried to run it. I don’t know if that was because of low memory or some other odd reason, but it led me down a path of unnecessary debugging. When I couldn’t figure out what was wrong with it, I ran the shortcut again and it worked perfectly. 🤷🏼‍♂️
  • The first couple of times I ran it, the Dropbox icon was dimmed and inaccessible. Then it became available. I’m sure I did something in between, but I can’t tell you what it was. 🤷🏼‍♂️🤷🏼‍♂️
  • You can change the Service in Step 2 to Dropbox if you know you’ll never use iCloud Drive. I prefer to keep it as iCloud Drive, because that gives me the option to use either service.
  • The Extract archive step can handle more that just zip files. According to the help info, it can also extract rar, tar.gz, tar.bz2, tar, gzip, cpio, cab, and iso archives. The only one missing from the list that I sometimes see is 7-zip.

I make no claim of originality with this. I’m sure plenty of people have written similar shortcuts, but I couldn’t find them. I didn’t even know there was an Extract Archive step in Shortcuts until today. I should spend more time on this page and this one.

Update Nov 12, 2018 6:53 PM
A few more things:

  • As with the Mac’s Archive Utility and the default behavior of the unzip command, the Extract Files action expands the archive exactly the way it was compressed. Whatever directory structure was zipped up will reappear within the folder you select in Step 2 of the shortcut. This could be a problem, as we’ll discuss below.
  • As you might expect, Federico Viticci has already made a shortcut to do this, which you can download and install. Federico’s is more complicated than mine. He uses the name of the archive (without the .zip extension) to create a new folder within the iCloud Drive Shortcuts folder and extracts the contents of the archive there.

    I’m of two minds on Federico’s shortcut. On the one hand, by creating a folder, it keeps things neat. With my shortcut, when you expand an archive that has lots of top-level files, those files can make a mess of the folder you save them in. On the other hand, Federico’s shortcut puts the expanded archive in a place I’ll never want it to go. It would be easy to edit Federico’s shortcut to put the extracted files somewhere other than the Shortcuts folder, but no matter what, it will go into a fixed location.

    As best I can tell, there’s no way to get the best of both worlds: a neat folder of the extracted files and a choice of where that folder will be.

  • Federico is putting together a massive list of shortcuts he’s built. The list is organized by category and includes both a download link for each shortcut and a brief description of what it does. The list is being continually updated, so it’ll be worth returning to after your first perusal.

[If the formatting looks odd in your feed reader, visit the original article]


Keeping up with IP number changes

Permalink - Posted on 2018-11-09 14:52

Last weekend I was at a hotel working on my iPad and launched Prompt to connect to my iMac at home and run a command. It wouldn’t connect. I launched Transmit (which still works well, despite its zombie status) and couldn’t connect there, either. Obviously, my service provider had changed my home IP number. This happens rarely,1 but I didn’t want to be caught like this again.

My first thought was to write a script that would check the IP number periodically and save it in a file on the leancrew.com server. But a bit of searching led me to this script by Charlie Hawker, which checks for the IP number and sends an email if it’s changed, a much better idea.2

There were, however, two things about the script I didn’t like:

  1. It’s written in PHP. I don’t disklike PHP—I use it to build this blog—but it’s not a language I’m deeply familiar with, and I’d prefer a Python script so I can easily make changes if necessary.
  2. It gets the IP number by loading a website (ip6.me) and scraping it. I wanted something a little more direct.

The solution to the second problem was to use dig, the domain information groper (a program that was undoubtedly named by a man). Here’s a command that does it by returning information from OpenDNS3

dig +short myip.opendns.com @resolver1.opendns.com

With that command in mind, the script, checkip, was easy to write:

python:
 1:  #!/usr/bin/python
 2:  
 3:  import smtplib
 4:  import subprocess as sb
 5:  import os
 6:  import sys
 7:  
 8:  # Parameters
 9:  ipFile = '{}/.ipnumber'.format(os.environ['HOME'])
10:  mailFrom = 'user@gmail.com'
11:  mailTo = 'user@gmail.com'
12:  gmailUser = 'user'
13:  gmailPassword = 'kltpzyxm'
14:  cmd = '/usr/bin/dig +short myip.opendns.com @resolver1.opendns.com'
15:  msgTemplate = '''From: {}
16:  To: {}
17:  Subject: Home IP number has changed
18:  Content-Type: text/plain
19:  
20:  {}
21:  '''
22:  
23:  # Get the current IP number and the filed IP number
24:  dig = sb.check_output(cmd.split(), universal_newlines=True)
25:  currentIP = dig.strip()
26:  with open(ipFile, 'r') as ip:
27:    oldIP = ip.read().strip()
28:  
29:  # Update the file and send an email if they differ
30:  if currentIP != oldIP:
31:    with open(ipFile, 'w') as ip:
32:      ip.write(currentIP)
33:    
34:    msg = msgTemplate.format(mailFrom, mailTo, currentIP)
35:    smtp = smtplib.SMTP_SSL('smtp.gmail.com', 465)
36:    smtp.ehlo()
37:    smtp.login(gmailUser, gmailPassword)
38:    smtp.sendmail(mailFrom, mailTo, msg)

Lines 9–21 set the parameters that are used later in the script. As you can see from Line 9, I save the IP number in the hidden file .ipnumber in my home directory. Line 14 is the dig command we just talked about. Lines 15–21 is the template for the email that the script sends when the IP number change, and Lines 10–13 are the personal settings needed to send the email.

Lines 24–25 run the dig command4 and store its result (after stripping the trailing newline) in the currentIP variable. Lines 26–27 read the .ipnumber file, which contains the IP number from the last time the script was run, and store the contents in oldIP.

Line 30 then compares the two numbers and continues the script only if they differ. Lines 31–32 puts the new IP number into the .ipnumber file, and Lines 34–38 build an email message and send it via GMail. This script works only if you have a GMail account—if you don’t, you’ll have to tweak these lines to get your email service to send the message.

The email it sends is nothing fancy: a subject line telling me the IP number has changed and a body with the new number (which I’ve obscured in this screenshot).

IP number change email

To avoid an error the first time this script is run, create the .ipnumber file and put any old IP number in it. I seeded mine with an incorrect value, 11.11.11.11, so I could make sure the file changed and the email was sent when the script ran.

I decided to have the script run every hour. If I were using Linux, I’d set this up through cron, but the (more complicated) Mac way is with launchd. Here’s the plist file that controls the running of the script. It’s called com.leancrew.checkip.plist and is saved in my ~/Library/LaunchAgents directory:

xml:
 1:  <?xml version="1.0" encoding="UTF-8"?>
 2:  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 3:  <plist version="1.0">
 4:  <dict>
 5:    <key>Label</key>
 6:    <string>com.leancrew.checkip</string>
 7:    <key>ProgramArguments</key>
 8:    <array>
 9:      <string>/Users/drang/Dropbox/bin/checkip</string>
10:    </array>
11:    <key>StartCalendarInterval</key>
12:    <array>
13:      <dict>
14:        <key>Minute</key>
15:        <integer>12</integer>
16:      </dict>
17:    </array>
18:  </dict>
19:  </plist>

The key entries here are Line 9, which gives the full path to the checkip script, and Lines 11–17, which tell launchd to run the script at 12 minutes past the hour.5 Because there are no Hour, Day, or Month entries in this section, it runs every hour of every day of every month.

Because of where it’s stored, this Launch Agent gets loaded into the launchd system every time I log in. To get it to load without logging out and logging back it, I ran these two commands:6

launchctl load ~/Library/LaunchAgents/com.leancrew.checkip.plist
launchctl start com.leancrew.checkip

With it in the system, the agent will continue to run as long as I’m logged in. For me, this is essentially forever, as I keep this computer running all the time specifically so I can log into it whenever I need to.

I’ve thought of improving the script by adding an error-handling section to alert me when the dig command fails. This would let me know, for example, if OpenDNS has stopped running its service. But the possibility of being inundated by false positive emails has kept me from trying that out.


  1. I managed to get the command run by connecting to my work computer, where the IP number is fixed. 

  2. Yes, I know there are services that will do this sort of thing. I don’t want to rely on them. 

  3. This does rely on OpenDNS, but if it ever shuts down or curtails this service, there will always be another similar way to get my IP number. I’ll just change my script to use that instead. 

  4. Note: I typically write scripts in Python 3 nowadays, where the subprocess module is considerably different. This script was easier to do in the Python 2 that comes with macOS because it gets run by the launchd system, which uses an environment that doesn’t know where my installation of Python 3 is. Rather than telling it, I decided to just write it for Python 2. You could call this the $PATH of least resistance. 

  5. Why 12 minutes? No particular reason. Any number under 60 would work just as well. 

  6. The load and start subcommands are now considered legacy features of launchctl and will probably be removed in the future. But for now they still work, so I’ll worry about learning the new subcommands later. 


[If the formatting looks odd in your feed reader, visit the original article]


Building a tweet in Shortcuts

Permalink - Posted on 2018-11-03 15:14

I occasionally tweet links to Kindle books that are on sale. Although I do include my Amazon Associate tag in the link so I get a commission on sales, this is not what you would call a significant money maker. The commission on Kindle books is 4%, which amounts to 8¢ on the typical $2 sale price. I do it because I think the people who follow me might be interested in the book.1

In the past, these tweets were purely functional: such-and-such book is on sale for $2—here’s a link to it. I’d use the Associate app—actually its Share Sheet extension—to get the link. A couple of days ago, I decided it would be fun to jazz up the tweet with an image of the book. I already had some Python code that could use Amazon’s product advertising API to get the URL of a book image; I figured it wouldn’t be too hard to jigger with it to pull out the author and title, too, and combine it with a Shortcuts to build a tweet.

Here’s how it works. While I’m on the web page for the book, I invoke Shortcuts from the Share Sheet and choose the Tweet Kindle Book shortcut. This launches Pythonista to extract the relevant information from the Amazon API. At this point, the shortcut gets stuck with my screen showing Pythonista. To get it unstuck, I tap the ◀︎Safari link in the top left corner.

Shortcut paused in Pythonista step

This little kick to get the shortcut going again is needed because Pythonista is not a particularly good iOS automation citizen. It has a URL scheme, which is what allows Shortcuts to invoke it, but it doesn’t implement callbacks, so it won’t automatically return to the app that called it. Pythonista does have ways to jump to other apps, but I haven’t figured out a way to get it to automatically continue this shortcut.

Anyway, after unsticking the shortcut, it assembles the book information into a tweet that I can edit and send.

Ready to edit and tweet

Here are the steps of the shortcut.

Tweet Kindle Book steps

The Python script (which we’ll get to in minute) takes the URL of the page being shared as input and puts a JSON dictionary of the book information on the clipboard. For this example, the JSON is

{"title": "Provenance",
 "author": "Ann Leckie",
 "imgURL": "https://images-na.ssl-images-amazon.com/images/I/51EiO-mWOtL.jpg"
 "link": "https://www.amazon.com/dp/B06XW6YTKV/?tag=andnowitsa085-20"}

The Get Dictionary from Input step parses the JSON and turns it into a Shortcuts dictionary, from which we can extract the values. The first thing we do is get the URL of the book cover image and use Get Contents of URL to get the image itself. The image is saved in the variable Tweet.

Next, we assemble the text of the tweet from the other parts of the dictionary and a few linking words. This text is added to the Tweet variable (yes, you can add text to a variable that contains an image—I was surprised at that, too) and the whole thing is finally sent off to the Tweet step. You’ll need to have the official Twitter app installed for the Tweet step to be available.

The Python script, amazon-tweet-info.py, is this:

python:
 1:  import requests
 2:  from datetime import datetime
 3:  import base64, hashlib, hmac
 4:  import urllib.parse
 5:  from bs4 import BeautifulSoup
 6:  import clipboard
 7:  import sys
 8:  import re
 9:  import json
10:  
11:  # Get the Amazon URL from the first argument and extract the ASIN
12:  amzURL = sys.argv[1]
13:  itemASIN = re.search(r'(dp/|gp/product/)([^/?%]+)', amzURL).group(2)
14:  
15:  # Date and time
16:  t = datetime.utcnow()
17:  timeStamp = urllib.parse.quote(t.strftime('%Y-%m-%dT%H:%M:%SZ'))
18:  
19:  # Parameters
20:  associateTag = 'yourtag'
21:  accessKey = 'youraccesskey'
22:  secretKey = b'yourlongsecretkey'
23:  parameters = ['Service=AWSECommerceService',
24:     'Operation=ItemLookup',
25:     'ResponseGroup=Large',
26:     'ItemId={}'.format(itemASIN),
27:     'AWSAccessKeyId={}'.format(accessKey),
28:     'AssociateTag={}'.format(associateTag),
29:     'Timestamp={}'.format(timeStamp)]
30:  parameters.sort()
31:  paramString = '&'.join(parameters)
32:  
33:  # Generate signature from parameters and secret key
34:  unsignedString = '''GET
35:  webservices.amazon.com
36:  /onca/xml
37:  {}'''.format(paramString)
38:  signedString = hmac.new(secretKey, msg=unsignedString.encode(), digestmod=hashlib.sha256).digest()
39:  sig = urllib.parse.quote(base64.b64encode(signedString))
40:  
41:  # Generate URL from parameters and signature
42:  url = 'http://webservices.amazon.com/onca/xml?{}&Signature={}'.format(paramString, sig)
43:  
44:  # Get image information
45:  resp = requests.get(url)
46:  xml = resp.text
47:  
48:  # Extract the information from from the XML
49:  # response and build a dictionary
50:  soup = BeautifulSoup(xml, 'html5lib')
51:  info = {}
52:  info['imgURL'] = soup.item.largeimage.url.text
53:  info['author'] = soup.item.itemattributes.author.text
54:  info['title'] = soup.item.itemattributes.title.text
55:  info['link'] = 'https://www.amazon.com/dp/{}/?tag={}'.format(itemASIN, associateTag)
56:  
57:  clipboard.set(json.dumps(info))

The first item given to it on the “command line” (which in Shortcuts is what flows into the Run Script step) is expected to be the URL of the web page for the book. Lines 12–13 slurp it in and extract its unique Amazon ID, the ASIN.

Lines 16–17 get the current date and time and format it for later use with the API. Lines 20–31 put together all the data needed to make an API request. The associateTag is what you get from Amazon when you sign up for the Associates program. It’s attached to the link URLs that earn you a commission. The accessKey and secretKey are ID strings you get when you sign up for the Product Advertising API. The secret key is defined as a byte string rather than a normal Python 3 string because the encoding that’s part of the signing process (see below) requires a byte string.

Your request to the API has to be “signed” by encoding the request with your secret key. That’s done in Lines 34–39. The details of this are a little hairy, but Amazon explains them pretty well. Line 42 puts everything together into a URL to send to the API.

An important part of the request is the ResponseGroup parameter. There are many response groups available for plucking out different bits of information on a product. As best I could tell from the documentation, the only one that provides all the pieces I want is the Large group. That’s why you see it on Line 25.

Lines 45–46 use the Requests module to send the request URL to the API and get the response, which is in XML format. Lines 50–54 use the Beautiful Soup module to parse the XML and build a Python dictionary with the author, title, and image URL. Line 55 uses the ASIN and the associate tag to create the link entry in the dictionary.

Finally, Line 57 uses the JSON module to convert the Python info dictionary into JSON format and put it on the clipboard. This is the result of the first step of the Tweet Kindle Book shortcut.

This was a relatively simple shortcut to put together, mainly because I already had a Python script written to get the image URL. I just had to figure out which response group also included the author and title data and where they fit within the XML hierarchy.

I’m still a little annoyed about the pause at the Pythonista step. Maybe there’s a trick I don’t know about to get it to return to the shortcut. Or maybe I should consider rewriting that step in JavaScript so I can use Scriptable, which I think is more suited for use as a Shortcuts step.


  1. If I’m buying the linked book myself because I don’t already own it, it takes 25 book sales for me to break even. Given how cheap frugal discerning my Twitter followers are, that many sales is rare. 


[If the formatting looks odd in your feed reader, visit the original article]


Photo location followup

Permalink - Posted on 2018-11-02 14:34

There’s always a better way to do it. Shortly after I posted my Photo Coordinates shortcut, professional automator Rosemary Orchard tweeted me a link to a cleaner shortcut that uses the Get Details of Images step.

Rosemary's improvement

As you can see, after extracting the Location property of Get Details of Images, you can further extract the Latitude and Longitude properties. This got me thinking that I could reduce the shortcut to a single step by treating the Shortcut Input magic variable as a Location, not as Photo Media, and then extracting the latitude and longitude from that.

One-step Photo Coordinates shortcut

This is a great idea, except it doesn’t work. Presumably, an image can’t be treated like a location because a location is a property of an image—and you can’t jump two levels down the hierarchy in one step.

But it does show I could have written my original shortcut more quickly by taking the Location property of the image instead of the Metadata Dictionary, and then extracting the Latitude and Longitude.

Three-step Photo Coordinates shortcut

This is no shorter than my original, but if I’d done it this way I wouldn’t have had to screw around with the nested JSON in the Metadata Dictionary. Rosemary’s is still shorter, of course, and easier to read because it doesn’t create an unnecessary variable.

On the other hand, looking into the Metadata Dictionary taught me some things about the GPS metadata in phone images that I’ve never seen in the metadata on standard cameras and that I wouldn’t have learned if I’d done it the easy way right from the start. At some point, I may make use of that. In particular, the direction the camera was pointing when the photo was taken seems like it would be nice to know in some situations.


[If the formatting looks odd in your feed reader, visit the original article]


Drawing the quarters

Permalink - Posted on 2018-11-02 02:03

As you probably heard on Twitter, or from the usual sources, Apple announced that today’s quarterly figures will be the last to include unit sales for the Mac, iPhone, and iPad. I’ve been posting plots of units sales since April of 2015, when I shook up the staid world of Apple punditry by introducing it to the four-quarter moving average. It’s a sad day here at Drang Global Headquarters as we post the last of its kind:

Apple sales

The dots are the raw sales figures, the thick solid lines are the four-quarter moving averages, and the thin dashed lines connect the same quarters in consecutive years, giving a sense of the year-over-year changes. You can see why Apple isn’t particularly interested in showing these figures anymore—you have to get out a magnifying glass to see any recent growth.

Oddly enough, back in August, after the last set of quarterly results came out, I decided it was time to start plotting revenue. Not because I had any inkling that Apple would stop providing unit sales, but because I wanted to compare these three products to Services and Other Products, the two remaining categories in Apple’s quarterly reports. So I dug back through the reports and added revenue to my data files for the Mac, iPhone, and iPad. This data update and a few small edits to my plotting script allowed me to quickly produce a new version of the chart above, but with this one focused on revenue:

Apple revenue

You can see why Apple prefers to talk revenue.

The dominance of the iPhone makes it hard to see the Mac and the iPad down there in the mud, so let’s show them all by themselves:

Mac and iPad revenue

(Stephen Hackett, feel free to use this the next time your co-hosts talk about the fading Mac platform. Do it quickly, though, as I suspect the iPad will rebound next quarter.)

Of course, in true best-laid-plans style, I didn’t get around to making data files for Services and Other Products, so I can’t make the plots I intended to. Next time.


[If the formatting looks odd in your feed reader, visit the original article]


Location, location, location

Permalink - Posted on 2018-10-31 04:25

After my two posts on extracting photo metadata using Shortcuts (the naive one and the improvement), Vinay Kashyap asked me on Twitter if I knew of a way to get the latitude and longitude of a photo. I didn’t, but with a little JSON exploration, I figured it out. And now I’ll inflict it on you.

First, you can’t use the Location property of the photo.1 Here’s a Share Sheet shortcut that tries that, where the input is restricted to Images and the contents of the Show Result step is the Location property of the Shortcut Input magic variable.

Photo Address shortcut

The Location property typically returns a street address, although in some of my tests, on photos taken way out in national or state parks, it returned just a city and state. But whatever it returns, it isn’t the latitude and longitude. For that we need to use the Metadata Dictionary property, which you’ll find if you scroll about two-thirds of the way down the property list.

Choosing the Metadata Dictionary item of photo

When you choose the Metadata Dictionary, you’ll get an opportunity to choose which item of this dictionary to return. But in contrast to selecting a photo property, you don’t get a list to select from—you have to know the name (or key) of the dictionary item to return. The key we want is {GPS} (yes, with the curly braces).

Choosing {GPS} item of Metadata Dictionary

How do you know which key name to use? You don’t until you understand the dictionary structure. If we leave the key field blank, this step will return the entire dictionary in JSON format. Here’s an example, where I’ve used indentation to make the dictionary structure easier to see:

{
  "{TIFF}":{
    "ResolutionUnit":2,
    "Software":"12.0.1",
    "DateTime":"2018:10:24 09:27:26",
    "XResolution":72,
    "Model":"iPhone XS",
    "YResolution":72,
    "Make":"Apple"
  },
  "{Exif}":{
    "DateTimeOriginal":"2018:10:24 09:27:26",
    "ComponentsConfiguration":[1,2,3,0],
    "ExposureBiasValue":0,
    "BrightnessValue":3.152467043314501,
    "SubsecTimeOriginal":"529",
    "MeteringMode":5,
    "FNumber":2.3999999999999999,
    "FocalLength":6,
    "ShutterSpeedValue":5.9093741654055014,
    "SubjectArea":[785,1749,514,545],
    "ApertureValue":2.5260688112781806,
    "SceneType":1,
    "SceneCaptureType":0,
    "ColorSpace":1,
    "LensSpecification":[4.25,6,1.7999999523162842,2.4000000953674316],
    "PixelYDimension":2048,
    "WhiteBalance":0,
    "FlashPixVersion":[1,0],
    "DateTimeDigitized":"2018:10:24 09:27:26",
    "ISOSpeedRatings":[200],
    "ExposureMode":0,
    "ExifVersion":[2,2,1],
    "PixelXDimension":1536,
    "LensModel":"iPhone XS back dual camera 6mm f\/2.4",
    "FocalLenIn35mmFilm":52,
    "SensingMethod":2,
    "ExposureProgram":2,
    "ExposureTime":0.016666666666666666,
    "SubsecTimeDigitized":"529",
    "Flash":16,
    "LensMake":"Apple"
  },
  "{GPS}":{
    "ImgDirection":293.0444947209653,
    "LatitudeRef":"N",
    "HPositioningError":65,
    "DestBearingRef":"M",
    "Latitude":41.770949999999999,
    "Speed":0,
    "TimeStamp":"14:27:20",
    "LongitudeRef":"W",
    "AltitudeRef":0,
    "Longitude":88.152322166666664,
    "Altitude":214.22999542752629,
    "DateStamp":"2018:10:24",
    "DestBearing":293.0444947209653,
    "ImgDirectionRef":"M",
    "SpeedRef":"K"
  },
  "PixelHeight":2048,
  "Depth":8,
  "PixelWidth":1536,
  "{JFIF}":{
    "DensityUnit":0,
    "YDensity":72,
    "XDensity":72,
    "JFIFVersion":[1,0,1]
  },
  "ProfileName":"sRGB IEC61966-2.1",
  "DPIWidth":72,
  "{ExifAux}":{
    "Regions":{
      "WidthAppliedTo":4224,
      "HeightAppliedTo":3168,
      "RegionList":[{
        "Height":0.18228571428571427,
        "Timestamp":2147483647,
        "AngleInfoRoll":270,
        "FaceID":2,
        "AngleInfoYaw":0,
        "Type":"Face",
        "X":0.19619047619047617,
        "Width":0.12990476190476191,
        "ConfidenceLevel":889,
        "Y":0.57961904761904748
      }]
    }
  },
  "DPIHeight":72,
  "ColorModel":"RGB",
  "{MakerApple}":{
    "25":0,
    "40":1,
    "33":0,
    "26":"q900n",
    "12":[22.50390625,33.046875],
    "1":10,
    "35":[416,268435841],
    "20":10,
    "2":{
      "__type__":"data",
      "__value__":"PwG6AUgCmQIZAq8BAAEaABUALQCBAEEAHgAfADQAEgHPAKcAjQBxAFYAQADfAPgADwATAGgALwAcAB0AWwAgAY8AmQClAKkApgCdAJAAXwGVABUAKQAhABsAGwCIAC0BeAB4AIMAmACTAG0AYgBkAFkAHwAcABgAGAAXAEwA2gB1AHoAhQD5AIwAkwBqAGsAWABEADgAJgAYABYARwBrAHgAdwCBAMMA1ABhAH8AXABCAEUAQwA9ADIALgBKAGsAfACvANwAlABXADUAPgAvADEAXgBmAKEASABcAEkAZQD0AtMAZQBlAEMAPAAqADkASABwAGsAoQCGAJwAWABVAEgDOgDUAN4AiwDMAG8AQwBXAJUAgQAcAbUAmABZAKsApwIyAPEA4QC\/AO4AzgBSAGMAaAB2ACkBygBzAHMABwH8AkAA7gDjAI0AowBQAEsAjgCkAHsAtADSAFwATwCSANgDzwHAAKEBpQAzAEwATgChAMAAsgCmAIwAaQBVAKkAZAMtAyIDRwNnAF0B1QKPASABIADzADwBAgFrAPkAzgJmArIBpgGQAbsA4QFFAsYBDQEuAPEASQOtAmEB1AGbA2kCTwEzAVwBGgHcAW8AWAA\/AC0AkQBbAQMDBwOrArIDUwI1AV0BTwEyAYgBrwBcAEkARACDAOoA7AIDA+QCIQM="
    },
    "13":31,
    "3":{
      "flags":1,
      "value":665823207958125,
      "timescale":1000000000,
      "epoch":0
    },
    "14":4,
    "4":1,
    "37":140,
    "22":"AVxYyp2uOFH5c4umzrvZQ1fqbx7z",
    "5":189,
    "6":183,
    "38":3,
    "23":4202496,
    "16":1,
    "7":1,
    "39":27.983028411865234,
    "31":0,
    "8":[-0.025775959715247154,-0.95222359895706177,-0.28761821985244751]
  },
  "{IPTC}":{
    "DigitalCreationTime":"092726",
    "DigitalCreationDate":"20181024",
    "DateCreated":"20181024",
    "TimeCreated":"092726"
  }
}

As you can see, this is a complex nested data structure. The latitude and longitude are in a subdictionary—a dictionary within a dictionary—whose key is {GPS}.2 The {GPS} dictionary also includes some cool stuff like the altitude and the direction the camera was pointing, but we’ll leave them aside for now.

OK, so now that we know which item of the Metadata Dictionary to select, how do we pull the latitude and longitude out of it? The best way I found was to save the Metadata Dictionary as a variable called GPS and then select the values of that dictionary using the key names I found in the JSON.

GPS dictionary

Putting it all together, we get this simple three-step shortcut

Photo Coordinates shortcut

that returns the latitude and longitude of the photo’s location.

Photo Coordinates result

The simplicity of the shortcut stands in contrast to the complexity of the Metadata Dictionary and glosses over the JSON spelunking needed to figure out where the data was hiding. But now that we have one example, we can use it to pull out all kinds of metadata. I’m especially curious about the {MakerApple} subdictionary, with its mysterious numerical key names.


  1. If it were that easy, Vinay wouldn’t have asked. 

  2. I assume the braces are used in the key name as a mnemonic to indicate that the value is a dictionary. 


[If the formatting looks odd in your feed reader, visit the original article]


Replacing regexes in Drafts

Permalink - Posted on 2018-10-29 01:24

If you’re a longtime regular expression user, you may be surprised at how the Find and Replace feature in Drafts works. The Find part is normal, but the Replace is a little different.

Let’s assume we have a set of book titles with subtitles set off by colons1:

The Films of Fritz Lang: Allegories of Vision and Modernity
In Pharaoh's Army: Memories of the Lost War
Understanding Comics: The Invisible Art
A Matter of Interpretation: Federal Courts and the Law
The Digital Dialectic: New Essays on New Media
I Saw Esau: The Schoolchild's Pocket Book

We’d like to reformat these titles by putting each subtitle on an indented line under the main title and then separating them all with blank lines. Like this:

The Films of Fritz Lang
  Allegories of Vision and Modernity

In Pharaoh's Army
  Memories of the Lost War

Understanding Comics
  The Invisible Art

A Matter of Interpretation
  Federal Courts and the Law

The Digital Dialectic
  New Essays on New Media

I Saw Esau
  The Schoolchild's Pocket Book

For compatibility with the formatting here on the blog, I’ve indented the subtitles above with two spaces, but let’s say we really want them indented with a tab.

If we were working in Perl, and had these titles in a text file, we could do the reformatting with this one-liner:

perl -pe 's/^(.+): (.+)$/$1\n\t$2\n/' titles.txt

where the find regex is

^(.+): (.+)$

and the replace regex is

$1\n\t$2\n

Most regular expression engines nowadays take their cues from Perl (Perl 5, anyway), so although Python isn’t really set up for one-liners, the find and replace regexes we’d use in a Python script are the same.

BBEdit’s “Grep” find and replace is basically the same,

BBEdit find and replace

The only difference is the use of backslashes instead of dollar signs to identify the parts captured by parentheses.

So if we wanted to do this reformatting in Drafts, it’s only natural to try this:

Perlish replace

As you can see from the lower half of the screenshot, the finding part worked as expected, but if we tap the Replace All button, we get this:

The Films of Fritz LangntAllegories of Vision and Modernityn
In Pharaoh's ArmyntMemories of the Lost Warn
Understanding ComicsntThe Invisible Artn
A Matter of InterpretationntFederal Courts and the Lawn
The Digital DialecticntNew Essays on New Median
I Saw EsauntThe Schoolchild's Pocket Bookn

What’s happened is the \n and \t in the replacement regex have not been interpreted as newline and tab. Instead, the backslashes have been interpreted as “treat the next character as literal, even if it would normally have a special meaning in a regex,” which is why we see an nt between the main titles and subtitles and an n at the end of each line.

So how do we get what we want? Don’t overthink it. If you want a newline in the replacement, type Return; if you want a tab, type Tab. Like this:

Replace with newline and tab

It’s hard to see because the Replace field isn’t tall enough to show three lines at once, but there’s a Return after the $2.

This is actually easier, but because of years of using \n and \t, I almost always type it the Perlish way first.

By the way, if you’re wondering whether you should use “real” tabs and newlines or the backslashed metacharacters in the Find field, the answer is Yes. You can use either one.


  1. I plucked these from this GoodReads page


[If the formatting looks odd in your feed reader, visit the original article]


Markdown footnotes in Drafts

Permalink - Posted on 2018-10-28 03:02

Footnotes in Markdown are pretty easy to just type in directly, and that’s what I did for quite a while when writing posts in Drafts on my iPad. A couple of weeks ago, I decided I wanted to add a bit of automation to avoid retyping the footnote marker. Seemed like an easy task, but it took me three tries to get what I wanted.

First, let’s go over what I mean by “footnotes in Markdown.” As defined by John Gruber, Markdown doesn’t support footnotes, but many popular implementations, like MultiMarkdown and PHP Markdown Extra (which I use to build ANIAT pages) do. I’ve even heard tell that Gruber himself uses an off-brand extension to plain ol’ Markdown to handle footnotes on Daring Fireball.

All of these implementations use a slight variation on Markdown’s reference link syntax. Here’s an example:

The low angle feels better when I'm working while sitting
cross-legged on a bed or floor.[^chicago]

[^chicago]: Twenty-five or -six to four.

The idea is to use an reference string that starts with a caret inside square brackets where you want the footnote marker to appear. Then elsewhere in the text (and it could be anywhere, but I like putting it right after the paragraph with the marker) comes that same marker string followed by a colon and the footnote text. The goal of my automation was to type the marker only once and to format the parts on separate lines.

My first thought was to use a TextExpander snippet with a fill-in. Drafts supports TextExpander and so do many other text editors, so this would be a portable solution. Here’s the snippet:

Footnote TextExpander snippet

The idea here is to work easily in the most common situation, which is when I’m typing along and think “here’s a good spot for a footnote.” After typing the snippet’s abbreviation, I’m swept away to the TextExpander app, where I fill in the part of the marker string that comes between the caret and the closing bracket.

TextExpander fill-in in action

After typing in the marker text and tapping the Done button, I’m taken back to Drafts, where the snippet has been put in place. And here’s where this solution is less than ideal. Moving from Drafts to TextExpander and back takes Drafts out of edit mode—when I return from the round-trip to TextExpander, there’s no cursor blinking in the draft, so I can’t just start typing in the footnote text. I have to tap after the colon to reestablish editing mode and set the cursor where it needs to be to type the footnote text. The need to shift back into editing mode (which will happen with any TextExpander snippet that uses fill-ins) is annoying and made me search for another solution.

My second solution was a two-step Drafts action that used the Prompt and Insert Text step types. The Prompt step asks for the marker,

Prompt for marker

and the Insert Text step uses that marker to insert text in a manner very similar to the TextExpander snippet

Insert Text step

In practice, there were two problems with this solution, both having to do with Drafts’s mode shifting. As with the TextExpander snippet, after the text is inserted, I need to tap after the colon to put Drafts back into editing mode and set the cursor. But even before that, when the Prompt window appears, the text field in it doesn’t have focus, so I have to reach up from the keyboard to tap in it before typing. So this was actually worse than the TextExpander solution.

To get what I really wanted, automation that didn’t take me away from the keyboard to tap the screen, I needed to do some scripting. What I came up with was a single-step Drafts action that works like this:

When I want to insert a footnote, I type the whole marker string—brackets, caret, and all—where the footnote marker should go.

The low angle feels better when I'm working while sitting
cross-legged on a bed or floor.[^chicago]|

(The vertical bar shows the cursor position.)

Then I invoke the Footnote action, which is in my “Blogging” action set and has both a keyboard button with an asterisk glyph and an external keyboard shortcut of ⇧⌘8 (⇧8 is the asterisk). This adds a blank line, a copy of the marker string, a colon, and a space. And it sets the cursor after the space, so I’m ready to type in the footnote text.

The low angle feels better when I'm working while sitting
cross-legged on a bed or floor.[^chicago]

[^chicago]: |

This solution requires a little bit more typing—I have to type the brackets and caret myself—but requires no tapping on the screen. The action is defined this way,

Footnote action

and its script looks like this:

javascript:
 1:  // Cursor location
 2:  var insertionPoint = editor.getSelectedRange()[0];
 3:  
 4:  // Get footnote marker from text before cursor
 5:  var nearby = draft.content.substring(insertionPoint - 25, insertionPoint);
 6:  var regex = /(\[\^[^\]]+\])$/;
 7:  var fnMarker = nearby.match(regex)[1];
 8:  
 9:  // Insert a blank line and the marker,
10:  // ready to add the footnote text.
11:  var footnote = '\n\n' + fnMarker + ': ';
12:  editor.setSelectedText(footnote);
13:  editor.setSelectedRange(insertionPoint + footnote.length, 0);

Line 5 grabs the 25 characters before the cursor and puts that string into the variable nearby.1 Lines 6 and 7 then search that string for the bracket-caret-string-bracket text that defines a Markdown footnote marker and put it into the variable fnMarker.

Lines 11 and 12 insert the blank line and the marker followed by a colon and a space. Line 13 sets the cursor after the space, ready for typing in the footnote text.

This was, clearly, harder to write than the TextExpander and Prompt/Insert solutions, but because it does what I want instead of almost what I want, it was worth it.


  1. I can’t search through all the text before the cursor, because there may be other footnote markers earlier in the post. I chose to look back 25 characters because I don’t expect to ever use a marker string longer than that. 


[If the formatting looks odd in your feed reader, visit the original article]


Tapped out

Permalink - Posted on 2018-10-27 06:16

Have I ever mentioned how frustrating I find programming in Shortcuts? I have? Well, I’m going to do it again. This time, I’m going to vent about Magic Variables, which everyone says are great but which I despise. Sort of.

First, let me thank Tim Nahumck for gently telling me on Twitter that my Image Info shortcut was horrifically (my word, not Tim’s) inefficient and could be cut down from sixteen steps to just one.1 This one:

Finished shortcut

All those green bubbles come from a single Magic Variable and its many parts. I’m going to show how to put them together—partly because it might help you, but mostly in the hope that doing this will reinforce the use of Magic Variables with me.

First, add the Show Result action to a blank shortcut and tap in the text field to start editing.

Show Result step

We want to get a property of the image that’s the input to the shortcut, so we tap on the magic wand button above the keyboard to get a new view of the shortcut that shows where the Magic Variables are. Because this shortcut is so small, there’s only one, the Shortcut Input.

Show Magic Variables

Tap on that green bubble to add it to Show Results. Once you see it in the Show Results text field, tap on it there to bring up a list of its properties.

Select Magic Variable properties

Since we want the Name property, tap on it, and the property will show up as a sort of extension to the green bubble.

Magic Variable with property selected

After adding a period after the name, we add that same Shortcut Input Magic Variable a second time, but this time we scroll down to choose the File Extension property.

Choosing File Extension part

Keep doing this again and again, each time choosing a different property of the Shortcut Input. When we’re done, we get this, which I’m showing in a wider view, so you can see the line breaks.

Finished shortcut

This is a far more idiomatic shortcut than my original. Not just because it’s one step instead of sixteen, but because it leverages Shortcuts’s real power, much of which lies in its Magic Variables. Which I hate.

OK, I don’t really hate Magic Variables themselves, what I hate is this hunting around to figure out where they are (in multi-step shortcuts, they appear throughout the workflow) and what they can do. Intellectually, I know this is essentially the same as looking through documentation, but it feels different. Documentation allows you to get a broader view of the capabilities of a language; even when you’re looking up a specific feature, in good documentation you see related features at the same time. Shortcuts treats this process like a game of hide and seek.

And although my inefficiency in building the original version of this shortcut was due to my ignorance, even after Tim cured me of that ignorance, the tap-tap-scroll-tap bullshit of adding each new piece to the Show Results step was maddening. The Shortcuts programming environment doesn’t reward you for knowing what you want to do; you still have to tap and scroll and tap and scroll to get it done.

Maybe I’ll get used to Shortcuts programming. I hope so, as it’s the only game in town. My fear, though, is that it’ll be like AppleScript, a language I’ve been using for over 20 years and have never felt comfortable with.


  1. Had I been checking my email, I would have seen that Matthias Fichtner sent me a similar message before Tim did, and I would have credited him for the improved shortcut. Great minds think alike (and my mind wasn’t one of them). 


[If the formatting looks odd in your feed reader, visit the original article]


Image Info shortcut

Permalink - Posted on 2018-10-26 22:01

Update Oct 27, 2018 1:21 AM
This shortcut, with the same functionality, has been completely rewritten in a more idiomatic Shortcuts style. You should probably read that post instead of this one.

When reviewing images in my iOS Photos library, I often find myself wishing I could see what size they are (both in pixels and in bytes) and what format they are (screenshots start out as PNGs, but when you edit them they turn into JPEGs). And if I’m going to email them or upload them to a server, it’s nice to know their filenames. The Photos app isn’t very helpful with any of those things, so I made a Shortcut that is.

To use the Image Info shortcut, I simply select an image in Photos and run the shortcut to display an information window.

Image selection and info

The filename includes the extension, which gives the format. Then comes the size in pixels (width×height) and the size in bytes (kilo or mega). Very handy.

Although you can download the shortcut and study it yourself, here are all the steps:

Image Info shortcut

(This stitched-together screenshot1 is courtesy of Picsew, which I learned about today from Gabe Weatherhead.)

As you can see, Image Info starts by saving the input image to a variable. It then repeatedly queries that image for the various pieces of information, saving each to its own variable. Finally, the last step shows all of the collected information, formated for easy reading.

You might be thinking it’d be nice to include the time and date the image was created. I think so, too, but I haven’t been able to get that to work. Whether I ask for the image’s creation date or modification date, what appears in the results window is the current date and time, which isn’t very helpful. I guess I should file a bug report.

Update Oct 26, 2018 5:24 PM
Instead of Creation Date and Last Modified Date, which are EXIF names I’m used to, I should have tried Date Taken, which does provide the correct date.

Get Details of Images options

Thanks to CFD on Twitter for setting me straight. The downloadable shortcut now includes the date and time, even though the screenshot above doesn’t.

And yes, the Photos app does show the date and time for an image; I just think it’s nice to include it with the other info.


  1. And elongated enclosing frame, which I’m not sure I’ll want to use again. 


[If the formatting looks odd in your feed reader, visit the original article]


What’s up?

Permalink - Posted on 2018-10-26 03:23

During the Upgrade podcast’s October Event Draft episode, Myke Hurley and Jason Snell discussed1 the likelihood of Face ID coming to the iPad (high) and what that means for people who use their iPads mostly in landscape mode. They were talking about landscape as a single orientation, but I see it as two.

People have been thinking about this since Face ID came out last year with the iPhone X. Face ID on the X—and, so far, on the X🅂— works only when you point the device toward your face in portrait orientation. While this is fine for phones, which are used overwhelmingly in portrait, it would be a disaster for iPad Pros, which are commonly used as pseudo-laptops in landscape orientation.2

So everyone breathed a sigh of relief last month when Steve Troughton-Smith found code to support landscape Face ID in an iOS beta. And earlier this month, Guilherme Rambo followed this up with a very confident post declaring that

The 2018 iPad Pro will include Face ID with the same image signal processor as the iPhone XS, iPhone XS Max and iPhone XR. Further, we can confirm that Face ID on the new iPad Pro will work in both portrait and landscape orientations, though it won’t work upside down.

So, problem solved, right? I hope so, but I have a nagging doubt. Not whether landscape will be supported, but whether both landscapes will be supported.

I have this Sena Vettra folio case for my current iPad Pro and would like to have something similar for my next one. Like the Apple Smart Cover, the Vettra can position the iPad at an upright angle, similar to a laptop screen,

Vettra in upright horizontal position

and at a lower angle

Vettra in low horizontal position

I use both of these. The upright angle feels better when I’m watching a movie or working at a desk. The low angle feels better when I’m working while sitting cross-legged on a bed or floor.3

As you can see from the position of the home button in those two images, there are two landscape orientations, and I use both of them a lot. I’m haunted by the last bit of the Guilherme Rambo quote: “though it won’t work upside down.” If Apple decides that there’s a right-side up and an upside down to landscape orientation—as there is to portrait—I’m screwed.


  1. In front of a live audience that included me. 

  2. Because I read a lot on my 9.7″ iPad Pro, I use it in portrait mode fairly often, possibly even as much as I use it in landscape. I think I’m unusual in this, and even I’d hate Face ID in portrait only. 

  3. Twenty-five or -six to four. 


[If the formatting looks odd in your feed reader, visit the original article]


A big target

Permalink - Posted on 2018-10-20 19:37

While wasting time on YouTube the other night, I came across this Computerphile video (a spinoff of the Numberphile series) on Fitts’s Law and its application to graphical user interfaces. It’s appalling in the number of things it gets wrong, especially with regard to the Mac.

As a warmup, let’s start with the things that are only half wrong.

Contextual menus (about 4:20 in the video)

Contextual menu

“You don’t need to make any movement whatsoever. So that is a target that’s really, really easy to get to.”

It is true that contextual menus pop up right where your cursor is, but that doesn’t necessarily mean there’s zero cursor movement involved. The context of a contextual menu—the thing you are interested in operating on with an item from the menu—is the thing under the cursor, e.g., a file icon or a selected text string. While it may be true that the thing of interest is already under the cursor (like selected text right after you’ve made the selection), it often isn’t. And when it isn’t (the usual case when the thing of interest is a file) you have to move the cursor to the target, and in those common cases the contextual menu is no easier to use than any other operation that requires targeting the cursor.

Corners and the X button (5:20)

Windows X button

“If you put a target in the corners of the screen, what you have essentially done there is create a target that is infinitely wide.”

Again, it is absolutely true that the edges of the screen are infinitely wide in a Fitts sense. And the corners are infinitely wide (actually semi-infinite, because they have a definite beginning but no definite end) in two directions, which makes them easier to hit than any of the other edge locations. But the example used for this principle, the X button in Windows, is only in the top right corner of the screen if you’ve expanded the window to full screen. Otherwise, it’s just a normal target.

I’ve noticed that less sophisticated Windows users, and users who typically work in just one app at a time, do tend to keep their windows fully expanded. For these users, the X really is infinite in two directions. But even for these users, how valuable is this? Should the easiest action to accomplish in an app be to quit it? Maybe for some apps, but not in general.1

At this point, Dr. Wiseman goes off the rails, saying things about the Mac user interface that are just plain wrong. The errors come on so rapidly and are so intertwined that it’s hard to separate them.

The Mac close box (6:00)

Pretend Apple window chrome

“Then I think Apple brought it [the X button] down and made it into a circle, so they made the target from being infinitely massive to a tiny little circle… which is kind of silly of them.”

Now you see what set me off, don’t you?

  • The clear implication is that Apple took Microsoft’s wonderful X button and ruined it by making it smaller. Surely a researcher in Human Computer Interaction knows that the Mac came before Windows, so why suggest the opposite?
  • Maybe she’s not implying Windows came first. Maybe she means Apple ruined the infinitely massive X button Xerox used in the Alto or Star. Nope.

    Star screenshot

    (Image from Prosthetic Knowledge.)

  • As anyone familiar with Apple GUI history knows, the current circular close button evolved from its own earlier square close box. It wasn’t shrunken down from a significantly larger interface widget.
  • Even if Apple were to expand its close button to fill the corner of the window, it still couldn’t be put in the corner of the screen because the top of a Mac screen is taken up by the menu bar. The menus, therefore, which are accessed repeatedly during normal use of an app, are infinitely large in one dimension. This is not a coincidence. Apple deliberately designed the user interface this way to take advantage of Fitts’s Law for common actions. How do I know this? Because back in the 80s when I started using a Mac, you couldn’t read an article on its user interface that didn’t mention Fitts’s law and the menu bar. Read anything by Bruce Tognazzi or Jef Raskin.
  • Quitting (or closing a window—we’ll get to that in a bit) is a destructive action. Apple didn’t think it was a good idea to make destructive actions the easiest ones. Dr. Wiseman may disagree, but it’s wrong to suggest Apple was being thoughtless or silly.
  • The close button on a Mac is not the same as the X button on Windows. While there are some exceptions (single-window apps), the close button typically closes the window without quitting the app.
  • The close button on a Mac is, of course not in the upper right corner of a window, it’s in the upper left. This is more an indication of laziness (or ignorance) on the part of the video editor than a user interface issue, but I couldn’t resist mentioning it.

One of the things Mac users pointed out when Windows came out (apart from all the copying) was that Microsoft’s decision to attach its menus to the app windows meant that its users couldn’t take advantage of Fitt’s Law when accessing menu commands. I’ve often wondered whether the enormous toolbars so common to Windows apps nowadays are an attempt to make up for that.

And Apple does allow its users to take advantage of the infinite Fitts size of corners for quick actions, but they are limited to system-wide actions (since the screen corners aren’t associated with any particular app) that aren’t destructive.

Hot corners

You can fling the pointer into a corner to perform any of these actions—no need to click—but nothing bad happens if the pointer wanders into a corner by accident.

The original Mac/Lisa interface designers spent a lot of time thinking about Fitts’s Law and other interaction matters. That we’re still using most of what they came up with three and half decades later is strong evidence that they knew what they were doing.2


  1. It is, however, definitely important to be able to quit, as newbie vi users can attest (:q!). 

  2. Overall, I’d say they did a better job than the iPhone/iOS designers, much of whose work is being redone to make the iPhone and iPad ready for their second decades. 


[If the formatting looks odd in your feed reader, visit the original article]