December Adventure Day 13
File Sizes and Previews
After taking day 12 off from my Psion December Adventure, I returned on day 13, adding a few quality of life improvements to Reconnect, my modern Psion connectivity software for macOS—I’m planning to spend the rest of the month focused on this and the other tooling that keeps these 30 year old computers going in 2024.
Reconnect
Working on Reconnect feels slower than cranking out OPL but there remain a collection of little fixes that will improve my daily experience and, hopefully, make it a little more enjoyable for others to use.
Continuing my focus on the Transfers window, I plumbed through file sizes so I can show the number of bytes remaining in a transfer. This is particularly useful on low-memory devices like the Psions where it’s easy to inadvertently copy a file larger than the whole device’s storage—the extra context can help spot such a mistake early. I also added support for showing previews of completed downloads which I find helps in quickly identifying the correct screenshots for these write-ups.
It doesn’t feel like there’s much novelty in the underlying code itself, with the minor exception of the approach taken to generating the file previews. This necessitated wrapping QuickLook’s classic completion-block API as a new cancellable async
function to make it easier to call from SwiftUI:
extension QLThumbnailGenerator {
public func thumbnailRepresentation(fileAt url: URL,
size: CGSize,
scale: CGFloat,
iconMode: Bool) async throws -> QLThumbnailRepresentation {
let request = Request(fileAt: url, size: size, scale: scale, representationTypes: .thumbnail)
request.iconMode = iconMode
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
generateRepresentations(for: request) { (thumbnail, type, error) in
if let error {
continuation.resume(throwing: error)
return
}
continuation.resume(returning: thumbnail!)
}
}
} onCancel: {
cancel(request)
}
}
}
While it looks convoluted, this code is actually relatively simple: it uses the platform-provided withCheckedThrowingContinuation
function to convert the classic QLThumbnailGenerator.generateRepresentations
completion-block API into an async
call, and then wraps that with withTaskCancellationHandler
which returns a cancellable Task
that will call QLThumbnailGenerator.cancel
.
It’s frustrating to have to massage platform-provided APIs in this way, but once it’s done, it’s easy to use in SwiftUI:
@MainActor
struct ThumbnailView: View {
let url: URL
let size: CGSize
@State var image: NSImage? = nil
var body: some View {
Image(nsImage: image ?? NSWorkspace.shared.icon(forFile: url.path))
.task {
let thumbnail = try? await QLThumbnailGenerator.shared.thumbnailRepresentation(fileAt: url, size: size, scale: 2.0, iconMode: true)
image = thumbnail?.nsImage
}
}
}
The .task
view modifier ensures the new async API is run once when the view is first attached and cancelled when it’s dismissed (if necessary).
With these usability improvements in-place, I took some time to experiment with adding drag-and-drop support and improving multi-image file conversions—is it better to convert multi-image MBMs to gifs, tiffs, or a zipped directory of image files? Questions for day 14 and beyond.