shaps80 / SwiftUIBackports

A collection of SwiftUI backports for iOS, macOS, tvOS and watchOS
MIT License
931 stars 59 forks source link

Add an option to fallback on Backport when first-party native API is not available? #68

Closed edonv closed 2 months ago

edonv commented 6 months ago

Describe the feature

A new public EnvironmentValue (and optionally a public-facing ViewModifier) that enables or disables internal use of the first-party APIs that this package backports when running on a device where it's available?

Is your feature request related to a problem?

Something I find myself doing when using SwiftUIBackports is creating local modifiers and views that check the OS version (#available) and return the native API when it's available and fallback on this package's backports for older OS'es. It'd be super awesome if this could be added as a built-in feature.

Proposed solution

A private EnvironmentKey of type Bool. Maybe the KeyPath for EnvironmentValues would be called something like \.preferNativeAPIWhenAvailable (which would be set to true when the developer wants to use the first-party API when the device is on an OS version that supports the native API). This could also be confusing because it doesn't refer to SwiftUIBackports in the name, so it might not be clear its purpose (aside from good documentation). Or it could be called \.preferBackportOnNewerOS, which would make its value imply the opposite.

This could also be implemented in SwiftBackports.

Additional context

An example of my current solution (which I'm suggesting be implemented in SwiftUIBackports) is as follows:

struct BackportNavigationLink<P: Hashable, Label: View>: View {
    var value: P?
    var label: () -> Label

    @ViewBuilder
    var body: some View {
        if #available(iOS 16, *) {
            NavigationLink(value: value, label: label)
        } else {
            Backport.NavigationLink(value: value, label: label)
        }
    }
}

The only difference in the implementation would be this:

struct BackportNavigationLink<P: Hashable, Label: View>: View {
    @Environment(\.preferBackportOnNewerOS)
    private var preferBackportOnNewerOS

    var value: P?
    var label: () -> Label

    @ViewBuilder
    var body: some View {
        if #available(iOS 16, *), !preferBackportOnNewerOS {
            NavigationLink(value: value, label: label)
        } else {
            Backport.NavigationLink(value: value, label: label)
        }
    }
}
shaps80 commented 6 months ago

This is a really interesting idea actually. I had considered baking this in before, but I wanted the API consumer to have consistency and sometimes a backport may not be 1-2-1 since I have limited knowledge in some cases as to how something should be implemented.

However this solves that problem by allowing the consumer to make this decision, right at the usage or even app-wide.

I love this! Would you be keen on raising a PR that updates the current code to this? I'm quite busy atm and wouldn't have the time this requires to update and test properly.

The SwiftUIBackportsDemo repo can be used to verify this I think which will aide in testing. I'd def be interested in a PR for this šŸ‘

edonv commented 6 months ago

Sounds good! I'll see if I have some time in the next few days or so to throw something together. Would you be interested in this being added to every single backport in the package? Happy to do so. And if it goes well, maybe I'll fork and do the same thing with SwiftBackports. I will admit, I'm not a pro at testing, but would be happy see what I can do.

shaps80 commented 6 months ago

Yeah the testing will be key, against each simulator to ensure we don't break anyway, but I'd def see this as a larger update that applies to all, to ensure consistency if you're up for it.

edonv commented 6 months ago

Looks like I can install simulators back to iOS 14, but iOS 13 would be good to have. Not sure why it isn't available to me in Xcode 15.0.1, but at least for now? I'll start things out and start a checklist for testing.

edonv commented 6 months ago

What do we think would be a good name for the property? And a default value? Should the default be to use the native API when available? So should people have to opt-in to use the native when available or "opt-out"?

I figure the only thing public facing would be a single ViewModifier function at a global level (or at a local level when needed) that takes a Bool.

I'm thinking the modifier would be called .preferNativeAPIWhenAvailable(_ preferNativeAPI: Bool) if the default is false.

shaps80 commented 6 months ago

I agree with the approach. To be honest in keeping with 'current' expectations we should probably make it false by default, because then nothing changes unless you use that API.

That being said, lets go the other way and I'll release this as a major update with notes.

So as you described, the user must OPT-OUT or else native APIs will be used by default (when available).

Therefore naming, I think we should go with: func backportPreferred(_ preferred: Bool)

The same name for the EnvironmentKey etc for consistency.

This aligns well with the current environment backports, where the prefix is always backport and I think its clear enough with documentation and auto-complete.

I don't think its necessary to include the word API since we'd never use this anywhere else so there shouldn't be any ambiguity.

edonv commented 2 months ago

Just saw that this got closed. I actually mostly finished this, but didn't have the time to get back to it and really finalize it. Unfortunately, I just don't have the time to catch back up on it and commit my work. Let me know if you want me to drop my files in here.

shaps80 commented 2 months ago

@edonv hey yeah I figured you may have lost track of it and I was just doing some cleanup. If you can drop a zipped project here I'll take a look when I have some time thanks šŸ™

Appreciate the effort.

edonv commented 2 months ago

Give it a peek. There are some of the backports that just aren't able to take advantage of this, but I tried to implement it anywhere I could. You'll want to check Environment+preferNativeAPIWhenAvailable.swift, which creates the internally-used \.backportPreferred Environment value. This would be set by using the public .backportPreferred(_:) modifier.

SwiftUIBackports.zip