swiftui

I’m happy to announce a new app today: Citator for iPhone and iPad.
The brain-child of Clemens Bauer, Citator is a currently free app that lets you store, cherish and share memorable quotes.

Earlier this year, Clemens – whom I’ve known from the time we worked together on a local Apple Retailer’s customer loyalty app – approached me with an idea for an app to safe-keep quotes in.
Previous apps he had used for this appeared to become abandoned, and he desired a fresh, modern approach.

Features

  • A beautiful list you can filter or search to quickly find quotes you’ve stored before
  • Save Quotes from the app, or from any app using the Share- or Action sheet
  • In addition to the quote itself, you can specify the author of a quote, where and when it originated, and where you found it
  • Author’s images are automatically loaded from Wikipedia if available
  • Display a static quote you select on your Home- or Lock Screen (requires iOS 16) with a widget
  • Display a random quote (which can change in an interval of your choosing) on your Home- or Lock Screen (requires iOS 16) with a widget
  • Share quotes as text, or as beautifully rendered images
A quote shared as an image from Citator
Automatic loading author images from Wikipedia in action.

Pricing and Availability

Citator is currently free, available for iPhone and iPad on the App Store.
The app requires iOS 15 or newer and is currently localized in English and German.

TidBits and Fun Facts about the App and its Development

SwiftUI

The app is 100% SwiftUI, which, apart from a few SwiftUI widgets here and there for my other apps, marks my first fully-featured SwiftUI app.
I’ve always said that learning something new is always easiest on-the-go. That’s how I learned Objective-C many years back, it’s how I learned Swift, and now, it’s how I’m learning SwiftUI.
But It’s been a struggle for sure, and I don’t know yet what to think of it. I do like parts of it.
Yet I passionately hate that sometimes the simplest of things require workarounds upon workarounds. I’m not saying I know a lot about SwiftUI (I do not!), but as “the future of developing for Apple platforms” (paraphrasing here), it’s nowhere where it should be, in my opinion.
You want different code-paths for different iOS versions in your @ViewBuilder? You’re entering a world of pain.
You want to show a simple share sheet pre-iOS 16? Not available natively.
You want to show a popover for a specific row in a list? Yeah, right, virtually impossible.
There are a lot more like these, and it can be very frustrating.
And I admit, I don’t quite understand the premise of SwiftUI. Years and years were spent on creating graphical user interfaces, to the point where, in Xcode, for example, one could drag user interface elements for the app you’re working on where you want them to be and have an options interface to configure them to look the way you desire.
Now, the supposed future is going back to command-line-like interface programming for GUIs? Isn’t that a step back?
For now, I’d choose Xcode’s storyboards and auto-layout over SwiftUI any time.

It me.
List Row Backgrounds

The background of a quote in Citator is a blurred, darkened (or lightened, in light mode) version of the author’s image.
However, sometimes, there is no author specified for a quote, or an image cannot be found, which would render that quote’s background solid black or white, making it stand out in an out-of-place fashion.
Luckily, I found an – in my opinion – elegant solution. I thought it would be neat to have a background image created from the quote text itself. I don’t know why, but the concept of the quote text itself creating its own, unique background felt fitting to me, especially for this app.

Colors can be represented as hexadecimal values (i.e., #FF0000 for red, #00FF00 for green, or #0000FF for blue). All I needed was to turn the quote text into a hexadecimal representation.
The hashing algorithm MD5 takes data and creates a hash value of it, which consists of characters from A-F and from 0-9, which is the same hex color values consist of.
So when no author image is available, Citator creates an MD5 hash of the quote text and a bit of “salt” for added randomness (the author and the date), splits it up into individual, 6-character/digit strings and uses those to create a blurred, darkened/lightened gradient background image.

Colorful quote backgrounds, created from the quotes themselves.
Customizable Widgets

I love how the widgets turned out.
You can have static ones, where a quote you select will be displayed until you change it, and you can have dynamic/random ones, where the quote changes in an interval you define, with rules you set up.

A Citator widget, displaying a quote by Joan Baez.
Options for a dynamic / random-quote widget.

I also adore the new Lock Screen widgets. Clemens in particular was very happy that widgets can now be displayed on the Lock Screen in iOS 16, and they are a perfect fit for Citator.

Two Citator widgets on the iOS 16 Lock Screen.

The quotes used in App Store promotional material

At first, I thought I’d use quotes from movies (like in the short video above, with quotes from the fictional characters Dr. Ellie Sattler, Dr. Ian Malcolm and Indiana Jones). But then I got worried about copyright issues and scrapped the idea.
After a while of thinking about it, my mind wandered to Apple’s Think Different campaign, and that’s when I had the solution to my problem.
All quotes featured in the App Store promotional screenshots are by personalities featured in Apple’s Think Different campaign, like Amelia Earheart, Orson Welles, Alfred Hitchcock, Joan Baez, Martha Graham and John Lennon.


Clemens and I hope you enjoy Citator.
There’s more cool stuff yet to come!

Read more

Yoink v3.6.5 re-introduces its clipboard history feature and the accompanying widget.
Here are a few details about its implementation for macOS Big Sur and newer.

The No-Button-Action Conundrum

Widgets on macOS Big Sur and up (and iOS, for that matter) can’t really react to a user’s click on them by themselves, so you cannot run any logic from a user’s input.
A click onto a widget will always bring you back to its containing app, where you can then run logic accordingly.
On iOS, that’s obvious, as it’ll open up the owning app and put it front and center.
On macOS, that can be a little more subtle, because macOS allows apps to run in the background (like Yoink does, mostly).

When you click on an item in Yoink’s widget, it’ll tell Yoink to run the logic to copy the clicked item (or pin it, or add it to Yoink, or reveal it in the browser, depending on the modifier key you pressed during the click).

But there’s a problem with that in the case of Yoink:

The Foreground Conundrum

No matter where you click on a widget (either the background of it, or a SwiftUI Link() object), it will bring its containing/owning app to the foreground.
For many apps, that will be fine. But Yoink is an app that runs in the background and only very rarely needs to actually become the active, keyboard-input-accepting app.

So I thought, perhaps I can have another application inside Yoink’s app bundle, which is LSBackgroundOnly, with a custom URL scheme that would be called from the widget using a SwiftUI Link() object. An app that has LSBackgroundOnly set to YES in its Info.plist cannot present any user interface, and cannot become “key”, even if the system tells it to.
But there’s another roadblock – even though only the secondary app inside Yoink’s app bundle should be able to open the custom URL scheme, the widget will send that Link() to its containing/owning app only, no matter the url scheme. In this case, the main Yoink app.

The point of the widget is to quickly re-copy something and then paste it somewhere right away.
Say you're editing text in TextEdit, then open the widget, click on it to re-copy something and then press command-v to paste it into TextEdit only to hear a beep because the app isn't active anymore (because Yoink is). So you have to click into the TextEdit document to make it active, and only then can you paste. That can (and will) become annoying very quickly.

In order to have Yoink never become the active app from a click on its widget, I’d have to move the widget plugin/appex bundle out of the Yoink app bundle’s PlugIns folder, into the secondary target one’s.
Only that way would the widget attempt to make the secondary target(and thus, not Yoink) frontmost – and fail because of LSBackgroundOnly, leaving the currently frontmost app active – and send the custom URL scheme link to it. The secondary target would then forward the link to the main Yoink app bundle.

Tada, it works. But that lead to yet another problem. (When did “it just works” turn into “it just won’t”? And while I’m at it, what’s with those widgets? They feel quite… neutered to me compared to what they were able to do before.)

The Widget Recognition Conundrum

So far, I managed to have Yoink not become active when an item in the widget is clicked.
But now, with the Yoink app containing the secondary target containing the widget, macOS was unwilling to recognize and show the widget in Notification Centre.
Interestingly, a double-click onto the secondary target in Finder would make it show right away.
Infuriatingly, launching the secondary target quietly from within Yoink at launch won’t – and I had a lot of different approaches:
– Launching the secondary app directly with NSWorkspace’s -launchApplicationAtURL:…
– Launching the secondary app via its URL scheme
– Having Finder open the secondary app
– Running an NSTask open -a <secondaryapp> /path/to/somefile
– Registering the app using LSRegisterURL

None of it worked. I figured, the problem was that it was Yoink launching the secondary app, not the “system” or “user”, like it was the case when double-clicking it in Finder.

Which made me think of login item helper apps.
Inside the macOS app sandbox, an app cannot set itself as a login item. It needs to have another “helper” app inside its Library/LoginItems folder which it designates as a login item, and that app will then in turn launch the containing app. Nuts, but there it is.

The point is, the system launches those login item helper apps, not the containing app.

Heureka. In Yoink, at launch, I set my secondary app, which contains the widget bundle, as a login item with SMLoginItemSetEnabled, causing the system to launch it. Now, finally, Notification Centre recognizes and shows Yoink’s widget.

What a needless journey.

Read more