I wish it is possible to change the background of a List with a .background() modifier.
Yes, I’m aware of the UIKit workaround.
I also wish it is possible to change the height and width of a .sheet with a .frame(height:width:) modifier.
No, I’m not aware any workaround… unless you want to reimplement your own sheet.
2. Some control over the default spacing
Here’s the toolbar of the Notes app:
Here’s the SwiftUI replica:
The alignment of the toolbar buttons are slightly off:
Why? I don’t know. But if you set the maxHeight and maxWidth of the button to .infinity, you’ll notice some non-removable spacing around the button:
And no, .ignoresSafeArea() doesn’t work in this case.
Similarly, it happens to views inside NavigationLink as well:
I wish it is possible to ignore the spacing.
3. Miscellaneous improvements to edit mode in List
The “Done” text should be bold in an EditButton.
The chevrons in NavigationLinks should be hidden in edit mode. (It would be even better if we have some control over the accessoryType.)
Note the whacky animation of the chevrons.
The items in a List should be clickable (e.g. to rename the items) in edit mode.
The state of the highlighted item is buggy in a multiple-selectable List when the List enters/leaves edit mode.
If the items in a List have rounded corners, the .swipeActions buttons should have rounded corners as well.
Make it possible to attach a .confirmationDialog to a .swipeActions button.
Compare it with the confirmation dialog in the Notes app:
Notice how the delete button expands instead of disappears when the confirmation dialog shows up.
You can search your browsing history in Safari, but that only works for the page titles and URLs. It gets tiresome when there are 20 pages with similar titles.
To solve this, some people will bookmark everything and meticulously tag them. Some people will copy URLs into their notes app and then add a description of why they saved them. Some people resort to never closing their browser tabs.
If you have similar problems, you might want to check out History Book.
History Book automatically saves the content of your browsing history for searching. And it does it in a privacy-friendly way.
By default, it only auto-saves pages that contain an article, so you don’t have to worry about it saving your sensitive data.
If you want, you can configure it to auto-save every webpage. If you are feeling paranoid, you can disable auto-save entirely. You can also exclude websites that you never want to auto-save.
Like Safari’s Reader Mode, it extracts and only saves the meaningful text. You can use it as a read-it-later app, but I mostly use it as a search app.
History Book Pricing
History Book is available for $6.99 on the App Store, with no subscriptions, no in-app purchases, no ads, and no tracking.
It’s a universal purchase so you only have to buy it once to use it on iPhone, iPad, and Mac. I hope you find it useful.
History Book syncs your browsing history via iCloud. Apart from the iCloud sync, History Book does not collect, store, or transmit any personal information.
In hindsight, Time Capsule had a few glaring issues:
Web archiving is a niche thing, especially on mobile. Very few people want to save the entire webpage, especially if the saving process is slow.
The saving process is slow. Because the app needs to fetch all the resources, base64-encode everything, and merge them into an archive file. And there are a lot of resources to process.
The archive files were huge. Who would’ve thought an average webpage with an article is about 10MB?
The sync requires a $3/month subscription. I need to rent a sync server to do cross-platform sync. It’s an ongoing cost, and the only way to cover that is to charge a subscription.
It’s not even a good business plan because most paid users will be the people who save tons of data. $3/month may not be enough to break even.
I still like the idea of saving webpages for full-text searching, though, so I gave it another go and created History Book.
Hopefully I got it right this time.
By default, History Book automatically saves webpages as you browse. And it only auto-saves pages that contain an article, so you don’t have to worry about it saving your sensitive data.
Like Safari’s Reader Mode, it extracts and only saves the “meaningful” text. As a result, the saved pages have no ads and are small enough to store in Core Data and sync via CloudKit.
Unlike Time Capsule’s share extension, History Book’s web extension can save pages that require login because it doesn’t need to load the page again in a separate window.
It uses iCloud for storage and sync. Meaning no ongoing server cost. Meaning no subscription.
History Book will be a paid app that costs $5.99 - $9.99 (I’m still deciding) and it will be available on iPhone, iPad, and Mac. I thought about making it free with an in-app purchase to unlock everything, but I don’t know how to cut features without making it crappy.
If you follow me on Twitter, you probably already know about this: I’m making a new app. It’s called History Book, and it automatically saves the content of your browsing history for full-text searching later.
A lot of my searches are looking for things I’ve seen, so…
By default, History Book only saves “readable” webpages, so you won’t see pages like www.google.com littered around. You can also configure it to save every webpage or disable autosave altogether.
The app uses a Safari extension to save webpages, so it only works if you use Safari as your main browser.
There are no “Trash” or “Archive” folders because I don’t want to impose any folder structure on you. (Initially, the app didn’t even have folders.) But you can create and name any folders however you like.
Also, if a saved webpage contains an embedded YouTube video, History Book will replace it with a minimal HTML <video> tag. Because uhh, why not.
The app uses readability.js to check whether a webpage is “readable” and extract the text. It is likely to produce both false positives and false negatives.
The app is written in SwiftUI, which means there are some not-quite-standard behaviors and UI bugs that I can’t fix.
For privacy reasons, I’m using iCloud for syncing. I’m a bit worried about the reliability, though.
I’m now putting the finishing touches on the CloudKit stuff, and I should announce a TestFlight very soon, so stay tuned!
(Yes, there was a second case. Yes, I filed a complaint to App Store a week ago. No, I haven’t heard back from Apple yet. Yes, I think there will be more cases like this in the future.)
Update: Apple has contacted the second developer. The developer has removed the app.
Anyway, Search Filter is a Safari extension that lets you delete spammy websites from Google or DuckDuckGo search results.
It’s like a spam filter for search results.
Here’s how it works: Swipe to delete any search results you don’t like. Tap the extension button to see the deleted websites, and recover them if you want. You can also delete image search results by tapping the red trashcan button.
Once you delete a search result, Search Filter will delete other search results from the same website, too. Deleted search results will stay deleted in subsequent searches.
I built Search Filter out of my frustration with StackOverflow scrapers, but that’s far from the only use case. You can use it to delete Pinterest, Quora, websites that publish clickbait, or other SEO’ed-to-the-death “content.”
Me: mom can we have VPN
Mom: we have VPN at home
VPN at home:
Crappy VPN is not a VPN. I don’t have a VPN, but I have some servers that I can ssh into. So when I need a proxy, I would set up an SSH tunnel. Crappy VPN is an app that simplifies that process.
Crappy VPN is built on top of the code of Staring. I only added a preference window and a few helper methods to execute the shell commands. Try building it yourself! Or download the app if you just want to use it.
Crappy VPN does not collect, store, or transmit any personal information.
Scratching my own itch, you probably don’t need this.
Do you prefer this toolbar color?
The Backstory: Someone asked me if I could make Vinegar toolbar’s appearance customizable. I said that’s probably out of the scope of the extension. Then I looked at the existing Safari extensions on iOS and couldn’t find any lightweight extensions that let you re-style web pages. So yeah, new extension time.
How do you find the CSS selectors on iOS? Well, I have another extension that does just that…
Like Vinegar, Baking Soda is a Safari extension that replaces custom video players with an HTML <video> tag.
Unlike Vinegar, Baking Soda works on all custom video players other than YouTube. It doesn’t block ads or let you switch the video quality, so the changes are mostly cosmetic. Picture-in-picture should work, but I didn’t (and couldn’t) test it on all the websites.
If Baking Soda doesn’t work well on certain websites, you can easily disable it in the settings.
Safari extensions’ code are not compiled (it’s just JS/CSS/HTML). So it’s trivial to get the IPA file, crack it open, and see the extension code inside. It doesn’t mean the code is open-source, though. There’s no license, so copying the code is illegal.
Now let’s crack open their IPA and compare their code to mine.
And this is the de-obfuscated code. Note the points of interest because they’re identical to mine:
Their video.css code is also nearly identical to mine, down to the weird 43px and 31px I use for the toolbar.
All these are pretty conclusive evidence that the developer has stolen Vinegar’s code. I have filed a complaint to the App Store about them. Although the complaint can only “put you in direct contact with the provider of the disputed app.” I’m not sure what to do next. (I suspect the developer will just ignore me. Their “support page” is a Facebook page with no content.)
Update: Apple has contacted the developer. The developer has removed the app.
Vinegar is not the only Safari extension that got its code stolen. A few months ago, the same thing happened to Amplosion. And I think Noir is affected, too.
So I accidentally shipped a critical bug in Vinegar 2.0.1. How critical? Well, it breaks YouTube’s mobile website. The entire web page doesn’t load if you have Vinegar 2.0.1 installed.
The bug doesn’t happen on all devices, which is why I never noticed it. It always worked on my iPhone during testing. Feeling the pressure, I quickly fixed the bug (by running the app through all the Simulators until I could finally reproduce the bug) and submitted an update. Then I waited.
Meanwhile, users have been contacting me nonstop via both email and Twitter. After I apologized and explained the situation, some of them were understanding, saying, “It’s OK, I will wait. Mistakes happen.” But some never replied.
It’s been four days now, a lot of users have left 1-star reviews complaining about the app. I don’t have the heart to see the number of refunds. The data in App Store Connect are one-day late, so I don’t know how many more are coming. Anything that could go wrong went wrong. Murphy’s law is in full force.
Side Note: Developer’s responses to a review also take one day to be approved. The minimum 2-day delay makes the App Store review section the worst supporting channel ever.
Update: After four days, Apple finally approved the update. Here are the lessons I learned from this incident:
Never submit an update in December. It’s the holiday season, and nobody is working. It’s too risky.
Don’t bother requesting an expedited review. The chances of you being granted one are slim. You might as well wait for the regular review.
Expedited review ≠ short review time. My expedited review was approved after being “in review” for 55 hours.
Contacting the app review team is like yelling into a black hole.
Don’t be fooled by emails from Apple Support. Those are just auto-generated boilerplate emails to give you false hope.
Every time you open App Store Connect to check the status, your review time gets delayed by an hour. (Haha.)
Being an Apple developer is like being religious: the Cupertino Gods never communicate, make arbitrary rules, can punish you at any time, and are never wrong.
The lessons are probably wrong, and I know the correct lessons are: be more careful, do more testing, yadda yadda. But what am I supposed to do if the bug doesn’t happen during testing?