#if DEBUG in Swift

Sigh.

The Swift team give an impeccable impression of a group of people who’ve never actually tried to use Swift.

An incredibly basic compiler task is to provide code a way to distinguish between debug & release builds, in order that it can behave accordingly (e.g. change the default logging verbosity, change asserts from fatal to non-fatal, etc).

Long story short there is no way to do this that works correctly with swift build.

You can make it work with Xcode only by way of a simple workaround – you manually define a custom Swift flag in your target’s settings (here’s one of a bajillion explanations of how do this).  You end up with basically identical code to other C-family languages, e.g.:

#if DEBUG
    let logVerbosity = 1
#else
    let logVerbosity = 0
#endif

But there is no way, when using swift build, to specify custom Swift flags in your package config.

You can specify them manually with every single swift build invocation, e.g.:

swift build -c debug -Xswiftc ‘-DDEBUG’

But now you have extra work and the possibility of screwing it up (e.g. omitting the flag, or mismatching it to your actual build style).

The closest you can get is to use some undocumented, hidden Swift internal library functions:

func _isDebugAssertConfiguration() -> Bool
func _isFastAssertConfiguration() -> Bool

These are defined in swift/stdlib/public/core/AssertCommon.swift.

Only the first is likely to be useful.  The second applies in the case where you’re building not just for release but unchecked (-Ounchecked).  If you just want to conditionalise on release builds generally, you have to do !_isDebugAssertConfiguration().

The additional problem with this approach is that these are then, in your code, runtime checks.  And the compiler then thinks it’s being helpful by pointing out that some of your code that uses them is unreachable.

And of course Swift has absolutely no way to silence compiler warnings, or otherwise tell the compiler not to trust its reachability checks.

Sigh.

Update (February 2018)

While the above method – using the _isDebugAssertConfiguration() method – does still work at time of writing, in Swift 4.1, there are some marginally better ways now available.  There’s still not a proper solution, infuriatingly, but with some creativity, as you’ll see momentarily, you can get pretty close to expected functionality.

First alternative

This will only suit some cases, but its relative cleanliness & simplicity makes it appealing when it is an option.

You can use #if targetEnvironment(simulator).  This of course only distinguishes between the simulator and a real iDevice environment, though that can often be useful too, perhaps orthogonally to DEBUG vs RELEASE concepts.

Second alternative

Wrap all uses of _isDebugAssertConfiguration() inside a minimal set of functions.  All this gets you is a reduced (and fixed) number of unreachable code warnings, though.

For example:

func inDebugBuilds(_ code: () -> Void) {
    if _isDebugAssertConfiguration() {
        code()
    }
}

func inReleaseBuilds(_ code: () -> Void) {
    if !_isDebugAssertConfiguration() {
        code()
    }
}

You can then use these in a fairly streamlined way:

inDebugBuilds {
    print("Hello, I only greet in debug builds!")
}

inReleaseBuilds {
    print("While I only greet in release builds - aloha!")
}
Third alternative

It’s possible to do one better than the above, and eliminate those unreachable code warnings entirely.  Plus, doing so actually removes any use of unofficial APIs… but it requires abusing the official API a bit.

The built-in assert() method is implemented atop _isDebugAssertConfiguration() just the same as inDebugBuilds() is, above.  However, because it’s part of the Swift standard library, you don’t have to be concerned about its implementation, its use of private / undocumented language, compiler, or library features – that’s up to the Swift compiler team.  Most importantly, use of it in your code doesn’t emit an unreachable code warning.

So we can do something like:

func inDebugBuilds(_ code: () -> Void) {
    assert({ code(); return true }())
}

Ugly and obtuse, but functional and reasonably expected to work in all future versions of Swift.

You can similarly create a variant for release-build-only code, though it’s even hackier:

func inReleaseBuilds(_ code: () -> Void) {
    var skip: Bool = false
    assert({ skip = true; return true }())

    if !skip {
        code()
    }
}

Icky, but it works.  On the upside, you only have to define this ugliness in one place in your module, and then you can try to forget about its implementation. 😝

One caveat is that I don’t know if the above approach will properly strip out the unreachable code from your built binaries.

Another caveat with these is that since you’re invoking a function, you can’t do a natural if … else … pattern – instead you need explicit, distinct inDebugBuilds & inReleaseBuilds blocks.  So it can’t be completely like the vanilla #if DEBUG … #else … that C-family languages have had since before the dinosaurs.  You could create versions that take two closures as parameters – one for the affirmative case, one for the other… the trade-off is that your invocations are then a little more verbose and obviously function-cally, e.g.:

func forBuildStyle(debug debugCode: () -> Void,
                    release releaseCode: () -> Void) {
    var debugBuild: Bool = false
    assert({ debugBuild = true; return true }())

    if debugBuild {
        debugCode()
    } else {
        releaseCode()
    }
}

forBuildStyle(
    debug: {
        print("I'm built for debuggin'!")
    },
    release: {
        print("I'm built for the wild!")
    }
)

Up to your personal preferences as to which API you adopt, and which implementation you choose (streamlined but private-Swift-bits-dependent with bonus unnecessary compiler warnings, or official-APIs-only but hacky).

EXIF metadata stores random gibberish for dates & times

I hadn’t ’til yesterday realised that EXIF metadata doesn’t actually store dates & times correctly.  Whoever came up with the spec all those decades ago clearly didn’t know how to work with dates & times correctly.  This is immensely frustrating since now we have countless images taken with timestamps that are collectively gibberish.

The problem is that the standard doesn’t specify time zones in almost all cases (the sole exception being for GPS timestamps, which are in UTC).  Which means if you see the date & time “2016-02-03T10:36:33.40” in your photo, that could be any actual time give or take ~25 hours to either side of that.

I realise now, in hindsight, that programs like Aperture & Lightroom manage this by implicitly associating a time zone with photos as they’re imported (and both have controls of varying degrees for ‘correcting’ the time of the photos, in cases where the camera’s clock is set wrong – including being set to the wrong time zone).  They leave it to the user to ensure the time zone that’s set for import matches what was on the camera at the time the photos were recorded.

However, if you’re processing images at scale and don’t have that explicit information from the user(s), you’re SOL.

Additionally, I don’t know anyone with a DSLR who hasn’t at least occasionally forgotten to change the date & time on their camera to account for changes in daylight savings time, or movement to a new time zone.  If the time zone were recorded, this wouldn’t really matter since you could reliable change it later.  But since it’s not, it’s impossible to tell programatically when and where the time zone changes, in a given series of photos.

Now, you might think that since the GPS timestamp is actually recorded as a real, definitive time, that you could just use that to determine the time zone of other dates & times in the metadata (by simply looking at the difference between them).  Unfortunately, in this case, the GPS timestamp is defined as the time at which the GPS data was recorded, not when the photo was created (or edited, or any of the other types of timestamps recorded in EXIF metadata).  Which means that in practice the GPS timestamp can be an unspecified & unpredictable amount of time older than the other timestamps[2. And that’s assuming the camera’s clock isn’t set wrong anyway – it’s possible to include GPS data in your photos but not sync the camera’s clock, in at least some popular cameras like Nikon’s.].

If it were just a matter of a few minutes difference then this wouldn’t be an issue, since the vast majority of the world only acknowledges half hour increments in time zone steps[1. Wikipedia reports that there are a couple of small regions of Australia & New Zealand which use 15 minute offsets, and the entirety of Nepal does too, but those are the only exceptions.  And only a small minority use half hour offsets, as opposed to hour offsets, to begin with.] and thus you could just round and get things right most of the time.  Unfortunately, at least some notable GPS implementations in popular cameras have potentially huge deltas (hours or more) – e.g. all of Nikon’s SnapBridge cameras, including the D500, D5600, & D3400.

Swift’s String.write(toFile:…) can’t handle tildes

let path = "~/Desktop/sigh.txt"
try "WTF?".write(toFile: path, atomically: true, encoding: .utf8)

Result?  Explode:

Error Domain=NSCocoaErrorDomain Code=4 "The folder “sigh.txt” doesn’t exist." UserInfo={NSFilePath=~/Desktop/sigh.txt, NSUserStringVariant=Folder, NSUnderlyingError=0x1018110b0 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}

And since there’s no documentation on that write() method, and this is obviously a perfectly reasonable request that can’t sanely yield that bizarre error message, you could be forgiven for having no idea how to fix this.

Long story short, in macOS Sierra at least that write() method happens to be implemented as NSString’s writeToFile:atomically:encoding:error: method.  If you look at the documentation for that method, it states:

path:  The file to which to write the receiver.  If path contains a tilde (~) character, you must expand it with stringByExpandingTildeInPath before invoking this method.

WTF it can’t actually say that in the Swift method’s documentation (or indeed why it has none at all), or why the exception thrown can’t at least give the real reason, or of course WTF it can’t just do the right thing and handle tildes in the path…  I have no idea.  This smells like a combination of laziness, snafu, and poor judgement in carrying over bad behaviours and design flaws from Objective-C.

So instead you have to do:

try "WTF?".write(toFile: (path as NSString).expandingTildeInPath, atomically: true, encoding: .utf8)

Sigh.