How to Change Scroll View Color in SwiftUI
This is harder than it should be
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.
- Your
ScrollView
has to be directly inside aNavigationView
(not navigated to) - Your
ScrollViews
content must scale horizontally using anHStack
withSpacers
to force theScrollView
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.