Zhenyi Tan And a Dinosaur

New in Web Inspector: Updated Layout, Search HTML, and Network Responses

Web Inspector 1.2.1 is out! This is a big update.

Updated layout

I moved all the buttons to the top because hunting for the buttons when Safari’s toolbar keeps appearing and disappearing is annoying.

Also, I moved the Inspect toolbar from the Elements tab to the DOM tab. The new layout should make more sense if you’re used to Web Inspector on the desktop: view the DOM elements in DOM, then switch over to Elements to see its properties and CSS styles.

Also, also: I removed a few redundant (read: useless) buttons.

Also, also, also: on non-mobile-optimized web pages, Web Inspector will scale its UI up so it won’t appear comically small on the iPhone.

Highlight and select

Now that the Inspect toolbar is in a more prominent location, perhaps I should talk about them since many people have never noticed them before.

The Highlight button (second from the left, looks like an eye) toggles the highlighting of the selected DOM element. On desktop, you get the highlight by hovering over DOM elements, but we can’t do that on mobile, so yeah, a button.

The Select button (first from the left, looks like a mouse cursor and a square) is a classic Web Inspector tool. Tap on it to start selecting any items on the web page, and see it in the DOM tree. Web Inspector will temporarily hide the panel while you’re doing this. This button triggers highlight mode. Remember to turn this off if you want to interact with items on the web page again.

The Edit button (third from the left, looks like a pencil) lets you edit the selected DOM element.

Search the HTML

The Search button on the right lets you search the HTML. You can cycle through the search results, and Web Inspector will try to move them (both the item and its representation in the DOM tree) into view. This button triggers highlight mode.

Formatted network responses

There’s a new View Response button on the details page of a network request. You can tap the button to see the prettified, syntax-highlighted network response. It works for HTML/CSS/JS, other text files, and images.

Check it out

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

Launch: ActiveTab for Safari

If you’re wondering why I made this, read this article by John Gruber.

ActiveTab makes it easier to spot the active tab in Safari on Mac by drawing a line below it. It works by predicting the position of the active tab based on the size of your browser window and the number of tabs.

“The extension [...] undeniably makes it easier to spot the active tab in Safari.” - MacStories
“It’s worth every penny.” - iMore
“ActiveTab simply makes it easier to spot the active tab in Safari.” - MacRumors
“It makes the tab instantly recognizable, even when you’re not looking directly at it.” - Cult of Mac
“It solves the problem of creating enough visual contrast between active and inactive tabs.” - iDownloadBlog
“It’s the latest must-have for Mac.” - Creative Bloq


There’s a bug in Safari that causes web extensions to disappear:

If you’re just looking for a solution to the issue, here it is: quit Safari, copy the following command to the clipboard, open the Terminal app (located in the Utilities subfolder of the Applications folder), and paste the command.

/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f -R /Applications/Safari.app


ActiveTab is available for $1.99 on the Mac App Store, with no in-app purchases, no ads, and no tracking. I hope you find it useful.

ActiveTab Privacy Policy

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

ActiveTab Support

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

By the way, let me know if you’re interested in ActiveTab for iPad.

Launch: AMP Shockwave

Filing this under “meh, why not.”

EMP Shockwave!

AMP Shockwave is a Safari extension that redirects AMPs to regular web pages.

There’s no user interface and no settings. It’s just one content.js file with 40 lines of code. I extracted the code from Time Capsule, and it works in my limited use case. If you like simple things, check it out.


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

AMP Shockwave Privacy Policy

AMP Shockwave does not collect, store, or transmit any personal information.

AMP Shockwave Support

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

Search Filter Is Now Available on TestFlight

Introducing my latest app, Search Filter! Search Filter is a Safari extension that lets you delete search results from spammy domains.

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 domains, and recover them if you want.

Once you delete a search result, Search Filter will delete other search results from the same domain, 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 websites that publish clickbait, content farm listicles, or SEO’ed-to-the-death “content.”

Search Filter currently only works on Google, but I will consider adding support for other search engines.

I’m still not sure about the price. How much should I charge for this? $3.99? Free, but with a $0.50/month subscription for syncing your blocklist? Let me know!

Disclaimer: Search Filter is beta software, and it is buggier than your average app. Google’s HTML seems randomly generated these days, and there must be a lot of edge cases that I’ve overlooked.

If you see any bugs (usually in the form of “delete buttons appear where they shouldn’t be”), send me a screenshot and let me know:

  1. the search query
  2. the URL of the search page

If you would like to TestFlight Search Filter, send me an email or DM me your email on Twitter, and I will invite you to the beta.

Launch: Web Inspector for iOS

Web Inspector is now available in the App Store 🥳!

Web Inspector is a Safari extension on iOS and iPadOS that works like its desktop counterpart but is less powerful. It allows web developers to edit web pages on the fly, debug JavaScript, and more.


Web Inspector is free, with no in-app purchases, no ads, and no tracking. You can get it in the App Store today. I hope you find it useful 🙏.

Web Inspector Privacy Policy

Web Inspector does not collect, store, or transmit any personal information.

Web Inspector Support

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

Safari Web Inspector on iOS

So, I made another app.

It’s called Web Inspector, a browser console for Safari on iOS and iPadOS.

It works pretty much as you would expect: tap the blue “i” button to open Web Inspector, tap it again to close.

It has 6 tabs: DOM and Elements let you inspect elements on a webpage, Console is your JavaScript console, Network lists network requests and responses, Timing displays the loading time of resources, and Resources shows you cookies, local storage, etc.

It’s not as powerful as the Web Inspector on the desktop because it’s basically a glorified JavaScript bookmarklet. Think Firebug Lite instead of Firebug.

Web Inspector will be free, with no in-app purchases, no ads, and no tracking, because I feel like this kind of developer tool should be free. I mean, Firebug was free, right?

Update: Web Inspector is out!

If you’re on the iOS 15 beta and would like to TestFlight Web Inspector, send me your email address or you can wait until iOS 15 is out. I hope to get Web Inspector in the App Store on day 1 if possible.

I will make another announcement when I launch it in the App Store.

Dismissed Share Extensions (Can) Still Run in the Background

There’s a weird bug in Time Capsule: when you’re in the middle of saving a web page, sometimes you can tap “Cancel” and still get the output HTML.

Actually… this behavior seems useful! If I can reliably reproduce this, I can then dramatically cut down the saving time and let the users get back to their browsing sooner. Unfortunately, no one on the internet is talking about this. Apple’s documentation is also unhelpful: “Tells the host app to cancel the app extension request.”

So to test it out, I created a simple app: the main app shows a number and a reset button, the share extension increment the number while running. I ran it, and here’s what happened:

Nice. It seems like the extension continues to run even after being dismissed.

I’m a bit tempted to add an “It’s now safe to close this popup” label in the extension UI, but as long as the behavior remains undocumented, it’s too risky.

So if you tried it and it works for you, great! But keep this between you and me, OK?

Marketing Is Hard

I know nothing about marketing.

Actually, I do know a bit about marketing. Based on my very limited knowledge, marketing is hard. But engineers are supposedly good at solving hard problems, right? Programming and design are both hard, and I’m not terrible at them.

So for many years, I’ve been trying to “solve” marketing. Maybe there’s an algorithm or something that I can implement, and I just haven’t figured it out.


Recently, I read a blog post by Yongfook about how he got his first 25 users. He didn’t talk about abstract things like “build the hype” or “define your campaign goals,” he just listed out the things he tried:

  • I launched big new features on ProductHunt 3 times - until people grew sick of me
  • I wrote about bootstrapping on my blog and built 8 other products before this one
  • I tweeted about my startup constantly and sometimes those tweets went viral
  • I implemented a referral credit system that had zero effect
  • I started an affiliate programme that has had some small effect
  • I built a shopify version of my product which was a failure, and shut it down

…and that's just the stuff I can remember right now.

So, is marketing literally metaphorically throwing everything at the wall to see what sticks?

Explain like I’m Five: GPL and AGPL

If I understand correctly, GPL basically means “if you use this library, your app must also become GPL, and you must open source your app.” No buts, no what-ifs.

AGPL is GPL, plus “you must also open source your app even if it’s a web app.”

GPL is like King Midas’ golden touch.

A few days ago, I discovered that I was using an AGPL-licensed JavaScript library in Time Capsule. I don’t want to open-source my app, so:

  1. I removed the library and rewrote the feature in the iOS app.
  2. I keep the library in the browser extensions because they’re just dumb clients that send data to my server.
  3. The server is a Rails app that doesn’t use the library.

In this case, I only need to open source the browser extensions, right?

Gotchas of Testing Subscriptions with Sandbox, Part 3

Here’s how to handle in-app purchase transactions in StoreKit:

  1. Implement SKPaymentTransactionObserver.
  2. Wait for paymentQueue(_:updatedTransactions:) to get called when transactions are updated.
  3. Loop through the transactions:
    1. If the transactionState is .purchased or .restored, unlock the in-app purchase.
    2. If the transactionState is .failed, show an error or something.
    3. Other states are less important, and you can ignore them if you want.
  4. Call finishTransaction(_:) on .purchased, .restored, and .failed transactions to remove them from the queue.

If you don’t call finishTransaction(_:), the transactions will remain in the queue and show up over and over again. But it seems like finished transactions don’t always get removed in the sandbox environment.

I noticed that the receipt validation endpoint on my server gets called every time I launch my app. But it’s only supposed to happen when I process the transactions in paymentQueue(_:updatedTransactions:). So I added a few print statements, and as I suspected, paymentQueue(_:updatedTransactions:) is getting called whenever I launch the app.


Maybe there’s a bug in my code? So I simplified paymentQueue(_:updatedTransactions:) to this:

func paymentQueue(
    _ queue: SKPaymentQueue,
    updatedTransactions transactions: [SKPaymentTransaction]
) {
    for transaction in transactions {
        switch transaction.transactionState {
        case .purchased, .restored:
        case .failed:

Nope, the transactions still keep coming back.


Next, I implemented paymentQueue(_:removedTransactions:) to confirm that the transactions are getting removed from the queue:

func paymentQueue(
    _ queue: SKPaymentQueue,
    removedTransactions transactions: [SKPaymentTransaction]
) {
    print("Removed \(transactions.count)")
    print("Remaining \(queue.transactions.count)")

They were removed, but come back after a relaunch for some reason.


And no, I didn’t register the observer twice: I start observing in application(_:didFinishLaunchingWithOptions:) and remove the observer in applicationWillTerminate(_:).

I’m not really sure what’s going on? I just hope that this is one of the gotchas with subscriptions and sandbox and doesn’t happen in production.