Zhenyi Tan And a Dinosaur

Random SwiftUI Complaint #123

SwiftUI can be really annoying when you try to do basic UI customization. Consider this list:

🙂

Let’s say you want to get rid of the left padding of the separators. After some googling (which is another problem because Google thinks you want to remove the separators completely), you find the .alignmentGuide modifier with .listRowSeparatorLeading. So you add the code:

.alignmentGuide(.listRowSeparatorLeading) { _ in
    -100 // set it to -100 just to be safe
}

But the navigation bar’s separator still has the padding:

😓

You don’t want to switch to .listStyle(.inset) because that would remove the navigation bar’s separator. After more googling, you find nothing useful. But setting .toolbarBackground to .visible will make the separator full-width. So you add the code:

.toolbarBackground(.visible, for: .navigationBar)

The navigation bar now has an off-white background color, but you can live with that. The separator seems to be full-width!

… until you pull down the list:

🫠

After even more googling, you cannot find any way to remove the left padding of this first separator. The closest thing you can do is to use a .listSectionSeparator modifier to hide it. So you do that:

.listSectionSeparator(.hidden, edges: .top)

Of course, the “real SwiftUI way” is to not remove the left padding of the separators. But sometimes you just want things to look a certain way, right? And that’s where SwiftUI can really, uhh, test your patience.

Launch: Unprocrastinator

Unprocrastinator is a Safari extension that makes you wait 30 seconds before you can see some websites.

Welcome back to my regular program of making weird apps.

Lately, many platforms have turned, uhh, not-so-nice to use, and a lot of people are saying they want to stop using them. Now you can do just that with Unprocrastinator. Unprocrastinator puts a 30-second delay on your chosen websites. It’s like a mini time-out for your brain before you dive into the internet rabbit hole.

This idea comes from an xkcd comic:

After years of trying various methods, I broke this habit by pitting my impatience against my laziness. I decoupled the action and the neurological reward by setting up a simple 30-second delay I had to wait through, in which I couldn’t do anything else, before any new page or chat client would load (and only allowed one to run at once). The urge to check all those sites magically vanished—and my ‘productive’ computer use was unaffected.

The 30-second delay is not customizable on purpose. It’s long enough to be a little annoying, but not so long that you’ll want to turn off the extension right away.

Note: When you first use Unprocrastinator, try not to add too many websites all at once. It could end up being more frustrating than helpful.


Unprocrastinator Pricing

Unprocrastinator costs $0.49 on the App Store. (I’m trying out the new App Store price points.) It does not include any subscriptions, in-app purchases, ads, or tracking. It’s a universal purchase, which means once you buy it, you can use it on all your Apple devices.


Unprocrastinator Privacy Policy

Unprocrastinator does not collect, store, or transmit any personal information.


Unprocrastinator Support

If you have any questions, you can contact me via email or Mastodon.

Youtube’s Anti-adblock and uBlock Origin

Recently, YouTube has been ramping up its anti-adblock effort, and I’ve been watching this closely. This blog post is where I write down what I know.


Some Background

Here’s how adblockers (used to) block YouTube ads. Before playing a video, YouTube would check its API, and the server would send back something like this:

{
  "video": "something.mp4",
  "ads": [ad1, ad2, ad3],
  "etc": { ... }
}

And the adblockers would override JSON.parse and Response.json to make it return this instead:

{
  "video": "something.mp4",
  "ads": [],
  "etc": { ... }
}

This trick worked for a few years. But earlier this year, YouTube started making fake requests to see if the responses were changed. If the responses were changed, it meant the user was using an adblocker.

Most adblockers stopped working, but a few like uBlock Origin and AdGuard updated their filters to avoid these fake requests. Then YouTube would update their fake requests so the adblockers would fall for them. It’s been a game of cat and mouse between YouTube and the adblockers ever since.


The Tech Support Hell

YouTube isn’t rolling out the anti-adblock to everyone. It seems to depend on things like your account, browser, and IP address. And if you’re not logged in or you’re in a private window, you’re safe. As a result, there are a bunch of people saying, “I use XYZ and I haven’t seen an anti-adblock popup yet,” unknowingly spreading misinformation.

But here’s the thing: YouTube isn’t just targeting adblockers. Use Privacy Badger? You’ll get flagged. Use Malwarebytes? You’ll get flagged. Set your Edge browser’s tracking protection to “strict”? Yep, you’ll get flagged. So a lot of people think their extensions are safe to use, but actually they’re not.

And contrary to what you might think, using multiple adblockers can actually make things worse. That’s because all your adblockers need to be up-to-date to dodge YouTube’s detection.

As you can imagine, this is creating a tech support nightmare if you’re part of an adblocker team.


The Redditors

On Reddit, the uBO team put up a detailed post on how to handle YouTube’s anti-adblock. But many people don’t actually follow it. You’d see people saying, “I did what the post said but I’m still having issues.” But when they’re asked to share their system info for troubleshooting, it turns out they didn’t really follow the post.

Then there are non-tech-savvy users looking at the post and saying, “This is too complex. I give up.”

Then there are tech-savvy users who say, “Your filter has CODE in it. That’s risky. Can you explain what it does? I don’t want to run anything I don’t understand.”

And of course, there’s always the classic “IVE TRIED EVERYTHING AND NOTHING WORKS HELP!!!!

All this noise makes it hard to find any useful info.


The Stupid Filters

Some people have been sharing custom filters that use CSS to hide the popups. But that’s like sticking your head in the sand. Because the popups don’t stop there: first, it’s just a friendly warning, then you can only close the popup after a delay, then you get a “three strikes and you’re out” popup. And finally, no more popups are shown, but at this point, you can’t play videos anymore.

Recently, someone shared a filter on Twitter that literally had code to set adBlocksFound to 0. It’s as if they think YouTube’s anti-adblock works like this:

if (adBlocksFound > 0) {
  blockUser(); //!!!!!
}

That tweet got super popular. And it probably led a lot of people to add those filters to their adblockers. This has really piled on the work for the troubleshooting team. Did the person who shared it not realize it was harmful? Or did they just care about getting likes?


The Script ID

Every time YouTube tweaks their script, part of the URL changes. This part is what uBO calls the ID, and they have a webpage that keeps track of the latest one.

But here’s where things get messy. Some people think this ID is what they need to block. Some even suggest ways to automate the process (like, “Why don’t you just block that with REGULAR EXPRESSION?”)

Another issue is that sometimes YouTube pushes out an update that has nothing to do with adblock. But it still changes the ID. Then you get people saying, “The ID changed, why hasn’t this post updated yet???”


The Moderator Who Quit Reddit

All this resulted in a ton of pressure on the uBO team members who were trying to help out in the thread. One by one, I saw them say they’d had enough of the comments and weren’t going to reply in these threads anymore.

And then one of the moderators actually deleted their Reddit account. “The ID in the post wasn’t updated because my mother was hospitalized,” they said.

It’s sad to see them leave because of some drive-by comments — new users who sign up for Reddit, leave their comments, and then delete their accounts without facing any consequences.

Sure, there are people who appreciate what the uBO team is doing. But the hurtful comments leave a bigger mark than the good ones.


The War of Attrition

Since May, uBO has been in a cat-and-mouse game with YouTube. And they’ve shown incredible resilience, especially when you consider that there are only two people on the uBO team dealing with YouTube.

The uBO team members are all volunteers. They’ve gone above and beyond to meet every little request from their users. But there’s a limit to how much they can take. At some point, the constant demands become too much, and they will leave uBO for good. It’s one thing to play cat and mouse with YouTube. It’s quite another to deal with a wave of angry users.

Maybe that’s how YouTube will win this war of attrition.

Make History Book More Responsive with This One Weird Trick

TLDR: Create a folder and move your saved pages into it.

Sorry about the “one weird trick” title 😬

If you’ve been using History Book for a while, you probably have thousands of saved web pages. This can really slow down the application. I suspect this might be due to SwiftUI somehow not lazy-loading all items, but I’m not 100% sure.

The good news is that you don’t need to load all the web pages to use History Book. The search works across all folders, so you can organize your saved web pages in any way you like.

(I considered adding an official Archive and Trash folder in History Book, but I didn’t want to impose any specific folder structure on users.)

Anyway, here’s how to do it:

  1. Create a new folder and name it Archive or something.
  2. Go back to your History folder with 15,000 pages.
  3. Press Command+A to select all the pages.
  4. Go do something else while the cursor beach-balling, it’s gonna take a while.
  5. Click the Move to… dropdown in the toolbar and select the new folder.
  6. That’s it! You can now enjoy a more efficient History Book.

Note: if you have enabled the auto-remove webpages option, History Book will not delete the old pages in the Archive folder. If you prefer to do this manually, you can create a new folder each month and name them “2023-01”, “2023-02”, etc., and delete the folder when they’re too old.

I’m Back

Err, umm, hello. 😬

Long time no see, it’s been a while since I last wrote anything. Sorry for my absence — I took some time off to deal with a burnout.


What happened

So long story short, I stupidly tried to create a custom video player that looks like the Safari video player, but with features like chapters support, showing thumbnail images when you mouse over the progress bar, and other customization options. It turned out to be too difficult for me. But due to sunk cost fallacy, I continued my attempts.

Since I can’t promote my apps on Twitter now (more on that later), I wanted the video player update to be a big, newsworthy one. The problem was, the more time and effort I put into it, the longer I neglected to update my existing apps, and the more their quality deteriorated. Then I felt more pressured, like “I really have to deliver this thing now.” Then I began to lose self-confidence.

Eventually, I found myself with no motivation left for my work. Although my brain knew what needed to be done, my body just couldn’t follow through. It’s like a self-fulfilling impostor syndrome, and now I am the impostor.


Other stuff

Then I started worrying about the business-y part of my business.

Twitter (and sometimes Reddit) used to be my go-to place for announcing my apps and talking to users. It was a valuable platform for small-time indie developers like me. But then Twitter changed their owner, Reddit enshittified their platform, Mastodon is niche, Bluesky is invite-only, and Threads… just doesn’t seem like the right fit. I’m left wondering: Where do I go from here?

I also thought about the potential unsustainability of my business model. My apps are paid upfront, and I’ve tried to keep their prices (I think) reasonably affordable. And I don’t charge anything for updates. However, this means I’ll have to rely on a steady influx of new users to sustain my business. I mean, I won’t switch Vinegar to a subscription model, but I just feel concerned about whether I can maintain this approach?

Then I listened to Under the Radar #234, where Marco Arment said:

I can’t tell you how many ideas I’ve had for other content-blocking methods and ways to make the web more tolerable. I hate the modern web. But if I made an app that was like made the web more tolerable, it would feel like janitorial work to me. Now every day I have a new pile of crap that the web has given me that I have to figure out how to deal with. And that’s just like a negative life for me. I don’t want that.

This really made me think: most of my apps fall into the “make the crappy web tolerable” category, and sometimes, it does feel like janitorial work. Do I want that?

I don’t have the answers to any of these questions.


Now what

I still haven’t recovered from the burnout. But I shipped an update to History Book a while ago, so yay me? I’ll now focus on shipping some inconsequential updates to Vinegar next and slowly ease my way back. I hope I made it.

Adding “Open in Mastodon” to your Mastodon App

I’m not launching a new app. This is just a simple guide for developers of Mastodon apps.

On iOS, the “open in X” behavior is typically implemented with Universal Links. However, since Mastodon is decentralized, there is no official list of URLs developers can use. Fortunately, we can create a simple Safari extension to work around this issue.

“I call on the power of the Mastodon!”


Creating the Safari Extension

  1. Register a custom URL scheme for your app from the Info tab of your project settings.

  2. Create a new Safari extension by choosing File → New → Target…, then choose Safari Extension.

  3. In the manifest.json file, update the "content_scripts" property to match every web page.

    "matches": [ "http://*/*", "https://*/*" ]
    

    You can also remove the "service_worker" and the "default_popup" properties if you don’t plan to use them.

  4. In the content.js file, paste in the following code:

    // A more reliable way is to check for an API response,
    // e.g. https://domain.name/api/v1/timelines/public?limit=1
    // but that requires an extra round trip to the server.
    // So we just do something quick and dirty:
    
    if (document.querySelector('[id*=mastodon]')) {
    
        // If you want to open your app only from the profile and post pages,
        // you can add an additional if statement.
        // if (location.pathname.match(/@(.+?)(\/|$)(\d+)?/))
        // The regex in this statement also lets you access the username and post ID,
        // in case you want to use a custom URL format.
        location.href = `blackranger:${location.host}${location.pathname}${location.search}`;
    
    }
    

  5. That’s it!

Note: You will also need to handle the incoming URLs in your app by implementing the application(_:open:options:) method in your AppDelegate. If your app uses SwiftUI, you can use the onOpenURL(perform:) view modifier to handle the URLs.


Customizing the Extension

  1. You should update the extension’s name and description in the messages.json file.

  2. You should also add some icons for the extension. Use your app’s icon as the basis for the extension’s icons, but remember to add the rounded corners yourself. The toolbar icons should be monochrome PNG files with an alpha channel.

  3. To ensure that the extension is effective, you should instruct your users to enable the extension and grant it permission to access all websites.

  4. As an advanced option, you can add the domains of Mastodon URLs to a database when opening the app from those URLs. This will allow you to navigate directly to the appropriate view in your app when the user taps on a link without going through the app-Safari-app hoop.


The source code for this project is available on GitHub, and I’m happy to answer any questions you may have. You can reach out to me on Mastodon or via email.

Launch: Rewinder

Rewinder is a Safari extension that lets you go back in time and see how websites looked in the past.

Okay, but that’s just marketing-speak. Rewinder is actually a front-end for the Wayback Machine API, and its UI is inspired by Time Machine. But instead of using arrow buttons, you can navigate through different snapshots by simply scrolling the page.

(Not shown: the time it took to load all of these snapshots.)

You’re probably already familiar with Wayback Machine, so I won’t repeat its benefits here. (In case you’re not familiar with Wayback Machine, here’s a blog post generated by ChatGPT for you.)

Keep in mind that archive.org is very slow, so refresh the page if snapshots take too long (say, longer than 1 minute) to load. And avoid opening multiple archive.org pages too quickly, or you may encounter the 429 Too Many Requests error.


Rewinder Pricing

Rewinder is available for $1.99 on the App Store with no subscriptions, in-app purchases, ads, or tracking. Plus, it’s a universal purchase, so you only need to buy it once to use it on all your Apple devices. I hope you find Rewinder useful.


Rewinder Privacy Policy

Rewinder uses the Wayback Machine API, so check out their privacy policy. The Safari extension does not collect, store, or transmit any personal information.


Rewinder Support

If you have any questions, you can contact me via email or Mastodon.

Launch: Medley Music Player

Medley is a music player that looks like the pre-iOS 6 Music app.


The Backstory

A few weeks ago, I was dealing with some SwiftUI bugs in the iOS 16 beta.

The toolbar was buggy, so I replaced it with a custom view. The navigation bar was buggy, so I replaced it with another custom view.

Then it suddenly hit me: if I can replace the navigation bar and toolbar, I can probably build an iOS 6 style UI with SwiftUI. Then I added background images and text shadows to my custom views, and it just… worked.


Sidenote: SwiftUI and Customization

The system components are notoriously hard to customize in SwiftUI. Like you’d get 4 methods for customizing something, and if they don’t do what you want, that’s it. The navigation bar and toolbar are probably the hardest to customize because they’re not even Views in SwiftUI. (They get automatically generated when you use some modifiers.)

So if I can do without the navigation bar and toolbar, I should be able to build the rest of the iOS 6 style UI with relative ease.


Anyway

So I wanted to make an app that looks like a pre-iOS 7 app. Ideally, it should have a healthy mix of images and text in the UI to keep it interesting, it shouldn’t require a lot of text input because the flat keyboard will break the illusion, and it shouldn’t be a UITableView snooze-fest. In the end, I decided to make a music player. I call it Medley.

Medley is not a pixel-perfect re-creation of the Music app. For example, it uses San Francisco instead of Helvetica, and almost all the icons are SF Symbols. It doesn’t have Cover Flow because I’m not sure why Apple discontinued it after the patent suit.

There’s a Cover… Wall thingy, though.

You can play music from Apple Music if you’ve already added them to your music library. But Medley doesn’t support browsing the Apple Music catalog or adding songs from Apple Music into your library. I hope you’ll like it.


Medley Pricing

Medley is available for $2.99 on the App Store, with no subscriptions, no in-app purchases, no ads, and no tracking.

Currently, Medley is only available on the iPhone. I will consider making an iPad version if there’s enough interest.


Medley Privacy Policy

Medley does not collect, store, or transmit any personal information.


Medley Support

If you have any questions, email me or contact me on Mastodon.

Launch: Sideways

Sideways is a Safari extension for rotating webpages when screen rotation is off.

I still can’t get over how weird this looks.

If you always keep the screen orientation on your phone locked, but… gah! I think this app is quite self-explanatory. Check it out if you want.


Sideways Pricing

Sideways is free, with no in-app purchases, no ads, and no tracking. Get it in the App Store today.


Sideways Privacy Policy

Sideways does not collect, store, or transmit any personal information.


Sideways Support

If you have any questions, email me or contact me on Mastodon.

Launch: Monies

Now you can also track your monies like it’s 1979!

Monies is a very basic expense tracker app I made for my wife. It looks like a spreadsheet because she’d been using Excel to track her expenses. But it’s like a spreadsheet on… whatever the opposite of steroids is.

The whole point of the interface is to let you add items quickly. Just tap anywhere to start editing, then tap next on the keyboard to jump to the next cell. To delete an item, just clear its price and name, then tap Save.

There are no fancy features like linking bank accounts, charts, or even a settings screen. It is available for free on iPhone and iPad, and it syncs via iCloud. I hope you find it useful.


Monies Privacy Policy

Monies does not collect, store, or transmit any personal information.


Monies Support

If you have any questions, email me or contact me on Mastodon.