Tobi Goes to Shipaton
I submitted Tobi to RevenueCat's Shipaton.
Last week, I submitted Tobi to RevenueCat’s Shipaton. This was the whole goal with Tobi, get it submitted to Shipaton as something I would want to use and use it to test out some big theories I’ve been seeing in the indie developer space, like hard paywalls and a longer onboarding. Throughout the last 2 months, I learned that those 2 probably work when you have a lot of people coming into the app. Due to Tobi’s lack of users or downloads, I didn’t really see an impact either way, so I decided to make it freemium with a shorter onboarding. The idea is to let more people get into the app as quickly as possible.
Numbers
Last month:
- MRR: $66
- iHog New Users: 53
- Camp Notes New Users: 27
- Tobi New Users: 109
This month:
- MRR: $80
- iHog New Users: 67
- Camp Notes New Users: 22
- Tobi New Users: 153
Experiment Report
From September 6 to 19, I tested the hard paywall against freemium with 3 free tasks on Tobi. People weren’t converting either way, so I was hoping to get more people through onboarding with freemium. In PostHog, 15 people got the hard paywall and 12 got freemium. One person in the freemium group set up a household within 7 days, and nobody in the hard paywall group did. That’s not enough people to call a winner, but freemium showed me more clearly where people drop off, so I rolled it out to everyone.
I unfortunately still haven’t seen many people complete onboarding. They usually drop on the first screen of onboarding, where you name your home. Next, I’ll remove that screen from setup because maybe people are wondering why they need to name the home.
Technical Spotlight
Tobi 1.5.0 added Siri, Shortcuts, and Spotlight support. Here’s the pattern, made generic: an “Open a tab” action for any SwiftUI app.
// OpenTabIntent.swiftimport AppIntents
import Observation
enum AppTab: String, AppEnum {
case home
case search
case settings
static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(name: "Tab")
}
static var caseDisplayRepresentations: [AppTab: DisplayRepresentation] {
[
.home: DisplayRepresentation(title: "Home"),
.search: DisplayRepresentation(title: "Search"),
.settings: DisplayRepresentation(title: "Settings"),
]
}
}
@MainActor
@Observable
final class AppRouter {
static let shared = AppRouter()
var selectedTab: AppTab = .home
func open(_ tab: AppTab) {
selectedTab = tab
}
}
struct OpenTabIntent: AppIntent {
static var title: LocalizedStringResource { "Open a tab" }
static var supportedModes: IntentModes { .foreground }
@Parameter(title: "Tab")
var tab: AppTab
func perform() async throws -> some IntentResult {
await AppRouter.shared.open(tab)
return .result()
}
}
struct MyAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: OpenTabIntent(),
phrases: [
"Open \(\.$tab) in \(.applicationName)",
],
shortTitle: "Open tab",
systemImageName: "square.grid.2x2"
)
}
}
Put this in one file in your app target and swap in your own tabs. You never call the intent yourself. Xcode picks up the intent and its phrase when it builds the app, so saying “Open Settings in” plus your app’s name works in Siri, and the action shows up in Shortcuts and Spotlight. In your root view, add @State private var router = AppRouter.shared and use TabView(selection: $router.selectedTab). Just remember, supportedModes needs iOS 26 or later.
It helps because the intent never touches your views, which might not be on screen when it runs. It only sets the tab on AppRouter, and your view picks it up when it appears.
This Month’s Focus
In October, I am focused on the following:
- Getting JABA to v0.1, which is local only without credit card payment logic
- Remove the onboarding screen from Tobi that asks for the house’s name
- Finish the postgres migration for Camp Notes
I'm looking for my next thing, whether that's full-time, consulting, or a long-term contract. If you're building something where the details matter, I'd like to hear about it. Send me an email.