RevenueCat / purchases-ios

In-app purchases and subscriptions made easy. Support for iOS, watchOS, tvOS, macOS, and visionOS.
https://www.revenuecat.com/
MIT License
2.28k stars 305 forks source link

Feature Request: Split the RevenueCat XCFramework into Separate UI and Core Frameworks #4056

Open jesus-mg-ios opened 1 month ago

jesus-mg-ios commented 1 month ago

Description

Dear RevenueCat Team,

I am writing to request an enhancement to the RevenueCat SDK. Specifically, I propose splitting the existing XCFramework into two distinct frameworks:

  1. RevenueCatCore.xcframework: This framework would include all core functionalities related to in-app purchases and subscription management, excluding any UI components.
  2. RevenueCatUI.xcframework: This framework would exclusively contain the UI components, providing a clear separation from the core logic.

Benefits

Implementation Considerations

Thank you for considering this request. I believe this enhancement would greatly benefit the developer community by providing more flexibility and efficiency in integrating RevenueCat into their projects.

RCGitBot commented 1 month ago

👀 We've just linked this issue to our internal tracker and notified the team. Thank you for reporting, we're checking this out!

aboedo commented 1 month ago

This is definitely something that we intend to do.

Note that our current xcframework actually doesn't include the RevenueCatUI.framework, so we're already kinda doing it 😅

Meaning that RevenueCatUI is currently (and sadly) not included in any way in the .xcframework we include in releases

jesus-mg-ios commented 1 month ago

Thanks for your comment @aboedo, so this is weird because if I get the xcframework and get the binary passing through the strings command in Mac I get things like "PaywallColor", or "https://api-paywalls.revenuecat.com" "@"UIColor"16@?0@"UITraitCollection"8" "PaywallViewMode" "blurredBackgroundImage" ... and so on ... that for the name should be UI. Could you double check it?

aboedo commented 1 month ago

Yeah, I can see that being very confusing.

Those entities are all a part of the main RevenueCat SDK instead of RevenueCat UI.

Our thinking at the time was to have the split be:

The distinction is that basically RevenueCat still is responsible for handling and validating the data, and RevenueCatUI is only responsible for rendering the data. So that technically you could use the same entities and data that RevenueCat uses to display your own Paywalls independent from the rest of our system.

This also makes it easier for us to reuse a lot of the tooling and automation we build for data validation, and to ensure that we're batching and grouping requests as efficiently as possible for RevenueCat without duplicating for RevenueCatUI.

Hope that makes sense!

jesus-mg-ios commented 1 month ago

Thank you for the clarification. However, I must say that the current approach doesn't make sense to me, as the binary is growing with entities and components that I won't use.

If UI components are leaking into the core SDK, it indicates a coupling that should be avoided. The core framework should handle all data processing, validation, and networking, returning raw or processed data to the consuming framework without any UI-specific dependencies. UI-related strings and entities should reside exclusively within the RevenueCatUI framework.

To maintain a clean separation and prevent unnecessary bloat, consider creating an intermediate module for shared entities or tools. This structure could be:

Does not mean to have 3 frameworks. You can include the shared in the core, linked statically and then for people using the UI RevenueCatUI and RevenueCatCore(with the shared entities). But for me, the paywall mode and color shouldn't be inside RevenueCatCore.

Thank you for considering these suggestions.

cnordvik commented 1 month ago

Just adding my +1 here as this was the first thing we checked when considering RevenueCat, the impact of download size.

I see things like a 100KB RevenueCat_RevenueCatUI.bundle/background.jpg and the total impact of adding the SDK without doing anything is 2.0MB (8.31%) increase in download size and 6.4MB (9.85%) increase in install size. This is on an a very mature app with millions of users and has WatchApp, extensions and a fair number of third party SDKs. So this impact is pretty significant considering that our needs are basically to just sync our subscriptions to the RevenueCat backend.

CleanShot 2024-07-30 at 09 52 51

jesus-mg-ios commented 1 month ago

Just out of curiosity, is the tool you used made in-house or can I find it somewhere? @cnordvik

cnordvik commented 1 month ago

Just out of curiosity, is the tool you used made in-house or can I find it somewhere? @cnordvik

We use EmergeTools for the size comparison on PRs.

aboedo commented 3 weeks ago

Hey folks, sorry for the radio silence here, I missed notifications for the thread.

Fully agree on keeping the UI separate from the core, but that is what we're doing:

The RevenueCat framework does not have any UI components or strings files. It only contains some structs definitions for types that the RevenueCatUI framework might use.

However, this is only the definition of the structs. If you do not use the Paywalls system, then other than those definitions (which should be very small as it's just a handful of Swift files, since it's only the entities related to the data, not views or anything actually visual), there should be no impact to an app using only RevenueCat.

The reason we still packed those in was so that we could reuse the very optimized networking logic in RevenueCat. Granted, we could have split things into a separate Core, as suggested. We decided against it at the time because when keeping it all together, we could also optimize the requests themselves, and do things like ensuring that the Paywall information is packed in the same network request that gets Offerings information. Again, if you don't use Paywalls, then no extra information is added.

But if you do use Paywalls, then by packing all the information in a single request we save one round trip, and we can retain control over how the relevant entities are cached in a way that is consistent between the RevenueCat framework and RevenueCatUI.

We also make those entities public, in the hopes that if you wanted to for example build your own Paywalls system, you would be able to do so and leverage all of our entities if you want to remotely control components, just like we did.

Meaning that our RevenueCatUI system would become "one possible implementation" of how to do UI with RevenueCat. We have a long ways to go before that dream is really fulfilled, admittedly, but that was a part of the consideration with keeping the structs in the main SDK, but still ensuring that all of the strings, images, and any UI components are still kept in the RevenueCatUI SDK.

Hope this helps shed some light on things!

We do have some other optimizations planned for SDK footprint, though, stay tuned!

jesus-mg-ios commented 3 weeks ago

Thank you for your explanation, @aboedo. However, your point of view doesn't align with the data provided by cnordvik and myself. There are colors and other elements from UIKit that need to be considered. Additionally, some customers don't want the paywall system (referring to us as your customers), and we have constraints related to app size that directly affect downloads and, consequently, the conversion rate. As you know, there's a warning message when the app exceeds 200MB during installation. While it's true that RevenueCat itself isn't 200MB, we still need to use other third-party frameworks, like Firebase, which are actively working to reduce their framework sizes.

In my opinion, the solution you're using has alternative ways to decouple your UI entities from the rest one, as you're currently using them in the UI Layer.

"We decided against it at the time because when keeping it all together, we could also optimize the requests themselves, and do things like ensuring that the Paywall information is packed in the same network request that gets Offerings information"

Regarding this point where you mentioned "optimize the requests themselves," I assume the endpoint is combining everything into one. In this case, you could pass a generic parameter decodable from the RevenueCat UI, leaving all the paywall logic to the UI itself. This way, the DTO entities, like Offerings, could reside within the core framework (specifically in the data layer). You could even have two implementations: one with generics and another returning the offerings.

Well, I don't know how the framework structure is, but I'm sure that you could decode data in the same request passing one DTO or another, and the DTO itself mustn't be sticky to the request using generics.

aboedo commented 3 weeks ago

Thanks for the input. I agree that it's not impossible, but it's also not an insignificant amount of work and we do have quite a few other things to take care of, balancing can be hard.

I do want to clarify here, though, that the impact of having Paywalls-related entities in our main SDK should be very small: it's ~184.1 kb, as you can see in this Emerge tools X-Ray: https://www.emergetools.com/app/example/ios/examp_7TSGKjCkcgfF Color information within that is less than 50kb.

image

This was tested by integrating a brand new app through SPM and archiving, while only adding the RevenueCat framework. The entire RevenueCat SDK is 1.7mb.

@cnordvik based on the output that you shared, I expect that you also had the RevenueCatUI framework in there. This is where the image that you pointed to lives.

We do have plans to further optimize the RevenueCatUI framework in itself, but the overall impact of Paywalls-related data in the main SDK should be very minimal, as you can see in the X-Ray (although definitely let me know if I'm missing something!)

jesus-mg-ios commented 3 weeks ago

Seems like it is spread along the SDK not only in the paywallData. PaywallEvents, Paywall Caches for prewarming, colors, texts (that maybe is the second greater part), customerCenterConfig, PaywallViewMode, and so on .. https://www.emergetools.com/app/example/ios/examp_7TSGKjCkcgfF?search=paywall https://www.emergetools.com/app/example/ios/examp_7TSGKjCkcgfF?search=color So yes, @aboedo I can see the hard work to do.

In that way in my case I would prefer an effort on creating a lightweight sdk for extensions https://github.com/RevenueCat/purchases-ios/issues/3985 Not because the size of the SDK that would be nice to reduce it, but the objects created and managed on execution time that are useless for this kinda extensions and we have a very tight memory to use in runtime.