/dev/trouble
Eric Roller's Development Blog

macOS

Everything related to development for Macintosh.

Creating screenshots is a tedious task that lends itself to automation. Ideally, it would be preferrable if it were possible to have simple, device-idependent testing scripts. The reality is that many exceptions creep into the code that simulates the UI interactions when generating the screenshots:

if UIDevice.current.userInterfaceIdiom == .phone {
    // Do something different for iPhones.
}

But the real problem is maintaining these scripts. Whether it is iOS SDK changes, new device names, new screen sizes, or indeed new apps features, suddenly the scripts no longer work. Even if everything worked nicely last time, the tiniest of changes will trigger hours of UI scripts re-tuning.

I usually find myself having to re-run fastlane over and over in order to create new screen shots. Quite often, the scripts work well for some devices but fail for others. At that point it would be nice if fastlane could be instructed to only re-run the scripts for the devices that had failed. But fastlane has no option for that.

But it is possible to re-code the Fastfile since it is essentially a Ruby script. All I need is a simple check whether the final screenshot exists for a device. When present, don't re-run the lengthy UI scripts for that device.

So here are the relevant parts of my new ./fastlane/Fastfile; beware custom names like MyApp:

delete_all_screenshots = false

swift_macro_defs =
    "SWIFT_ACTIVE_COMPILATION_CONDITIONS='FASTLANE'"
output_directory = "./screenshots/fastlane"

# All languages and devices to run:
all_languages = [ "en-us" ]

all_devices = [
    "iPad Pro (9.7-inch)",
    "iPad Pro (10.5-inch)",
##  "iPad Pro (11-inch)",
    "iPad Pro (11-inch) (4th generation)",
    "iPad Pro (12.9-inch) (2nd generation)",
    "iPad Pro (12.9-inch) (6th generation)",
    "iPhone 8",
    "iPhone 8 Plus",
    "iPhone 14",
    "iPhone 14 Plus",
    "iPhone 14 Pro",
    "iPhone 14 Pro Max",
    "Apple TV",
    "Apple TV 4K (3rd generation)",
]

platform ::ios do
  desc "Take screenshots (iOS)"
  lane :ios_screenshot do

    all_languages.each do |language|
      # Work out the path to the screenshots.
      # Beware: CWD is the directory of this file!
      path_shot = File.join(
          "..", output_directory, language
      )

      run_devices = Array.new

      all_devices.each do |device|
        # Neither 'Apple TV' nor 'Apple Watch' devices
        next if device.start_with? "Apple"

        unless delete_all_screenshots then
          # If the last screenshot exists, skip this device:
          last_shot = device + "-10_Exercises.lite.png"
          last_shot = device + "-09_Exercises.lite.png" \
              if device.start_with? "iPad"

##        puts "Checking if file exists: #{last_shot}"
          file_shot = File.join(path_shot, last_shot)
          next if File.exists?(file_shot)
        end

        run_devices << device
      end

      next if run_devices.empty?

      snapshot(
          project: "./MyApp.xcodeproj",
          scheme: "MyApp-iOS",
          xcargs: swift_macro_defs,
          devices: run_devices,
          languages: [ language ],
          localize_simulator: true,
          reinstall_app: true,
          headless: false,
          concurrent_simulators: false,
          clean: delete_all_screenshots,
          clear_previous_screenshots:
              delete_all_screenshots,
          stop_after_first_error: true,
          number_of_retries: 0,
          override_status_bar: true,
          test_without_building: false,
          output_directory: output_directory,
          disable_slide_to_type: true,
          skip_open_summary: true,
          only_testing: [
              "UITests-iOD/UITests_Fastlane",
          ],
      )

      delete_all_screenshots = false
    end
  end
end

If you want to recreate all screenshots, you could set delete_all_screenshots to true or, well, delete the files manually.

Also note the SWIFT_ACTIVE_COMPILATION_CONDITIONS compiler flag. This allows me to define a FASTLANE macro that can be used in the app's Swift code:

#if FASTLANE
loadExampleData()
#endif

Time Machine can be configured to exclude folders from your backups (see System Settings > General > Time Machine > Options). I usually exclude these:

  • /Library/Caches
  • ~/Library/Caches
  • ~/Library/Developer/CoreSimulator
  • ~/Library/Developer/XCPGDevices
  • ~/Library/Developer/XCTestDevices
  • ~/Library/Developer/Xcode/DerivedData
  • ~/Library/Developer/Xcode/DocumentationCache
  • ~/Library/Developer/Xcode/iOS DeviceSupport
  • ~/Library/Developer/Xcode/tvOS DeviceSupport
  • ~/Library/Developer/Xcode/watchOS DeviceSupport
  • ~/Library/Logs/CoreSimulator

Also, if you need to make space to be able to install Xcode or a macOS update, you can delete the contents of the above folders (assuming you no longer need old device SDKs).

Last night, I found myself in this endless loop:

  1. The Mac OS Catalina installer cancels with the message that "macOS could not be installed on your computer: There is not enough free space on your disk to install", offering me to quit the installation or to restart and try again. The only visible button is "Restart".
  2. Trying to delete files using the Terminal application from the installer is fruitless. It never is enough.
  3. Reboot always returns to the installer since the original start volume is no longer bootable. Back to square one.

Diagnosis

The only tools available from the installer are Disk Utility and Terminal. Opening Disk Utility reveals a mere 10 GB of space on my disk, where it should have been more than 30 GB.

Running "First Aid", and opening the disclosure triangle to get more information reveals that there are three Time Machine snapshots on my disk. Fine, let’s delete them, but how?

Disk Utility offers no commands to handle Time Machine snapshots.

Where is tmutil?

Numerous solutions articles on the web suggest to use tmutil to manage Time Machine snapshots and settings.

Back in the Terminal, I type tmutil, but all I get is:

-bash-3.2# tmutil
tmutil : command not found

It turns out, the installer image is not a complete macOS system and tmutil is not available.

I also searched the contents of my drive, but to no avail:

-bash-3.2# find /Volume/Dizzy -name tmutil -print

Not knowing which commands are available, I find myself typing a<TAB> to see commands starting with 'a', b<TAB>, then c<TAB>. For d<TAB>, I see diskutil, and this is where it the solution lies.

Solution: diskutil

All the steps in order:

  1. If the menu bar in your installer isn't visible, click on one of the icons in the top-right corner of the screen. The menu bar should become visible.
  2. From the menu bar, open the Terminal application.
  3. You can type diskutil to see the commands that are available:

    -bash-3.2# diskutil
    Disk Utility Tool
    Utility to manage local disks and volumes
    Most commands require an administrator or root user
    
    WARNING: Most destructive operations are not prompted
    
    Usage:  diskutil [quiet] <verb> <options>, where <verb> is as follows:
    
         list                 (List the partitions of a disk)
    [...]
         apfs <verb>          (Perform additional verbs related to APFS)
    
    diskutil <verb> with no options will provide help on that verb
    
  4. Note that there are additional "apfs" commands (verbs):

    -bash-3.2# diskutil apfs
    Usage:  diskutil [quiet] ap[fs] <verb> <options>
            where <verb> is as follows:
    
         list                (Show status of all current APFS Containers)
         listUsers           (List cryptographic users/keys of an APFS Volume)
         listSnapshots       (List APFS Snapshots in a mounted APFS Volume)
    [...]
         deleteSnapshot      (Remove an APFS Snapshot from an APFS Volume)
    [...]
    diskutil apfs <verb> with no options will provide help on that verb
    
  5. If you don't know the name of your hard disk, you can use diskutil apfs list to see all disks and their names.

  6. Use diskutil apfs listSnapshots to list the snapshots, e.g. for your disk named 'Dizzy':

    -bash-3.2# diskutil apfs listSnapshots Dizzy
    Snapshots for disk1s1 (3 found)
    |
    +-- F5D46466-3269-4480-BA1A-8BE23DF1800
    |   Name:        com.apple.TimeMachine.2019-10-07-205243
    |   XID:         2201791
    |   Purgeable:   Yes
    |
    [...]
    
  7. To delete a snapshop, use diskutil apfs deleteSnapshot with the UUID copied from the list above:

    -bash-3.2# diskutil apfs deleteSnapshot Dizzy -uuid F5D46466-3269-4480-BA1A-8BE23DF1800
    Deleting APFS Snapshot F5D46466-3269-4480-BA1A-8BE23DF1800 "com.apple.TimeMachine.2019-10-07-205243" from APFS Volume disk1s1
    Started APFS operation
    Finished APFS operation
    
  8. Repeat for the other snapshots.

In my case, that freed up 50 GB on my disk, allowing the macOS Catalina installer to continue.

[Update 2019-10-21: Corrected typo 'apgf' -> 'apfs']

Resizing Finder Columns

- Posted in macOS by

I enjoy using the column view in the macOS Finder, but more more often than not, I need to resize the columns to be able to read all the file or folder names.

Certainly, resizing is easily done by dragging the vertical column separators to the right until all names are readable. What has been bugging me, however, is that when I do that for the right-most column, I often end up inadvertently resizing the window at the same time.

How to resize the right-most column without resizing the window?

It turns out, you can Ctrl-click (or right-click) on a column separator to open a context menu:

Ctrl-click on a column separator

There you can chose to:

  • Right Size This Column to resize the column to the left of the separator;

  • Right Size All Columns Individually to resize all columns such that they are just as wide as they need to be to be able to read all file and folder names;

  • Right Size All Columns Equally to resize all columns such that they are all equally wide and readable.

So, if you chose one of the Resize All Columns… options, the right-most column will be resized. But, if you had selected the right-most separator, the window may still resize!

Therefore: Ctrl-click on one of the other column separators, not the right-most one. When resizing is initiated there, the window size will not be changed.

Finally, here is the best part:

This also works in a standard macOS "File Open" or "Save As" dialog!

Update

[2019-08-16] You can also double-click on the column separator with your mouse:

Double-click on the column separator to Right Size This Column.

Option+Double-click on the column separator to Right Size All Columns Individually.

MacOS: Internet Recovery

- Posted in macOS by

Starting off with a replaced internal SSD on a MacBook Air, it doesn’t boot, obviously. It is necessary to boot into “Internet Recovery Mode” by selecting Command-R at startup, or, as I did, Option-Command-R to recover onto High Sierra.

I erase the disk in Disk Utility and select to install macOS High Sierra. After a few seconds I am greeted with the error:

The Recovery Server Could Not Be Contacted

A number of possible solutions are listed on the Mac OS X Blog. I used the first one, except that using sudo was not possible (command not found ?); I ran date in the Terminal which confirmed that the date was wrong, then I updated it using Apple’s time server (without sudo):

% ntpdate -u time.euro.apple.com

The run date once more to check that the date & time has been updated.

Back in the High Sierra installer, I get asked to select a destination Disk, but my internal SSD cannot be selected, because:

This disk doesn’t use the GUID Partition Table Scheme

The solution is to use Disk Utility to erase the drive, but in the High-Sierra version of Disk Utility, I had no such option to select the partitioning scheme!!! The Partition button is disabled.

I eventually solved it by rebooting into the old recovery mode (Command-R) and using Lion’s Disk Utility to reformat the drive, before returning to the new recovery mode (Option-Command-R). Checking again in Disk Utility looks fine.

Setup

The following steps are:

  • Create a default (admin) user as part of the installation, but don’t use the usual name to avoid conflicts later. Try "Migrator".
  • Do NOT create a standard user.
  • Connect the Time Machine Backup drive and enter its password.
  • Launch Migration Assistant and restore from the Time Machine backup.
  • Restore all files, including those of our standard user (write down the temporary password). Promote the admin user to be admin.
  • Wait for an hour...
  • Re-install applications (which have not been backed-up).
  • Login, change password, re-authenticate iCloud.
  • Correct file ownership of the Things database, from the admin account:

    % sudo chown gollum "/Users/gollum/Library/Containers/com.culturedcode.things/Data/Library/Application Support/Cultured Code/Things/ThingsLibrary.db"
    
  • Delete the Migrator user.

Here are the symptoms: open System Preferences, select Desktop & Screen Saver and click on the Screen Saver tab. In the list of the screen savers, I had an RSS feed which turned out just to be too slow for my aging machine.

However, selecting the feed to delete it just gave me the beach ball, rendering System Preferences totally unresponsive. I gave up after a few minutes. To close System Preferences, click long on its icon in the dock and select "Force Quit".

To delete the RSS feed, I resolved to tracking down the corresponding preferences file and I found this one:

~/Library/Preferences/com.apple.desktopscreensaver.rsspictures.plist

While System Preferences is closed, open the file in a text editor (I like TextWrangler) and delete the feed entry from the dictionary. My resulting file looked like this:




    
        rssPicSubscriptions
        
            com.apple.screensaver.rsspics
            
        
    

Save the file and reopen System Preferences to verify that the RSS screen save is no longer in the list. Done!

Update: I hear that you also can simply delete the file and you may need to restart or at least logout and login again.

Trying to update Xcode 4.0.1 from within the Mac AppStore application, I was greeted with this error message:

You have updates available for other accounts Sign in to (null) to update applications for that account.

There exist numerous forum entries on Apple's discussion board on that subject and the recommended solution is to trash the "Install Xcode" application and then to re-launch the AppStore, selecting to re-install Xcode.

I trashed "Install Xcode" but AppStore produced the same message. Relaunched one additional time and it allowed me to update (note: not install). However, when it was finished, there was no new "Install Xcode" in the Applications folder!

It turns out, the AppStore had located and updated a different copy of the software on a separate drive where I had made a backup, even though I had renamed it to "Install Xcode-4.0.1". Its info window revealed that it had been modified and that its version number now was 4.0.2.

Installation from that modified backup worked correctly.

The old backup could be restored from the version that I had put into the trash.

Compiling an application for the MacOX10.4u SDK (with Xcode 3.2.4, GCC 4.2.1) results in this linker warning:

ld: warning: object file compiled with -mlong-branch which is no longer needed. To remove this warning, recompile without -mlong-branch: /Developer/SDKs/MacOSX10.4u.sdk/usr/lib/crt1.o

This is not a critical issue and can be ignored. However, it can also be fixed as shown by an answer from Bernhard Baehr on lists.apple.com:

What needs to be done is to recompile the Csu package which can be found on Apple's opensource archive, e.g. the one for 10.4.11 x86 where the -mlong_branch flag has already been removed: http://www.opensource.apple.com/release/mac-os-x-10411x86 http://www.opensource.apple.com/tarballs/Csu/Csu-71.tar.gz

Then it's just a question of recompiling it:

cd ~/Downloads
tar zxvf Csu-71.tar.gz
cd Csu-71
make RC_ARCHS="ppc ppc64 i386 x86_64"

Then we can replace the (stripped) object file in the SDK:

cd /Developer/SDKs/MacOSX10.4u.sdk/usr/lib/
sudo mv crt1.o{,.org}
sudo strip -S ~/Downloads/Csu-71/crt1.o -o crt1.o
sudo chmod 644 crt1.o

On Mac OS X 10.5 (Leopard) it appears to work fine, but when I quit my app, on a machine running 10.4 (Tiger), these messages appear in the Console:

2009-03-03 21:49:10.680 MyApp[6793] *** Illegal NSTableView data source
    (). Must implement numberOfRowsInTableView: and
    tableView:objectValueForTableColumn:row:
2009-03-03 21:49:23.139 MyApp[6793] *** -[NSCFString count]:
    selector not recognized [self = 0x1bc7f00]
2009-03-03 21:49:23.140 MyApp[6793] Exception raised during posting
    of notification.  Ignored.  exception: *** -[NSCFString count]:
    selector not recognized [self = 0x1bc7f00]

The interesting fact is that the MyApp class is the delegate of the NSTableView since it implements support for drag & drop. However, it is not designed to be the data source for the NSTableView. Instead, I use an NSArrayController through a binding in the NSTableView instance. This works fine until one quits.

What I suspect is happening (on Tiger) is that the NSArrayController is purged before the NSApp, which suddely remains as the only data source of the NSTableView, but without the access methods implemented.

The solution would be to implement dummy methods in the MyApp class for numberOfRowsInTableView: and tableView:objectValueForTableColumn:row: where one returns 0 and nil, respectively.

Searched for it online and found this solution, which worked fine for me (Xcode 3.0):

defaults write com.apple.xcode  PBXCustomTemplateMacroDefinitions  \
        '{ ORGANIZATIONNAME = "tredje design"; }'