Using iOS 16 view modifiers with iOS 15 targets

With each new version of iOS, Apple introduces new SwiftUI features to add new features and plug holes in the framework. This article provides a solution for using new view modifiers that ship with new iOS versions--without raising the app's minimum iOS target setting.

What are View Modifiers

If you've used SwiftUI you've almost certainly used View Modifiers.  Basically a view modifier is a builder-like syntax to take a view as an input and modify it in some way. These modifications can be modifications, like changing a font, or transformative, such as embedding a Text view inside a Frame view.

By way of example, here's a simple view modifier to change the font of text in a text view. Probably you've used this view modifier before.

Text("Hello, World")
  .font(.caption2)

Apple adds new view modifiers with every new version of SwiftUI.  New view modifiers only work with the version of iOS they first shipped with (and later versions).  While it's nice to get new view modifiers, the lack of backward compatibility means we can't use new modifiers without raising the target iOS version for our app to the version that first supported the modifier (or a later version).  Raising the target iOS version effectively cuts off devices running older iOS versions from future updates.

But there is a way to work around this issue, which I've used to good effect.  In this article I'll walk through how to use the new iOS 16+ List view modifier .scrollContentBackground in a project that retains iOS 15 (or older) as a minimum target.

What this Solution Cannot Do

This solution enables you to use iOS 16 modifiers on iOS 16 devices while not excluding iOS 15 users from running your app.  It will not make new view modifiers work on older versions of iOS. So, you'll need to keep in mind that whatever a new iOS 16 view modifier does works only on iOS 16+ devices. For lower iOS versions, some other code needs to run to apply the UI effect to lower versions (or if we can do without the modifier's effect on < iOS 16 devices then that may be fine too).

Ok Let's get started.

The Problem to Solve

Before iOS 16, there wasn't a SwiftUI view modifier to set the background of a List to Color.clear. This meant that if we used a custom View background, the scroll area of a list would look like below. The problem is that the background of the List scroll area is gray, rather than yellow.  This isn't what we want...we want yellow behind the List cells.

List background without modification

Solution Before iOS 16

Before iOS 16, a List (on iOS) is backed by UITableView, so we can work around the lack of a modifier in various ways--the easiest is to force all UITableView scrollview backgrounds to have a .clear color. We can accomplish this by adding the following to the ContentView.init() method. Or we could add it at app startup...it doesn't matter where, as long as the code executes before the List is instantiated.

init() {
  UITableView.appearance().backgroundColor = .clear
}

Below is the result on iOS 15 and iOS 16 of using the UIKit modification above to achieve the transparent List background.

iOS 15 UITableView modification applied

This "fixes" the problem on iOS 15 and earlier (Right), since it changes the global appearance of UITableView.

As you can see on the Left, this solution has a big problem--it doesn't work on iOS 16!

Why?

While List on iOS 15 is backed by UITableView, it's evidently been re-implemented as a native List on iOS 16, so the UITableView modification has no effect.  On iOS 16, the List background reverts to gray.

So we have a problem:

  • The UITableView modification for iOS <16 doesn't work on iOS 16 (or on native macOS, for example, because macOS isn't UIKit based)
  • The new scrollContentBackground approach for iOS 16+ doesn't work on on <= iOS15. It won't even compile when targeting < iOS 16 because Xcode can see the compatibility issue at compile-time.

OK, enough background, let's get into the solution.

Custom View Modifier Solution

As with anything in programming, there are undoubtedly multiple solutions for this problem. Here's mine, and it works pretty well for me. You may have a better solution and I'd be interested to know what it is.

Basically the solution is to implement both the UITableView solution and the .scrollContentBackground solution in the same code, and have the app execute the appropriate solution for whatever iOS version it's installed on.

What's a Custom View Modifier?

A custom view modifier is just what it says -- a modifier we implement that can be applied to a View the same way Apple's own modifiers are.  If we implemented a custom modifier called .myModifier, it might be used as so:

Text("Hello, World")
  .font(.caption2)
  .myModifier()

The SwiftUI compiler would apply the .font modifier, then call our custom code to apply whatever modification we coded within .myModifier to the Text view.

Let's make a modifier

For this solution I'll create a custom modifier that applies the Apple .scrollContentBackground modifier to a List -- but only if iOS 16 features are available.  What if we're running on iOS less than 16?  Well, since the modifier doesn't exist before iOS 16, the custom modifier does nothing and just returns the View in its original state.

Here's the modifier code:

struct ClearListBackgroundModifier: ViewModifier {
    func body(content: Content) -> some View {
        if #available(iOS 16.0, *) {
            content.scrollContentBackground(.hidden)
        } else {
            content
        }
    }
}

Here's what this code does:

  1. Starting with the content of some View (but really it has to be a List), the modifier checks if the app supports iOS 16 features. If so, it applies the scrollContentBackground modifier, hiding the list background view.  This will result in the background under the List becoming visible behind the cells, as we want.
  2. On iOS less than 16.0, the content is returned with no modifications.

Add a View extension

To make adding the ViewModifier to the List easier, I'll add an extension to the SwiftUI View type.

extension View {
    func clearListBackground() -> some View {
        modifier(ClearListBackgroundModifier())
    }
}

With that done, we can now just add .clearListBackground() as a modifier to the List, like so:

List(1...10, id: \.self) { i in
   Text("List Item \(i)")
}
.clearListBackground()  // This is the custom modifier being applied to the List

If we had made no other customizations, then with the app on iOS 16 (Left) and iOS 15 (Right), we have the following output:

iOS 16 Modifier Applied

You can see on the Left (iOS 16), the problem is solved! However....on iOS 15, nothing is solved.

Add the iOS 15 Solution

Assuming we can't think of a better solution for < iOS 16, we'll still use the UITableView modification to remove the List background on iOS 15.

Let's add the UITableView solution, but this time execute the code only when iOS 16 features aren't available:

init() {
  if #unavailable(iOS 16.0) {
    UITableView.appearance().backgroundColor = .clear
  }
}

With this code applied, we now get the same result on iOS 16 and on iOS 15!

Solution for iOS 16 and prior versions

Get the Code

The project used to demonstrate this technique is available on my GitHub repository.  Check it out here.