How to Change Scroll View Color in SwiftUI

This is harder than it should be

Zane Carter
Better Programming

--

Photo by DocuSign on Unsplash

The release of iOS 15 brought some much-needed improvements to SwiftUI. We now have native search bars and even mark-down support for Text views.

But somehow, the SwiftUI ScrollView is still a bit of a buggy mess. This guy from Reddit put it quite nicely:

Something like changing the background color of a SwiftUI ScrollView still presents quite a challenge. The intuitive thing to do is to use the .background modifier directly on the ScrollView, like so:

ScrollView {
Text(“Hello, World”)
}
.background(Color.purple)

I’ve found this does work in certain circumstances, with a few caveats.

  1. Your ScrollView has to be directly inside a NavigationView (not navigated to)
  2. Your ScrollViews content must scale horizontally using an HStack with Spacers to force the ScrollView to fill the screen width (or set its frame manually)

If either of these caveats is not met you’ll end up with unwanted behavior.

Either your ScrollView shows in the middle with a different color from the actual view’s background (less problematic) or if you don’t wrap your ScrollView directly in a NavigationView, the ScrollView will not automatically collapse the Navigation Bar — it goes under it — as if the Navigation Bar is transparent.

Definitely not an ideal solution!

In my own use case, I found myself using a ScrollView inside a details screen (navigated to from another screen).

This means I had no way to use another NavigationView around the ScrollView (see caveat 2). And wrapping my ScrollView in two layers of NavigationViews (and maybe hiding one?) just doesn’t scream stability to me.

If you’ve been googling these issues as long as I have, you probably came across solutions using the appearance() modifier.

Usually, something like below, when I wanted my ScrollViews background to be changed to pink:

UIScrollView.appearance().backgroundColor = UIColor.systemPink

On first inspection, things work great. You get the exact behavior you’d expect from a UIKit ScrollView. The navigation bar collapses and all is well…

Until you try to use a TextField. When editing a TextField, the background colour of your TextField changes to the colour of your ScrollView. You can see an example of it happening below (right):

Why the hell does this happen? I have no idea. Using the appearance() modifier is sort of frowned upon but it’s the only way to get some things working in SwiftUI.

Regardless, it makes no sense that it should also change the color of completely unrelated components.

Using the appearance() modifier like this changes both TextFields background colors (when editing them) as well as (somehow) the keyboard autocorrect area. I’m guessing this has something to do with some magical SwiftUI voo-doo.

We’re getting closer but it’s not really a solution if you have to break every TextField in your app to get it working.

After a whole bunch of fiddling and testing, I found an appearance() modifier that seems to contain the damage.

You can actually specify conditions for appearance modifiers. This is useful for UIKit because you can specify View Controllers where you want the styles to apply. You can also specify a UITraitCollection to filter down where the styles apply even further.

With a bit of trial and error, I found the combination that works pretty well:

There are two parts to this:

First, we specify userInterfaceLevel to apply to only areas where the interface level is considered as base.

The Apple docs say this is basically just your windows main content. There’s also the value of elevated which I can only assume is Alerts or Sheets.

Then we specify the whenContainedInInstancesOf property to be :

...whenContainedInInstancesOf: [UINavigationController.self]...

The combination of these conditions stops the ScrollView appearance() modifier breaking our TextFields and our keyboards.

Specifying the whenContainedInInstancesOf property fixes the keyboard autocorrection color to be the default grey. My guess is this is because the keyboard is not contained in a Navigation Controller as it’s not part of our app (it’s presented by iOS).

And then, specifying the userInterfaceLevel fixes the TextField background. I’m not sure why this is. You would think any view you have in your app could be considered as a base user interface level. Maybe the part of the TextField that changes background color is considered as elevated.

One unfortunate side effect is that this will change every ScrollView’s colour, everywhere in your app. But if you have consistent styling across your app then this solution may just be what you’re looking for.

To use it, put the appearance modifier in your root view or if you’d like to keep it separate you can use a helper view like the one I made.

I hope this article helped you navigate the tricky and treacherous world of SwiftUI. Hopefully, things improve in iOS 16 (knock on wood).

Thanks for reading.

Want to Connect With the Author?Give me a follow on Twitter to see what I’m working on.

--

--

I make apps for the AppStore. Currently working on an app that makes gardening easier. Twitter: @iamzanecarter