In my app dataFude for Simulator I’ve decided to offer some features for free and others only to users who pay for the app.

This way, everyone including students and hobbyist can meaningfully use the app, while professional users can support the development.

Some features disabled because only available to pro users

I considered few different approaches including compiling two separate versions of the app, but ultimately went for shipping a single binary that relies on RevenueCat and enables/disables some parts of the UI depending on the purchase status.

SwiftUI view modifier for paid-only UI

I ended up building a single view modifier, that can enable or disable content across the app. Let’s have a look.

Firstly, I added a new custom ViewModifier that would either:

  • display its content unchanged, or
  • disable user interaction for its contents and optionally insert a label on top:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import SwiftUI

struct ProFeatureModifier: ViewModifier {
  let revenueModel: RevenueCatModel
  let message: String?
  let font: Font?

  func body(content: Content) -> some View {
    
  }
}

My ProFeatureModifier takes my model class that exposes the app subscription status received from RevenueCat and optionally two more parameters — a custom message to show on top of the disabled piece of UI and additionally a custom font for that message in case I need that.

The body of the modifier just checks my model and renders either of the two options above:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
VStack(alignment: .leading) {
  if let message, revenueModel.status != .pro {
    HStack {
      ProUpgradeButton(billingPeriod: .constant(.monthly))
      Text(message)
    }.font(font)
  }

  content
    .opacity(revenueModel.status == .pro ? 1.0 : 0.7)
    .allowsHitTesting(revenueModel.status == .pro)
}

Note how if the UI is disabled I put it at 70% opacity next to actually disabling the user interactions for a little bit of added drama. 🤡

The final step is to add the modifier method so you can add it directly to SwiftUI views in your view hierarchy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
extension View {
  func proFeature(
    _ model: RevenueCatModel,
    message: String? = "This feature is available in dataFude Pro",
    font: Font? = nil
  ) -> some View {
  
    modifier(ProFeatureModifier(
      revenueModel: model, 
      message: message, 
      font: font
      )
    )
    
  }
}

Converting features to paid model in practice

Finally, let’s have a quick look how this looks in practice. It takes just a minute to convert buttons, menus, and whatever else you have to paid features.

Let’s add the new modifier to the list of auto-capture groups in the app’s preferences:

1
2
3
4
5
6
List($preferences.autoCapture) { $group in
  .. render each group ..
}
.border(Color.controlOuterSelection, width: 1)
.frame(maxWidth: .infinity)
.proFeature(revenueModel, font: .callout)

proFeature lives alongside all the other modifiers in the SwiftUI code above like border and frame. revenueModel is my already-initialized RevenueCat model object.

That’s all the code changes to convert editing auto-capture groups to a paid feature:

dataFude preferences with some of the UI disabled

When clicked, the “Upgrade to PRO” button opens the subscription UI from just about anywhere in the app.

And after purchasing a Pro plan, that preferences tab from above looks like this:

dataFude preferences

Where to go from here?

Obviously, check out dataFude for Simulator to super-charge your debugging flow.

Thank you!

My DMs are open at https://twitter.com/icanzilb and https://mastodon.social/@icanzilb