SpamSieve 3.3.1
SpamSieve 3.3.1 is an update of my Mac e-mail spam filter that improves the filtering accuracy and includes various optimizations, fixes, and updates for macOS 27. Some interesting engineering issues were:
I’m still supporting macOS 10.13, but the app can now build with the macOS 12 deployment target in preparation for Xcode 27. The way Swift handles calling deprecated APIs remains annoying. You need workarounds like protocols to suppress a warning in a particular region of code. Often, I am updating to a newer API, but I want to continue using the old one on earlier versions of macOS. Right now I am heavily testing on macOS 26, so I would like to only deploy the new code there. Sometimes the initial versions of APIs are buggy, and I don’t want to have to retest every feature on every version. But Swift doesn’t like me to gate the calls to macOS 26 and later if the new API was actually introduced in, say, macOS 11.
The most risky update was probably switching to the Swift API for
Scanner. This is tricky because the current scan position changes from being based on UTF-16 indexes toString.Index. And in some cases my code was intentionally scanning into the middle of a combining character. I was really glad to have extensive tests here.Recent versions of Tahoe introduced a Core Data bug that broke save recovery (when invalid Unicode can’t be written to the database and we have to fix it up first). When the error comes in, SpamSieve looks under the
NSDetailedErrorsKeykey, but the value of this key is"NSDetailedErrorsKey"while the actual error info is under the key"NSDetailedErrors".Database corruption combined with a Core Data bug and two Swift behaviors were causing crashes. The root problem, I think, is that in this situation Core Data neglects to set the error pointer for
-[NSManagedObjectContext countForFetchRequest:error:]. Normally, Swift would detect this and throw a genericnilError. However, this method signals errors by returningNSNotFoundinstead ofnil. It’s annotated asswift_error(nonnull_error), which I think means that Swift doesn’t even look at the return value, only the (unset) error. So my code was receivingNSNotFoundon the non-error path, treating it as a real count, and doing some math with it, which (by default in Swift) leads to a crash if there’s overflow. Now my code checks forNSNotFoundand usesInt.addingReportingOverflow().I reimplemented Core Data batch fetching because of two problems with the
fetchBatchSizeoption. First, it logs spurious errors when fetching from multiple stores at once. Second, it leads to high memory use. It will load objects in batches, but it never releases the earlier batches. You can manually refault objects that you no longer need, but it will still hold onto the full object shells, which can add up. My implementation only holds onto IDs, and it can fetch the IDs using a different context than the objects to push more work to background threads.I’m now using
String.makeContiguousUTF8()in various places to avoid slow iteration of bridged strings.There was a regression where some menu code could crash on macOS 10.13.
-[NSMenu setItemArray:]is only available on macOS 10.14 and later, whereas the getter is always available. With old Objective-C, the two methods could be annotated with separate availabilities. The Objective-C property syntax doesn’t allow this, so there’s no way for the compiler to flag calls where the setter is missing.The AppleScript support works much better with large data sets. The key, I think, is to never return an array to Cocoa for a scripting property/element. First, if you give it a lazy array it will sometimes try to access all the elements even if it only needs a few. And, second, AppleScript will run out of memory if the array is too large. By using indexed accessors, you can maintain laziness and only convert one object to a descriptor at a time. By caching a batch-fetching collection, you can limit the number of fetches and respond quickly when AppleScript requests individual objects.
Previously:
- Xcode 27 Announced
- SpamSieve 3.3
- Swift Proposal: Precise Control Flags Over Compiler Warnings
- Swift at 10
2 Comments RSS · Twitter · Mastodon
I'd love to see a developer who still supports 10.13 write an article for non-programmers to explain the whole "how far back can software support" issue.
I regularly see apps that do quite complicated things - SpamSieve here, or, I think ChronoSync that are still supporting old OS versions, and yet are very pleasant to use on modern systems.
And then there's apps which seem positively lightweight in how taxing they should be in terms of the work they do, and they're starting on the Sequoia + only thing.
To be honest, it just seems like a bit of a cop-out; web developers have had to think about backward compatibility since year dot, seems like a lot of "app" developers are less keen to put in that work.
@Someone It really depends on the app and what it’s doing and which technologies the developer has chosen to use. In a lot of cases, I think it’s mostly a matter of choosing to put in some more work maintaining and testing extra code paths that you keep around as you modernize things. My apps also include old-style toolbar icons for pre-Big Sur.
For a brand new app starting today, it’s a different story because maybe you want to design your app around a technology that didn’t exist in earlier versions. So then supporting them might require writing two versions of the same thing, and it’s hard to do new development for old versions because current Xcode doesn’t even run on those systems. There’s little business case for this because you don’t have old customers to support, and people sticking with older versions aren’t buying much new software.
In the middle is if you have an app that currently runs on an older version and you want to rearchitect it or something. This is what I did with SpamSieve 3, and I ended up writing three different corpus importers, optimized for the APIs that are available on different OS versions. But there may also be cases where a particular feature is central to the app but can’t really be done on older versions at all.